diff --git a/contract/Cargo.lock b/contract/Cargo.lock index 1013d81b..68e4a5f6 100644 --- a/contract/Cargo.lock +++ b/contract/Cargo.lock @@ -553,9 +553,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9f351a4e4d81ef7c890e44d903f8c0bdcdc00f094fd3a181eaf70c0eec7a3a" +checksum = "1c4a657e5caacc3a0d00ee96ca8618745d050b8f757c709babafb81208d4239c" dependencies = [ "cosmwasm-schema", "cosmwasm-std", diff --git a/contract/Cargo.toml b/contract/Cargo.toml index e74485ac..4f4eb459 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -29,7 +29,7 @@ cosmwasm-schema = "1.5.0" cosmwasm-std = { version = "1.5.0", features = ["cosmwasm_1_1"] } cw-ownable = "0.5.1" cw-storage-plus = "1.2.0" -cw-utils = "1.0.2" +cw-utils = "1.0.3" cw2 = "1.1.1" hex = "0.4.3" serde_json = "1.0.108" diff --git a/contract/src/contract.rs b/contract/src/contract.rs index 9431e1b1..7f655ed0 100644 --- a/contract/src/contract.rs +++ b/contract/src/contract.rs @@ -194,6 +194,9 @@ pub fn execute( account_sequence, number_of_tickets, ), + ExecuteMsg::RecoverXRPLTokenRegistration { issuer, currency } => { + recover_xrpl_token_registration(deps.into_empty(), info.sender, issuer, currency) + } ExecuteMsg::SaveSignature { operation_id, signature, @@ -595,6 +598,53 @@ fn recover_tickets( .add_attribute("account_sequence", account_sequence.to_string())) } +fn recover_xrpl_token_registration( + deps: DepsMut, + sender: Addr, + issuer: String, + currency: String, +) -> CoreumResult { + assert_owner(deps.storage, &sender)?; + + let key = build_xrpl_token_key(issuer.to_owned(), currency.to_owned()); + + let mut token = XRPL_TOKENS + .load(deps.storage, key.to_owned()) + .map_err(|_| ContractError::TokenNotRegistered {})?; + + // Check that the token is in inactive state, which means the trust set operation failed. + if token.state.ne(&TokenState::Inactive) { + return Err(ContractError::XRPLTokenNotInactive {}); + } + + // Put the state back to Processing since we are going to try to activate it again. + token.state = TokenState::Processing; + XRPL_TOKENS.save(deps.storage, key, &token)?; + + // Create the pending operation to approve the token again + let config = CONFIG.load(deps.storage)?; + let ticket = allocate_ticket(deps.storage)?; + + create_pending_operation( + deps.storage, + Some(ticket), + None, + OperationType::TrustSet { + issuer: issuer.to_owned(), + currency: currency.to_owned(), + trust_set_limit_amount: config.trust_set_limit_amount, + }, + )?; + + Ok(Response::new() + .add_attribute( + "action", + ContractActions::RecoverXRPLTokenRegistration.as_str(), + ) + .add_attribute("issuer", issuer) + .add_attribute("currency", currency)) +} + fn save_signature( deps: DepsMut, sender: Addr, @@ -740,7 +790,7 @@ fn query_xrpl_tokens( limit: Option, ) -> StdResult { let limit = limit.unwrap_or(MAX_PAGE_LIMIT).min(MAX_PAGE_LIMIT); - let offset = offset.unwrap_or(0); + let offset = offset.unwrap_or_default(); let tokens: Vec = XRPL_TOKENS .range(deps.storage, None, None, Order::Ascending) .skip(offset as usize) @@ -758,7 +808,7 @@ fn query_coreum_tokens( limit: Option, ) -> StdResult { let limit = limit.unwrap_or(MAX_PAGE_LIMIT).min(MAX_PAGE_LIMIT); - let offset = offset.unwrap_or(0); + let offset = offset.unwrap_or_default(); let tokens: Vec = COREUM_TOKENS .range(deps.storage, None, None, Order::Ascending) .skip(offset as usize) diff --git a/contract/src/error.rs b/contract/src/error.rs index 67e45542..3d2c6464 100644 --- a/contract/src/error.rs +++ b/contract/src/error.rs @@ -124,12 +124,17 @@ pub enum ContractError { #[error("XRPLTokenNotEnabled: This token must be enabled to be bridged")] XRPLTokenNotEnabled {}, - #[error("CoreumOriginatedTokenDisabled: This token is currently disabled and can't be bridged")] + #[error( + "CoreumOriginatedTokenDisabled: This token is currently disabled and can't be bridged" + )] CoreumOriginatedTokenDisabled {}, #[error("XRPLTokenNotInProcessing: This token must be in processing state to be enabled")] XRPLTokenNotInProcessing {}, + #[error("XRPLTokenNotInactive: To recover this token it must be inactive")] + XRPLTokenNotInactive {}, + #[error("AmountSentIsZeroAfterTruncation: Amount sent is zero after truncating to sending precision")] AmountSentIsZeroAfterTruncation {}, diff --git a/contract/src/msg.rs b/contract/src/msg.rs index 4505dac1..aea00610 100644 --- a/contract/src/msg.rs +++ b/contract/src/msg.rs @@ -41,6 +41,11 @@ pub enum ExecuteMsg { account_sequence: u64, number_of_tickets: Option, }, + #[serde(rename = "recover_xrpl_token_registration")] + RecoverXRPLTokenRegistration { + issuer: String, + currency: String, + }, SaveSignature { operation_id: u64, signature: String, diff --git a/contract/src/operation.rs b/contract/src/operation.rs index fe2f8605..b672673d 100644 --- a/contract/src/operation.rs +++ b/contract/src/operation.rs @@ -42,7 +42,7 @@ pub fn check_operation_exists( ticket_sequence: Option, ) -> Result { // Get the sequence or ticket number (priority for sequence number) - let operation_id = account_sequence.unwrap_or(ticket_sequence.unwrap_or_default()); + let operation_id = account_sequence.unwrap_or_else(|| ticket_sequence.unwrap()); if !PENDING_OPERATIONS.has(storage, operation_id) { return Err(ContractError::PendingOperationNotFound {}); @@ -64,7 +64,7 @@ pub fn create_pending_operation( operation_type, }; - let operation_id = ticket_sequence.unwrap_or(account_sequence.unwrap_or_default()); + let operation_id = ticket_sequence.unwrap_or_else(|| account_sequence.unwrap()); if PENDING_OPERATIONS.has(storage, operation_id) { return Err(ContractError::PendingOperationAlreadyExists {}); } diff --git a/contract/src/state.rs b/contract/src/state.rs index 9c505b9c..9ce16278 100644 --- a/contract/src/state.rs +++ b/contract/src/state.rs @@ -141,6 +141,7 @@ pub enum ContractActions { RegisterXRPLToken, SendFromXRPLToCoreum, RecoverTickets, + RecoverXRPLTokenRegistration, XRPLTransactionResult, SaveSignature, SendToXRPL, @@ -154,6 +155,7 @@ impl ContractActions { ContractActions::RegisterXRPLToken => "register_xrpl_token", ContractActions::SendFromXRPLToCoreum => "send_from_xrpl_to_coreum", ContractActions::RecoverTickets => "recover_tickets", + ContractActions::RecoverXRPLTokenRegistration => "recover_xrpl_token_registration", ContractActions::XRPLTransactionResult => "submit_xrpl_transaction_result", ContractActions::SaveSignature => "save_signature", ContractActions::SendToXRPL => "send_to_xrpl", diff --git a/contract/src/tests.rs b/contract/src/tests.rs index 22679204..915c4b10 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -3842,6 +3842,202 @@ mod tests { assert_eq!(query_available_tickets.tickets, tickets.clone()); } + #[test] + fn xrpl_token_registration_recovery() { + let app = CoreumTestApp::new(); + let signer = app + .init_account(&coins(100_000_000_000, FEE_DENOM)) + .unwrap(); + let wasm = Wasm::new(&app); + let asset_ft = AssetFT::new(&app); + + let relayer = Relayer { + coreum_address: Addr::unchecked(signer.address()), + xrpl_address: generate_xrpl_address(), + xrpl_pub_key: generate_xrpl_pub_key(), + }; + + let token_issuer = generate_xrpl_address(); + let token_currency = "BTC".to_string(); + let token = XRPLToken { + issuer: token_issuer.to_owned(), + currency: token_currency.to_owned(), + sending_precision: -15, + max_holding_amount: 100, + }; + + let contract_addr = store_and_instantiate( + &wasm, + &signer, + Addr::unchecked(signer.address()), + vec![relayer.clone()], + 1, + 2, + Uint128::new(TRUST_SET_LIMIT_AMOUNT), + query_issue_fee(&asset_ft), + generate_xrpl_address(), + ); + + // We successfully recover 3 tickets to perform operations + wasm.execute::( + &contract_addr, + &ExecuteMsg::RecoverTickets { + account_sequence: 1, + number_of_tickets: Some(3), + }, + &vec![], + &signer, + ) + .unwrap(); + + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(generate_hash()), + account_sequence: Some(1), + ticket_sequence: None, + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::TicketsAllocation { + tickets: Some(vec![1, 2, 3]), + }, + }, + }, + &vec![], + &signer, + ) + .unwrap(); + + // We perform the register token operation, which should put the token to Processing state and create the PendingOperation + wasm.execute::( + &contract_addr, + &ExecuteMsg::RegisterXRPLToken { + issuer: token.issuer.clone(), + currency: token.currency.clone(), + sending_precision: token.sending_precision, + max_holding_amount: Uint128::new(token.max_holding_amount), + }, + &query_issue_fee(&asset_ft), + &signer, + ) + .unwrap(); + + // If we try to recover a token that is not in Inactive state, it should fail. + let recover_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::RecoverXRPLTokenRegistration { + issuer: token.issuer.clone(), + currency: token.currency.clone(), + }, + &vec![], + &signer, + ) + .unwrap_err(); + + assert!(recover_error + .to_string() + .contains(ContractError::XRPLTokenNotInactive {}.to_string().as_str())); + + // If we try to recover a token that is not registered, it should fail + let recover_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::RecoverXRPLTokenRegistration { + issuer: token.issuer.clone(), + currency: "NOT".to_string(), + }, + &vec![], + &signer, + ) + .unwrap_err(); + + assert!(recover_error + .to_string() + .contains(ContractError::TokenNotRegistered {}.to_string().as_str())); + + // Let's fail the trust set operation to put the token to Inactive so that we can recover it + + let query_pending_operations = wasm + .query::( + &contract_addr, + &QueryMsg::PendingOperations {}, + ) + .unwrap(); + + assert_eq!(query_pending_operations.operations.len(), 1); + + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(generate_hash()), + account_sequence: None, + ticket_sequence: Some( + query_pending_operations.operations[0] + .ticket_sequence + .unwrap(), + ), + transaction_result: TransactionResult::Rejected, + operation_result: OperationResult::TrustSet { + issuer: token.issuer.clone(), + currency: token.currency.clone(), + }, + }, + }, + &[], + &signer, + ) + .unwrap(); + + let query_pending_operations = wasm + .query::( + &contract_addr, + &QueryMsg::PendingOperations {}, + ) + .unwrap(); + + assert!(query_pending_operations.operations.is_empty()); + + // We should be able to recover the token now + wasm.execute::( + &contract_addr, + &ExecuteMsg::RecoverXRPLTokenRegistration { + issuer: token.issuer.clone(), + currency: token.currency.clone(), + }, + &vec![], + &signer, + ) + .unwrap(); + + let query_pending_operations = wasm + .query::( + &contract_addr, + &QueryMsg::PendingOperations {}, + ) + .unwrap(); + + assert_eq!(query_pending_operations.operations.len(), 1); + assert_eq!( + query_pending_operations.operations[0], + Operation { + ticket_sequence: Some( + query_pending_operations.operations[0] + .ticket_sequence + .unwrap() + ), + account_sequence: None, + signatures: vec![], + operation_type: OperationType::TrustSet { + issuer: token_issuer, + currency: token_currency, + trust_set_limit_amount: Uint128::new(TRUST_SET_LIMIT_AMOUNT), + }, + } + ); + } + #[test] fn rejected_ticket_allocation_with_no_tickets_left() { let app = CoreumTestApp::new(); diff --git a/contract/src/tickets.rs b/contract/src/tickets.rs index 0959b4f0..7a86221c 100644 --- a/contract/src/tickets.rs +++ b/contract/src/tickets.rs @@ -40,7 +40,7 @@ pub fn register_used_ticket(storage: &mut dyn Storage) -> Result= config.used_ticket_sequence_threshold && !PENDING_TICKET_UPDATE.load(storage)? { - // If our creation of a ticket allocation operation failed because we have no tickets left, we need to propagate + // If our creation of a ticket allocation operation failed because we have no tickets left, we need to propagate // this so that we are aware that we need to allocate new tickets because we've run out of them match reserve_ticket(storage) { Ok(ticket_to_update) => {