From 621eebb0a1c94aaa5bc4d620c849f7e46619ff70 Mon Sep 17 00:00:00 2001 From: Keyne Date: Tue, 19 Dec 2023 16:27:15 +0000 Subject: [PATCH] Contract: Token status update implementation (#64) # Description - Allow admin to disable/enable tokens (both Coreum or XRPL) to block them from being bridged / or bridged back --- contract/Cargo.lock | 58 +++--- contract/Cargo.toml | 2 +- contract/src/contract.rs | 70 +++++-- contract/src/error.rs | 6 + contract/src/fees.rs | 2 +- contract/src/lib.rs | 1 + contract/src/msg.rs | 16 +- contract/src/operation.rs | 3 +- contract/src/state.rs | 4 + contract/src/tests.rs | 397 +++++++++++++++++++++++++++++++++++++- contract/src/token.rs | 37 ++++ go.work.sum | 14 +- 12 files changed, 559 insertions(+), 51 deletions(-) create mode 100644 contract/src/token.rs diff --git a/contract/Cargo.lock b/contract/Cargo.lock index 3f4acbd2..85182b35 100644 --- a/contract/Cargo.lock +++ b/contract/Cargo.lock @@ -51,7 +51,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -130,7 +130,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.40", "which", ] @@ -733,9 +733,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" +checksum = "8bbb8258be8305fb0237d7b295f47bb24ff1b136a535f473baf40e70468515aa" dependencies = [ "indenter", "once_cell", @@ -838,7 +838,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1019,9 +1019,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1147,9 +1147,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" @@ -1188,9 +1188,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libloading" @@ -1404,7 +1404,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1448,7 +1448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1503,7 +1503,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1646,9 +1646,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.26" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", @@ -1684,9 +1684,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "same-file" @@ -1827,7 +1827,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1860,7 +1860,7 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -1999,9 +1999,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e" dependencies = [ "proc-macro2", "quote", @@ -2157,7 +2157,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2205,9 +2205,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ "backtrace", "bytes", @@ -2228,7 +2228,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] [[package]] @@ -2398,7 +2398,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-shared", ] @@ -2420,7 +2420,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2652,5 +2652,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.40", ] diff --git a/contract/Cargo.toml b/contract/Cargo.toml index 3a5211b4..acf926f8 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -30,7 +30,7 @@ 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.3" -cw2 = "1.1.1" +cw2 = "1.1.2" hex = "0.4.3" serde_json = "1.0.108" sha2 = "0.10.8" diff --git a/contract/src/contract.rs b/contract/src/contract.rs index 33c973fc..2f3367ac 100644 --- a/contract/src/contract.rs +++ b/contract/src/contract.rs @@ -26,6 +26,7 @@ use crate::{ tickets::{ allocate_ticket, handle_ticket_allocation_confirmation, register_used_ticket, return_ticket, }, + token::{build_xrpl_token_key, is_token_xrp, set_token_state}, }; use coreum_wasm_sdk::{ @@ -62,8 +63,8 @@ pub const XRPL_TOKENS_DECIMALS: u32 = 15; // For more info check https://xrpl.org/transfer-fees.html#technical-details pub const XRPL_ZERO_TRANSFER_RATE: Uint128 = Uint128::new(1000000000); -const XRP_CURRENCY: &str = "XRP"; -const XRP_ISSUER: &str = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; +pub const XRP_CURRENCY: &str = "XRP"; +pub const XRP_ISSUER: &str = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; // Initial values for the XRP token that can be modified afterwards. const XRP_DEFAULT_SENDING_PRECISION: i32 = 6; @@ -236,6 +237,14 @@ pub fn execute( ExecuteMsg::SendToXRPL { recipient } => { send_to_xrpl(deps.into_empty(), env, info, recipient) } + ExecuteMsg::UpdateXRPLToken { + issuer, + currency, + state, + } => update_xrpl_token(deps.into_empty(), info.sender, issuer, currency, state), + ExecuteMsg::UpdateCoreumToken { denom, state } => { + update_coreum_token(deps.into_empty(), info.sender, denom, state) + } ExecuteMsg::ClaimFees {} => claim_fees(deps.into_empty(), info.sender), } } @@ -879,6 +888,52 @@ fn send_to_xrpl( .add_attribute("coin", funds.to_string())) } +fn update_xrpl_token( + deps: DepsMut, + sender: Addr, + issuer: String, + currency: String, + state: Option, +) -> 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 {})?; + + set_token_state(&mut token.state, state)?; + + XRPL_TOKENS.save(deps.storage, key, &token)?; + + Ok(Response::new() + .add_attribute("action", ContractActions::UpdateXRPLToken.as_str()) + .add_attribute("issuer", issuer) + .add_attribute("currency", currency)) +} + +fn update_coreum_token( + deps: DepsMut, + sender: Addr, + denom: String, + state: Option, +) -> CoreumResult { + assert_owner(deps.storage, &sender)?; + + let mut token = COREUM_TOKENS + .load(deps.storage, denom.to_owned()) + .map_err(|_| ContractError::TokenNotRegistered {})?; + + set_token_state(&mut token.state, state)?; + + COREUM_TOKENS.save(deps.storage, denom.to_owned(), &token)?; + + Ok(Response::new() + .add_attribute("action", ContractActions::UpdateCoreumToken.as_str()) + .add_attribute("denom", denom)) +} + fn claim_fees(deps: DepsMut, sender: Addr) -> CoreumResult { assert_relayer(deps.as_ref(), sender.clone())?; @@ -986,13 +1041,6 @@ fn check_issue_fee(deps: &DepsMut, info: &MessageInfo) -> Result< Ok(()) } -pub fn build_xrpl_token_key(issuer: String, currency: String) -> String { - // Issuer+currency is the key we use to find an XRPL - let mut key = issuer; - key.push_str(currency.as_str()); - key -} - pub fn validate_xrpl_issuer_and_currency( issuer: String, currency: String, @@ -1130,10 +1178,6 @@ fn truncate_and_convert_amount( Ok((converted_amount, remainder)) } -fn is_token_xrp(issuer: String, currency: String) -> bool { - issuer == XRP_ISSUER && currency == XRP_CURRENCY -} - fn convert_currency_to_xrpl_hexadecimal(currency: String) -> String { // Fill with zeros to get the correct hex representation in XRPL of our currency. format!("{:0<40}", hex::encode(currency)).to_uppercase() diff --git a/contract/src/error.rs b/contract/src/error.rs index 8879dd62..ae516d23 100644 --- a/contract/src/error.rs +++ b/contract/src/error.rs @@ -157,6 +157,12 @@ pub enum ContractError { #[error("CannotCoverBridgingFees: The amount sent is not enough to cover the bridging fees")] CannotCoverBridgingFees {}, + #[error("TokenStateIsImmutable: Current token state is immutable")] + TokenStateIsImmutable {}, + + #[error("InvalidTargetTokenState: A token state can only be updated to enabled or disabled")] + InvalidTargetTokenState {}, + #[error("InvalidTransferRate: The transfer rate sent is invalid, it must be more than 1000000000 (0%) and less or equal than 2000000000 (100%)")] InvalidTransferRate {}, } diff --git a/contract/src/fees.rs b/contract/src/fees.rs index 7aa6a61e..5eb2fe95 100644 --- a/contract/src/fees.rs +++ b/contract/src/fees.rs @@ -57,7 +57,7 @@ pub fn handle_fee_collection( Ok(fee_collected) } -pub fn collect_fees(storage: &mut dyn Storage, fee: Coin) -> Result<(), ContractError> { +fn collect_fees(storage: &mut dyn Storage, fee: Coin) -> Result<(), ContractError> { // We only collect fees if there is something to collect // If for some reason there is a coin that we are not charging fees for, we don't collect it if !fee.amount.is_zero() { diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 518a7007..859ff611 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -10,3 +10,4 @@ pub mod state; #[cfg(test)] mod tests; pub mod tickets; +pub mod token; diff --git a/contract/src/msg.rs b/contract/src/msg.rs index 533e3be9..4388ac08 100644 --- a/contract/src/msg.rs +++ b/contract/src/msg.rs @@ -4,7 +4,7 @@ use cw_ownable::{cw_ownable_execute, cw_ownable_query}; #[allow(unused_imports)] use crate::state::{Config, CoreumToken, XRPLToken}; -use crate::{evidence::Evidence, operation::Operation, relayer::Relayer}; +use crate::{evidence::Evidence, operation::Operation, relayer::Relayer, state::TokenState}; #[cw_serde] pub struct InstantiateMsg { @@ -65,6 +65,20 @@ pub enum ExecuteMsg { SendToXRPL { recipient: String, }, + // All fields that can be updatable for XRPL originated tokens will be updated with this message + // They are all optional, so any fields that have to be updated can be included in the message. + #[serde(rename = "update_xrpl_token")] + UpdateXRPLToken { + issuer: String, + currency: String, + state: Option, + }, + // All fields that can be updatable for Coreum tokens will be updated with this message. + // They are all optional, so any fields that have to be updated can be included in the message. + UpdateCoreumToken { + denom: String, + state: Option, + }, // Any relayer can claim fees at any point in time. They will be distributed proportionally among all of them. ClaimFees {}, } diff --git a/contract/src/operation.rs b/contract/src/operation.rs index 8d3e39e0..a63cdc77 100644 --- a/contract/src/operation.rs +++ b/contract/src/operation.rs @@ -3,11 +3,12 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Response, Storage, Uint128}; use crate::{ - contract::{build_xrpl_token_key, convert_amount_decimals, XRPL_TOKENS_DECIMALS}, + contract::{convert_amount_decimals, XRPL_TOKENS_DECIMALS}, error::ContractError, evidence::TransactionResult, signatures::Signature, state::{TokenState, COREUM_TOKENS, PENDING_OPERATIONS, XRPL_TOKENS}, + token::build_xrpl_token_key, }; #[cw_serde] diff --git a/contract/src/state.rs b/contract/src/state.rs index 36aeee34..ee0c110f 100644 --- a/contract/src/state.rs +++ b/contract/src/state.rs @@ -152,6 +152,8 @@ pub enum ContractActions { SaveSignature, SendToXRPL, ClaimFees, + UpdateXRPLToken, + UpdateCoreumToken, } impl ContractActions { @@ -167,6 +169,8 @@ impl ContractActions { ContractActions::SaveSignature => "save_signature", ContractActions::SendToXRPL => "send_to_xrpl", ContractActions::ClaimFees => "claim_fees", + ContractActions::UpdateXRPLToken => "update_xrpl_token", + ContractActions::UpdateCoreumToken => "update_coreum_token", } } } diff --git a/contract/src/tests.rs b/contract/src/tests.rs index 2352d5ce..0fa08242 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -12,6 +12,7 @@ mod tests { use sha2::{Digest, Sha256}; use crate::{ + contract::{XRP_CURRENCY, XRP_ISSUER}, error::ContractError, evidence::{Evidence, OperationResult, TransactionResult}, msg::{ @@ -28,8 +29,6 @@ mod tests { const XRP_SYMBOL: &str = "XRP"; const XRP_SUBUNIT: &str = "drop"; const XRPL_DENOM_PREFIX: &str = "xrpl"; - const XRP_CURRENCY: &str = "XRP"; - const XRP_ISSUER: &str = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; const TRUST_SET_LIMIT_AMOUNT: u128 = 1000000000000000000; // 1e18 const XRP_DECIMALS: u32 = 6; const XRP_DEFAULT_SENDING_PRECISION: i32 = 6; @@ -6411,6 +6410,398 @@ mod tests { ); } + #[test] + fn token_update() { + let app = CoreumTestApp::new(); + let accounts_number = 3; + let accounts = app + .init_accounts(&coins(100_000_000_000, FEE_DENOM), accounts_number) + .unwrap(); + + let signer = accounts.get((accounts_number - 1) as usize).unwrap(); + let xrpl_addresses: Vec = (0..2).map(|_| generate_xrpl_address()).collect(); + let xrpl_pub_keys: Vec = (0..2).map(|_| generate_xrpl_pub_key()).collect(); + + let mut relayer_accounts = vec![]; + let mut relayers = vec![]; + + for i in 0..accounts_number - 1 { + relayer_accounts.push(accounts.get(i as usize).unwrap()); + relayers.push(Relayer { + coreum_address: Addr::unchecked(accounts.get(i as usize).unwrap().address()), + xrpl_address: xrpl_addresses[i as usize].to_string(), + xrpl_pub_key: xrpl_pub_keys[i as usize].to_string(), + }); + } + + let wasm = Wasm::new(&app); + let asset_ft = AssetFT::new(&app); + + let contract_addr = store_and_instantiate( + &wasm, + &signer, + Addr::unchecked(signer.address()), + vec![relayers[0].clone(), relayers[1].clone()], + 2, + 4, + Uint128::new(TRUST_SET_LIMIT_AMOUNT), + query_issue_fee(&asset_ft), + generate_xrpl_address(), + ); + + // Recover enough tickets for testing + wasm.execute::( + &contract_addr, + &ExecuteMsg::RecoverTickets { + account_sequence: 1, + number_of_tickets: Some(5), + }, + &vec![], + &signer, + ) + .unwrap(); + + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(tx_hash.to_owned()), + account_sequence: Some(1), + ticket_sequence: None, + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::TicketsAllocation { + tickets: Some((1..6).collect()), + }, + }, + }, + &vec![], + relayer, + ) + .unwrap(); + } + + // Register one XRPL token and one Coreum token + let xrpl_token = XRPLToken { + issuer: generate_xrpl_address(), + currency: "USD".to_string(), + sending_precision: 15, + max_holding_amount: Uint128::new(1000000000), + bridging_fee: Uint128::zero(), + transfer_rate: None, + }; + + let subunit = "utest".to_string(); + asset_ft + .issue( + MsgIssue { + issuer: signer.address(), + symbol: "TEST".to_string(), + subunit: subunit.to_owned(), + precision: 6, + initial_amount: "100000000".to_string(), + description: "description".to_string(), + features: vec![MINTING as i32], + burn_rate: "0".to_string(), + send_commission_rate: "0".to_string(), + uri: "uri".to_string(), + uri_hash: "uri_hash".to_string(), + }, + &signer, + ) + .unwrap(); + + let coreum_token_denom = format!("{}-{}", subunit, signer.address()).to_lowercase(); + + let coreum_token = CoreumToken { + denom: coreum_token_denom.to_owned(), + decimals: 6, + sending_precision: 6, + max_holding_amount: Uint128::new(1000000000), + bridging_fee: Uint128::zero(), + }; + + wasm.execute::( + &contract_addr, + &ExecuteMsg::RegisterXRPLToken { + issuer: xrpl_token.issuer.clone(), + currency: xrpl_token.currency.clone(), + sending_precision: xrpl_token.sending_precision, + max_holding_amount: xrpl_token.max_holding_amount, + bridging_fee: xrpl_token.bridging_fee, + transfer_rate: xrpl_token.transfer_rate, + }, + &query_issue_fee(&asset_ft), + &signer, + ) + .unwrap(); + + let query_xrpl_tokens = wasm + .query::( + &contract_addr, + &QueryMsg::XRPLTokens { + offset: None, + limit: None, + }, + ) + .unwrap(); + + let xrpl_token_denom = query_xrpl_tokens + .tokens + .iter() + .find(|t| t.issuer == xrpl_token.issuer && t.currency == xrpl_token.currency) + .unwrap() + .coreum_denom + .clone(); + + // If we try to update the status of a token that is in processing state, it should fail + let update_status_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::UpdateXRPLToken { + issuer: xrpl_token.issuer.to_owned(), + currency: xrpl_token.currency.to_owned(), + state: Some(TokenState::Disabled), + }, + &vec![], + &signer, + ) + .unwrap_err(); + + assert!(update_status_error.to_string().contains( + ContractError::TokenStateIsImmutable {} + .to_string() + .as_str() + )); + + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(tx_hash.to_owned()), + account_sequence: None, + ticket_sequence: Some(1), + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::TrustSet { + issuer: xrpl_token.issuer.to_owned(), + currency: xrpl_token.currency.to_owned(), + }, + }, + }, + &vec![], + relayer, + ) + .unwrap(); + } + + // We will try to send one evidence with the token enabled and the other one with the token disabled, which should fail. + let tx_hash = generate_hash(); + // First evidence should succeed + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLToCoreumTransfer { + tx_hash: tx_hash.to_owned(), + issuer: xrpl_token.issuer.clone(), + currency: xrpl_token.currency.clone(), + amount: Uint128::one(), + recipient: Addr::unchecked(signer.address()), + }, + }, + &[], + relayer_accounts[0], + ) + .unwrap(); + + // Disable the token + wasm.execute::( + &contract_addr, + &ExecuteMsg::UpdateXRPLToken { + issuer: xrpl_token.issuer.to_owned(), + currency: xrpl_token.currency.to_owned(), + state: Some(TokenState::Disabled), + }, + &vec![], + &signer, + ) + .unwrap(); + + // If we send second evidence it should fail because token is disabled + let disabled_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLToCoreumTransfer { + tx_hash: tx_hash.to_owned(), + issuer: xrpl_token.issuer.clone(), + currency: xrpl_token.currency.clone(), + amount: Uint128::one(), + recipient: Addr::unchecked(signer.address()), + }, + }, + &[], + relayer_accounts[1], + ) + .unwrap_err(); + + assert!(disabled_error + .to_string() + .contains(ContractError::XRPLTokenNotEnabled {}.to_string().as_str())); + + // If we try to change the status to something that is not disabled or enabled it should fail + let update_status_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::UpdateXRPLToken { + issuer: xrpl_token.issuer.to_owned(), + currency: xrpl_token.currency.to_owned(), + state: Some(TokenState::Inactive), + }, + &vec![], + &signer, + ) + .unwrap_err(); + + assert!(update_status_error.to_string().contains( + ContractError::InvalidTargetTokenState {} + .to_string() + .as_str() + )); + + // If we try to change the status back to enabled and send the evidence, the balance should be sent to the receiver. + wasm.execute::( + &contract_addr, + &ExecuteMsg::UpdateXRPLToken { + issuer: xrpl_token.issuer.to_owned(), + currency: xrpl_token.currency.to_owned(), + state: Some(TokenState::Enabled), + }, + &vec![], + &signer, + ) + .unwrap(); + + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLToCoreumTransfer { + tx_hash: tx_hash.to_owned(), + issuer: xrpl_token.issuer.clone(), + currency: xrpl_token.currency.clone(), + amount: Uint128::one(), + recipient: Addr::unchecked(signer.address()), + }, + }, + &[], + relayer_accounts[1], + ) + .unwrap(); + + let request_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: signer.address(), + denom: xrpl_token_denom.clone(), + }) + .unwrap(); + + assert_eq!(request_balance.balance, "1".to_string()); + + // If we disable again and we try to send the token back it will fail + wasm.execute::( + &contract_addr, + &ExecuteMsg::UpdateXRPLToken { + issuer: xrpl_token.issuer.to_owned(), + currency: xrpl_token.currency.to_owned(), + state: Some(TokenState::Disabled), + }, + &vec![], + &signer, + ) + .unwrap(); + + let send_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::SendToXRPL { + recipient: generate_xrpl_address(), + }, + &coins(1, xrpl_token_denom.to_owned()), + &signer, + ) + .unwrap_err(); + + assert!(send_error + .to_string() + .contains(ContractError::XRPLTokenNotEnabled {}.to_string().as_str())); + + // Register the Coreum Token + wasm.execute::( + &contract_addr, + &ExecuteMsg::RegisterCoreumToken { + denom: coreum_token_denom.to_owned(), + decimals: coreum_token.decimals, + sending_precision: coreum_token.sending_precision, + max_holding_amount: coreum_token.max_holding_amount, + bridging_fee: coreum_token.bridging_fee, + }, + &query_issue_fee(&asset_ft), + &signer, + ) + .unwrap(); + + // If we try to change the status to something that is not disabled or enabled it should fail + let update_status_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::UpdateCoreumToken { + denom: coreum_token_denom.to_owned(), + state: Some(TokenState::Processing), + }, + &vec![], + &signer, + ) + .unwrap_err(); + + assert!(update_status_error.to_string().contains( + ContractError::InvalidTargetTokenState {} + .to_string() + .as_str() + )); + + // Disable the Coreum Token + wasm.execute::( + &contract_addr, + &&ExecuteMsg::UpdateCoreumToken { + denom: coreum_token_denom.to_owned(), + state: Some(TokenState::Disabled), + }, + &vec![], + &signer, + ) + .unwrap(); + + // If we try to send now it will fail because the token is disabled + let send_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::SendToXRPL { + recipient: generate_xrpl_address(), + }, + &coins(1, coreum_token_denom.to_owned()), + &signer, + ) + .unwrap_err(); + + assert!(send_error.to_string().contains( + ContractError::CoreumOriginatedTokenDisabled {} + .to_string() + .as_str() + )); + } + #[test] fn invalid_transaction_evidences() { let app = CoreumTestApp::new(); @@ -6440,7 +6831,7 @@ mod tests { let tx_hash = generate_hash(); let account_sequence = 1; - let tickets = vec![1, 2, 3, 4, 5]; + let tickets: Vec = (1..6).collect(); let invalid_evidences_input = vec![ Evidence::XRPLTransactionResult { diff --git a/contract/src/token.rs b/contract/src/token.rs new file mode 100644 index 00000000..7bd04c51 --- /dev/null +++ b/contract/src/token.rs @@ -0,0 +1,37 @@ +use crate::{ + contract::{XRP_CURRENCY, XRP_ISSUER}, + error::ContractError, + state::TokenState, +}; + +// Build the key to access the Tokens saved in state +pub fn build_xrpl_token_key(issuer: String, currency: String) -> String { + // Issuer+currency is the key we use to find an XRPL + let mut key = issuer; + key.push_str(currency.as_str()); + key +} + +// Helper to distinguish between the XRP token and other XRPL originated tokens +pub fn is_token_xrp(issuer: String, currency: String) -> bool { + issuer == XRP_ISSUER && currency == XRP_CURRENCY +} + +// Helper function to update the status of a token +pub fn set_token_state( + state: &mut TokenState, + target_state: Option, +) -> Result<(), ContractError> { + if let Some(target_state) = target_state { + if (*state).eq(&TokenState::Inactive) || (*state).eq(&TokenState::Processing) { + return Err(ContractError::TokenStateIsImmutable {}); + } + if target_state.eq(&TokenState::Inactive) || target_state.eq(&TokenState::Processing) { + return Err(ContractError::InvalidTargetTokenState {}); + } + + *state = target_state; + } + + Ok(()) +} diff --git a/go.work.sum b/go.work.sum index 3b170ceb..e85421a6 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,9 +1,19 @@ -github.com/CoreumFoundation/coreum-tools v0.4.1-0.20231212055417-5e835e510490 h1:TulOLS6eFjUrKi0LBs7kdPuaRMLjcO8CLWlMjA/oHHU= -github.com/CoreumFoundation/coreum-tools v0.4.1-0.20231212055417-5e835e510490/go.mod h1:VD93vCHkxYaT/RhOesXTFgd/GQDW54tr0BqGi5JU1c0= +cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= +cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=