From 9ea11bc32758f822d06a74648bd7b42bb66e3a08 Mon Sep 17 00:00:00 2001 From: Keyne Date: Mon, 27 Nov 2023 16:25:13 +0000 Subject: [PATCH] Contract: XRPL Token Registration Recovery (#51) # Description - Add recovery of XRPL token registration that failed the trust set operation. Contract owner will have to call the recovery that will only succeed if the token is registered and is in inactive state because the previous trust set operation has failed. --- contract/Cargo.lock | 4 +- contract/Cargo.toml | 2 +- contract/src/contract.rs | 54 ++++++++++- contract/src/error.rs | 7 +- contract/src/msg.rs | 5 + contract/src/operation.rs | 4 +- contract/src/state.rs | 2 + contract/src/tests.rs | 196 ++++++++++++++++++++++++++++++++++++++ contract/src/tickets.rs | 2 +- 9 files changed, 267 insertions(+), 9 deletions(-) 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) => {