diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 6615b1f9..cddd6723 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -7,7 +7,6 @@ use stellar_axelar_gateway::executable::AxelarExecutableInterface; use stellar_axelar_gateway::AxelarGatewayMessagingClient; use stellar_axelar_std::address::AddressExt; use stellar_axelar_std::events::Event; -use stellar_axelar_std::token::validate_token_metadata; use stellar_axelar_std::ttl::{extend_instance_ttl, extend_persistent_ttl}; use stellar_axelar_std::types::Token; use stellar_axelar_std::{ensure, interfaces, Operatable, Ownable, Upgradable}; @@ -23,6 +22,7 @@ use crate::executable::InterchainTokenExecutableClient; use crate::flow_limit::FlowDirection; use crate::interface::InterchainTokenServiceInterface; use crate::storage_types::{DataKey, TokenIdConfigValue}; +use crate::token_metadata::TokenMetadataExt; use crate::types::{ DeployInterchainToken, HubMessage, InterchainTransfer, Message, TokenManagerType, }; @@ -240,6 +240,8 @@ impl InterchainTokenServiceInterface for InterchainTokenService { let deploy_salt = Self::interchain_token_deploy_salt(env, caller.clone(), salt); let token_id = Self::interchain_token_id(env, Address::zero(env), deploy_salt); + token_metadata.validate()?; + let deployed_address = Self::deploy_interchain_token_contract( env, initial_minter, @@ -474,100 +476,11 @@ impl InterchainTokenService { Self::get_execute_params(env, source_chain, source_address, payload)?; match message { - Message::InterchainTransfer(InterchainTransfer { - token_id, - source_address, - destination_address, - amount, - data, - }) => { - let destination_address = Address::from_xdr(env, &destination_address) - .map_err(|_| ContractError::InvalidDestinationAddress)?; - - let token_config_value = - Self::token_id_config_with_extended_ttl(env, token_id.clone())?; - - FlowDirection::In.add_flow(env, token_id.clone(), amount)?; - - token_handler::give_token( - env, - &destination_address, - token_config_value.clone(), - amount, - )?; - - InterchainTransferReceivedEvent { - source_chain: source_chain.clone(), - token_id: token_id.clone(), - source_address: source_address.clone(), - destination_address: destination_address.clone(), - amount, - data: data.clone(), - } - .emit(env); - - let token_address = token_config_value.token_address; - - if let Some(payload) = data { - let executable = - InterchainTokenExecutableClient::new(env, &destination_address); - executable.execute_with_interchain_token( - &source_chain, - &message_id, - &source_address, - &payload, - &token_id, - &token_address, - &amount, - ); - } - } - Message::DeployInterchainToken(DeployInterchainToken { - token_id, - name, - symbol, - decimals, - minter, - }) => { - ensure!( - Self::token_id_config(env, token_id.clone()).is_err(), - ContractError::TokenAlreadyDeployed - ); - - let token_metadata = TokenMetadata { - name, - symbol, - decimal: decimals as u32, - }; - - ensure!( - validate_token_metadata(&token_metadata).is_ok(), - ContractError::InvalidTokenMetaData - ); - - // Note: attempt to convert a byte string which doesn't represent a valid Soroban address fails at the Host level - let minter = minter - .map(|m| Address::from_xdr(env, &m)) - .transpose() - .map_err(|_| ContractError::InvalidMinter)?; - - let deployed_address = Self::deploy_interchain_token_contract( - env, - minter, - token_id.clone(), - token_metadata, - ); - - Self::set_token_id_config( - env, - token_id, - TokenIdConfigValue { - token_address: deployed_address, - token_manager_type: TokenManagerType::NativeInterchainToken, - }, - ); + Message::InterchainTransfer(message) => { + Self::execute_transfer_message(env, &source_chain, message_id, message) } - }; + Message::DeployInterchainToken(message) => Self::execute_deploy_message(env, message), + }?; extend_persistent_ttl(env, &DataKey::TrustedChain(source_chain)); extend_instance_ttl(env); @@ -748,4 +661,96 @@ impl InterchainTokenService { deployed_address } + + fn execute_transfer_message( + env: &Env, + source_chain: &String, + message_id: String, + InterchainTransfer { + token_id, + source_address, + destination_address, + amount, + data, + }: InterchainTransfer, + ) -> Result<(), ContractError> { + let destination_address = Address::from_xdr(env, &destination_address) + .map_err(|_| ContractError::InvalidDestinationAddress)?; + + let token_config_value = Self::token_id_config_with_extended_ttl(env, token_id.clone())?; + + FlowDirection::In.add_flow(env, token_id.clone(), amount)?; + + token_handler::give_token( + env, + &destination_address, + token_config_value.clone(), + amount, + )?; + + InterchainTransferReceivedEvent { + source_chain: source_chain.clone(), + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: destination_address.clone(), + amount, + data: data.clone(), + } + .emit(env); + + let token_address = token_config_value.token_address; + + if let Some(payload) = data { + let executable = InterchainTokenExecutableClient::new(env, &destination_address); + executable.execute_with_interchain_token( + source_chain, + &message_id, + &source_address, + &payload, + &token_id, + &token_address, + &amount, + ); + } + + Ok(()) + } + + fn execute_deploy_message( + env: &Env, + DeployInterchainToken { + token_id, + name, + symbol, + decimals, + minter, + }: DeployInterchainToken, + ) -> Result<(), ContractError> { + ensure!( + Self::token_id_config(env, token_id.clone()).is_err(), + ContractError::TokenAlreadyDeployed + ); + + let token_metadata = TokenMetadata::new(name, symbol, decimals as u32)?; + + // Note: attempt to convert a byte string which doesn't represent a valid Soroban address fails at the Host level + let minter = minter + .map(|m| Address::from_xdr(env, &m)) + .transpose() + .map_err(|_| ContractError::InvalidMinter)?; + + let deployed_address = + Self::deploy_interchain_token_contract(env, minter, token_id.clone(), token_metadata); + + Self::set_token_id_config( + env, + token_id, + TokenIdConfigValue { + token_address: deployed_address, + token_manager_type: TokenManagerType::NativeInterchainToken, + }, + ); + + Ok(()) + } } diff --git a/contracts/interchain-token-service/src/error.rs b/contracts/interchain-token-service/src/error.rs index f4d91d8b..ed368d98 100644 --- a/contracts/interchain-token-service/src/error.rs +++ b/contracts/interchain-token-service/src/error.rs @@ -30,7 +30,10 @@ pub enum ContractError { NotApproved = 22, InvalidDestinationChain = 23, InvalidData = 24, - TokenAlreadyRegistered = 25, + InvalidTokenName = 25, + InvalidTokenSymbol = 26, + InvalidTokenDecimals = 27, + TokenAlreadyRegistered = 28, } impl_not_approved_error!(ContractError); diff --git a/contracts/interchain-token-service/src/token_metadata.rs b/contracts/interchain-token-service/src/token_metadata.rs index 4d516d5a..7fe2527a 100644 --- a/contracts/interchain-token-service/src/token_metadata.rs +++ b/contracts/interchain-token-service/src/token_metadata.rs @@ -1,13 +1,51 @@ use soroban_sdk::{token, Address, Env, String}; use soroban_token_sdk::metadata::TokenMetadata; use stellar_axelar_std::ensure; -use stellar_axelar_std::token::validate_token_metadata; use crate::error::ContractError; const NATIVE_TOKEN_NAME: &str = "Stellar"; const NATIVE_TOKEN_SYMBOL: &str = "XLM"; +const MAX_DECIMALS: u32 = u8::MAX as u32; const MAX_NAME_LENGTH: u32 = 32; +const MAX_SYMBOL_LENGTH: u32 = 32; + +pub trait TokenMetadataExt: Sized { + fn new(name: String, symbol: String, decimals: u32) -> Result; + + fn validate(&self) -> Result<(), ContractError>; +} + +impl TokenMetadataExt for TokenMetadata { + fn new(name: String, symbol: String, decimals: u32) -> Result { + let token_metadata = Self { + name, + symbol, + decimal: decimals, + }; + + token_metadata.validate()?; + + Ok(token_metadata) + } + + fn validate(&self) -> Result<(), ContractError> { + ensure!( + self.decimal <= MAX_DECIMALS, + ContractError::InvalidTokenDecimals + ); + ensure!( + !self.name.is_empty() && self.name.len() <= MAX_NAME_LENGTH, + ContractError::InvalidTokenName + ); + ensure!( + !self.symbol.is_empty() && self.symbol.len() <= MAX_SYMBOL_LENGTH, + ContractError::InvalidTokenSymbol + ); + + Ok(()) + } +} pub fn token_metadata( env: &Env, @@ -32,16 +70,5 @@ pub fn token_metadata( (name, symbol) }; - let token_metadata = TokenMetadata { - name, - symbol, - decimal: decimals, - }; - - ensure!( - validate_token_metadata(&token_metadata).is_ok(), - ContractError::InvalidTokenMetaData - ); - - Ok(token_metadata) + TokenMetadata::new(name, symbol, decimals) } diff --git a/contracts/interchain-token-service/tests/deploy_interchain_token.rs b/contracts/interchain-token-service/tests/deploy_interchain_token.rs index d0daa26b..ada93b36 100644 --- a/contracts/interchain-token-service/tests/deploy_interchain_token.rs +++ b/contracts/interchain-token-service/tests/deploy_interchain_token.rs @@ -193,25 +193,58 @@ fn deploy_interchain_token_zero_initial_supply_no_minter() { } #[test] -#[should_panic(expected = "HostError: Error(Context, InvalidAction)")] -fn deploy_interchain_token_fails_with_invalid_decimals() { +fn deploy_interchain_token_fails_with_invalid_token_metadata() { let (env, client, _, _, _) = setup_env(); - env.mock_all_auths(); let sender = Address::generate(&env); let minter: Option
= None; let salt = BytesN::<32>::from_array(&env, &[1; 32]); - let invalid_decimals = (u8::MAX) as u32 + 1; - let token_metadata = TokenMetadata::new(&env, "Test", "TEST", invalid_decimals); let initial_supply = 0; - client.deploy_interchain_token(&sender, &salt, &token_metadata, &initial_supply, &minter); + let cases = [ + ( + TokenMetadata::new(&env, "", "symbol", 6), + ContractError::InvalidTokenName, + ), + ( + TokenMetadata::new(&env, "A".repeat(33).as_str(), "symbol", 6), + ContractError::InvalidTokenName, + ), + ( + TokenMetadata::new(&env, "name", "", 6), + ContractError::InvalidTokenSymbol, + ), + ( + TokenMetadata::new(&env, "name", "A".repeat(33).as_str(), 6), + ContractError::InvalidTokenSymbol, + ), + ( + TokenMetadata::new(&env, "name", "symbol", (u8::MAX) as u32 + 1), + ContractError::InvalidTokenDecimals, + ), + ( + TokenMetadata::new(&env, "name", "symbol", u32::MAX), + ContractError::InvalidTokenDecimals, + ), + ]; + + for (token_metadata, expected_error) in cases { + assert_contract_err!( + client.mock_all_auths().try_deploy_interchain_token( + &sender, + &salt, + &token_metadata, + &initial_supply, + &minter + ), + expected_error + ); + } } #[test] fn deploy_interchain_token_fails_with_invalid_auth() { let (env, client, _, _, _) = setup_env(); - env.mock_all_auths(); let sender = Address::generate(&env); let user = Address::generate(&env); @@ -223,6 +256,6 @@ fn deploy_interchain_token_fails_with_invalid_auth() { assert_auth_err!( user, - client.deploy_interchain_token(&sender, &salt, &token_metadata, &initial_supply, &minter,) + client.deploy_interchain_token(&sender, &salt, &token_metadata, &initial_supply, &minter) ); } diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index 97efb465..e408c687 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -2,7 +2,7 @@ mod utils; use soroban_sdk::testutils::Address as _; use soroban_sdk::xdr::ToXdr; -use soroban_sdk::{vec, Address, Bytes, BytesN, String}; +use soroban_sdk::{vec, Address, Bytes, BytesN, Env, String}; use soroban_token_sdk::metadata::TokenMetadata; use stellar_axelar_gateway::types::Message as GatewayMessage; use stellar_axelar_std::{assert_contract_err, events}; @@ -14,7 +14,9 @@ use stellar_interchain_token_service::event::{ use stellar_interchain_token_service::types::{ DeployInterchainToken, HubMessage, InterchainTransfer, Message, TokenManagerType, }; -use utils::{approve_gateway_messages, register_chains, setup_env, setup_its_token}; +use utils::{ + approve_gateway_messages, register_chains, setup_env, setup_its_token, TokenMetadataExt, +}; #[test] #[should_panic(expected = "Error(Contract, #22)")] // ContractError::NotApproved @@ -235,104 +237,83 @@ fn deploy_interchain_token_message_execute_succeeds() { } #[test] -#[should_panic(expected = "Error(Contract, #16)")] // ContractError::InvalidTokenMetadata -fn deploy_interchain_token_message_execute_fails_empty_token_name() { - let (env, client, gateway_client, _, signers) = setup_env(); - register_chains(&env, &client); - - let source_chain = client.its_hub_chain_name(); - let source_address = client.its_hub_address(); - let token_id = BytesN::from_array(&env, &[1u8; 32]); - let original_source_chain = String::from_str(&env, "ethereum"); - client - .mock_all_auths() - .set_trusted_chain(&original_source_chain); - - let msg_empty_name = HubMessage::ReceiveFromHub { - source_chain: original_source_chain, - message: Message::DeployInterchainToken(DeployInterchainToken { - token_id, - name: String::from_str(&env, ""), - symbol: String::from_str(&env, "TEST"), - decimals: 18, - minter: None, - }), - }; - let payload_empty_name = msg_empty_name.abi_encode(&env).unwrap(); - let payload_hash_empty_name: BytesN<32> = env.crypto().keccak256(&payload_empty_name).into(); - - let message_id_empty_name = String::from_str(&env, "no_name"); - - let messages = vec![ - &env, - GatewayMessage { - source_chain: source_chain.clone(), - message_id: message_id_empty_name.clone(), - source_address: source_address.clone(), - contract_address: client.address.clone(), - payload_hash: payload_hash_empty_name, - }, - ]; - - approve_gateway_messages(&env, &gateway_client, signers, messages); - - client.execute( - &source_chain, - &message_id_empty_name, - &source_address, - &payload_empty_name, - ); -} - -#[test] -#[should_panic(expected = "Error(Contract, #16)")] // ContractError::InvalidTokenMetadata -fn deploy_interchain_token_message_execute_fails_empty_token_symbol() { - let (env, client, gateway_client, _, signers) = setup_env(); - register_chains(&env, &client); - - let source_chain = client.its_hub_chain_name(); - let source_address = client.its_hub_address(); - let token_id = BytesN::from_array(&env, &[1u8; 32]); - let original_source_chain = String::from_str(&env, "ethereum"); - client - .mock_all_auths() - .set_trusted_chain(&original_source_chain); - - let msg_empty_symbol = HubMessage::ReceiveFromHub { - source_chain: original_source_chain, - message: Message::DeployInterchainToken(DeployInterchainToken { - token_id, - name: String::from_str(&env, "test"), - symbol: String::from_str(&env, ""), - decimals: 18, - minter: None, - }), - }; - let payload_empty_symbol = msg_empty_symbol.abi_encode(&env).unwrap(); - let payload_hash_empty_symbol: BytesN<32> = - env.crypto().keccak256(&payload_empty_symbol).into(); - - let message_id_empty_symbol = String::from_str(&env, "no_symbol"); - - let messages = vec![ - &env, - GatewayMessage { - source_chain: source_chain.clone(), - message_id: message_id_empty_symbol.clone(), - source_address: source_address.clone(), - contract_address: client.address.clone(), - payload_hash: payload_hash_empty_symbol, - }, +fn deploy_interchain_token_message_execute_fails_invalid_token_metadata() { + let env = Env::default(); + + let cases = [ + ( + TokenMetadata::new(&env, "", "symbol", 6), + ContractError::InvalidTokenName, + ), + ( + TokenMetadata::new(&env, "A".repeat(33).as_str(), "symbol", 6), + ContractError::InvalidTokenName, + ), + ( + TokenMetadata::new(&env, "name", "", 6), + ContractError::InvalidTokenSymbol, + ), + ( + TokenMetadata::new(&env, "name", "A".repeat(33).as_str(), 6), + ContractError::InvalidTokenSymbol, + ), ]; - approve_gateway_messages(&env, &gateway_client, signers, messages); - - client.execute( - &source_chain, - &message_id_empty_symbol, - &source_address, - &payload_empty_symbol, - ); + for ( + i, + ( + TokenMetadata { + name, + symbol, + decimal, + }, + expected_error, + ), + ) in cases.into_iter().enumerate() + { + let (env, client, gateway_client, _, signers) = setup_env(); + + let source_chain = client.its_hub_chain_name(); + let source_address = client.its_hub_address(); + let original_source_chain = String::from_str(&env, "ethereum"); + let message_id = String::from_str(&env, "message_id"); + + client + .mock_all_auths() + .set_trusted_chain(&original_source_chain); + + let token_id = BytesN::from_array(&env, &[i as u8; 32]); + let msg = HubMessage::ReceiveFromHub { + source_chain: original_source_chain.clone(), + message: Message::DeployInterchainToken(DeployInterchainToken { + token_id, + name, + symbol, + decimals: decimal as u8, + minter: None, + }), + }; + let payload = msg.abi_encode(&env).unwrap(); + let payload_hash: BytesN<32> = env.crypto().keccak256(&payload).into(); + + let messages = vec![ + &env, + GatewayMessage { + source_chain: source_chain.clone(), + message_id: message_id.clone(), + source_address: source_address.clone(), + contract_address: client.address.clone(), + payload_hash: payload_hash.clone(), + }, + ]; + + approve_gateway_messages(&env, &gateway_client, signers, messages); + + assert_contract_err!( + client.try_execute(&source_chain, &message_id, &source_address, &payload), + expected_error + ); + } } #[test] diff --git a/contracts/interchain-token/src/contract.rs b/contracts/interchain-token/src/contract.rs index ad847e1a..54167266 100644 --- a/contracts/interchain-token/src/contract.rs +++ b/contracts/interchain-token/src/contract.rs @@ -7,7 +7,6 @@ use soroban_token_sdk::event::Events as TokenEvents; use soroban_token_sdk::metadata::TokenMetadata; use soroban_token_sdk::TokenUtils; use stellar_axelar_std::interfaces::OwnableInterface; -use stellar_axelar_std::token::validate_token_metadata; use stellar_axelar_std::ttl::{extend_instance_ttl, extend_persistent_ttl}; use stellar_axelar_std::{ensure, interfaces, Upgradable}; @@ -31,10 +30,6 @@ impl InterchainToken { ) { interfaces::set_owner(&env, &owner); - if let Err(err) = validate_token_metadata(&token_metadata) { - panic_with_error!(env, err); - } - Self::write_metadata(&env, token_metadata); env.storage().instance().set(&DataKey::TokenId, &token_id); diff --git a/contracts/interchain-token/tests/test.rs b/contracts/interchain-token/tests/test.rs index 4c429e5c..b59646d0 100644 --- a/contracts/interchain-token/tests/test.rs +++ b/contracts/interchain-token/tests/test.rs @@ -30,42 +30,6 @@ fn setup_token<'a>(env: &Env) -> (InterchainTokenClient<'a>, Address, Address) { (token, owner, minter) } -#[test] -#[should_panic(expected = "HostError: Error(Context, InvalidAction)")] -fn register_token_with_invalid_decimals_fails() { - let env = Env::default(); - let owner = Address::generate(&env); - let minter = Address::generate(&env); - let token_id: BytesN<32> = BytesN::<32>::random(&env); - let token_metadata = setup_token_metadata(&env, "name", "symbol", u32::from(u8::MAX) + 1); - - env.register(InterchainToken, (owner, minter, &token_id, token_metadata)); -} - -#[test] -#[should_panic(expected = "HostError: Error(Context, InvalidAction)")] -fn register_token_with_invalid_name_fails() { - let env = Env::default(); - let owner = Address::generate(&env); - let minter = Address::generate(&env); - let token_id: BytesN<32> = BytesN::<32>::random(&env); - let token_metadata = setup_token_metadata(&env, "", "symbol", 1); - - env.register(InterchainToken, (owner, minter, &token_id, token_metadata)); -} - -#[test] -#[should_panic(expected = "HostError: Error(Context, InvalidAction)")] -fn register_token_with_invalid_symbol_fails() { - let env = Env::default(); - let owner = Address::generate(&env); - let minter = Address::generate(&env); - let token_id: BytesN<32> = BytesN::<32>::random(&env); - let token_metadata = setup_token_metadata(&env, "name", "", 1); - - env.register(InterchainToken, (owner, minter, &token_id, token_metadata)); -} - #[test] fn register_interchain_token() { let env = Env::default(); diff --git a/packages/axelar-std/src/error.rs b/packages/axelar-std/src/error.rs index dc490b64..de6a4b97 100644 --- a/packages/axelar-std/src/error.rs +++ b/packages/axelar-std/src/error.rs @@ -64,7 +64,7 @@ macro_rules! assert_err { /// `assert_contract_err(client.try_fun(...), ContractError);` #[macro_export] macro_rules! assert_contract_err { - ($given:expr, $expected:pat) => { + ($given:expr, $expected:expr) => { match $given { Ok(v) => panic!( "Expected error {:?}, got {:?} instead", @@ -72,16 +72,11 @@ macro_rules! assert_contract_err { v ), Err(e) => match e { - Ok(v) => { - if !matches!(v, $expected) { - panic!( - "Expected error {}, got {:?} instead", - stringify!($expected), - v - ) - } - } Err(e) => panic!("Unexpected error {e:?}"), + Ok(v) if v != $expected => { + panic!("Expected error {:?}, got {:?} instead", $expected, v) + } + _ => (), }, } }; diff --git a/packages/axelar-std/src/lib.rs b/packages/axelar-std/src/lib.rs index c7a9af36..e07bc637 100644 --- a/packages/axelar-std/src/lib.rs +++ b/packages/axelar-std/src/lib.rs @@ -24,7 +24,5 @@ pub mod interfaces; pub mod address; -pub mod token; - #[cfg(feature = "derive")] pub use stellar_axelar_std_derive::*; diff --git a/packages/axelar-std/src/token.rs b/packages/axelar-std/src/token.rs deleted file mode 100644 index 48d4625b..00000000 --- a/packages/axelar-std/src/token.rs +++ /dev/null @@ -1,29 +0,0 @@ -use soroban_sdk::contracterror; -use soroban_token_sdk::metadata::TokenMetadata; - -use crate::ensure; - -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[repr(u32)] -pub enum TokenError { - InvalidDecimal = 0, - InvalidTokenName = 1, - InvalidTokenSymbol = 2, -} - -pub fn validate_token_metadata(token_metadata: &TokenMetadata) -> Result<(), TokenError> { - ensure!( - token_metadata.decimal <= u8::MAX.into(), - TokenError::InvalidDecimal - ); - ensure!( - !token_metadata.name.is_empty(), - TokenError::InvalidTokenName - ); - ensure!( - !token_metadata.symbol.is_empty(), - TokenError::InvalidTokenSymbol - ); - Ok(()) -}