diff --git a/Cargo.lock b/Cargo.lock index b097574e..e6ab265c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,7 @@ dependencies = [ "goldie", "hex", "soroban-sdk", + "soroban-token-sdk", ] [[package]] diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 9a6cf4fd..952e87db 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -1,11 +1,13 @@ use axelar_gas_service::AxelarGasServiceClient; use axelar_gateway::{executable::AxelarExecutableInterface, AxelarGatewayMessagingClient}; +use axelar_soroban_std::assert_ok; use axelar_soroban_std::events::Event; +use axelar_soroban_std::token::validate_token_metadata; use axelar_soroban_std::{ address::AddressExt, ensure, interfaces, types::Token, Ownable, Upgradable, }; use interchain_token::InterchainTokenClient; -use soroban_sdk::token::StellarAssetClient; +use soroban_sdk::token::{self, StellarAssetClient}; use soroban_sdk::xdr::{FromXdr, ToXdr}; use soroban_sdk::{contract, contractimpl, panic_with_error, Address, Bytes, BytesN, Env, String}; use soroban_token_sdk::metadata::TokenMetadata; @@ -13,6 +15,7 @@ use soroban_token_sdk::metadata::TokenMetadata; use crate::abi::{get_message_type, MessageType as EncodedMessageType}; use crate::error::ContractError; use crate::event::{ + InterchainTokenDeployedEvent, InterchainTokenDeploymentStartedEvent, InterchainTokenIdClaimedEvent, InterchainTransferReceivedEvent, InterchainTransferSentEvent, TrustedChainRemovedEvent, TrustedChainSetEvent, }; @@ -20,7 +23,9 @@ use crate::executable::InterchainTokenExecutableClient; use crate::interface::InterchainTokenServiceInterface; use crate::storage_types::{DataKey, TokenIdConfigValue}; use crate::token_handler; -use crate::types::{HubMessage, InterchainTransfer, Message, TokenManagerType}; +use crate::types::{ + DeployInterchainToken, HubMessage, InterchainTransfer, Message, TokenManagerType, +}; const ITS_HUB_CHAIN_NAME: &str = "axelar"; const PREFIX_INTERCHAIN_TOKEN_ID: &str = "its-interchain-token-id"; @@ -183,7 +188,7 @@ impl InterchainTokenServiceInterface for InterchainTokenService { env: &Env, caller: Address, salt: BytesN<32>, - token_meta_data: TokenMetadata, + token_metadata: TokenMetadata, initial_supply: i128, minter: Option
, ) -> Result, ContractError> { @@ -211,12 +216,22 @@ impl InterchainTokenServiceInterface for InterchainTokenService { Self::interchain_token_wasm_hash(env), ( env.current_contract_address(), - initial_minter, + initial_minter.clone(), token_id.clone(), - token_meta_data, + token_metadata.clone(), ), ); + InterchainTokenDeployedEvent { + token_id: token_id.clone(), + token_address: deployed_address.clone(), + name: token_metadata.name, + symbol: token_metadata.symbol, + decimals: token_metadata.decimal, + minter: initial_minter, + } + .emit(env); + if initial_supply > 0 { StellarAssetClient::new(env, &deployed_address).mint(&caller, &initial_supply); @@ -239,17 +254,74 @@ impl InterchainTokenServiceInterface for InterchainTokenService { Ok(token_id) } + /// Deploys an interchain token to a remote chain. + /// + /// This function initiates the deployment of an interchain token to a specified + /// destination chain. It validates the token metadata, emits a deployment event, + /// and triggers the necessary cross-chain call. + /// + /// # Arguments + /// - `env`: Reference to the contract environment. + /// - `caller`: Address of the caller initiating the deployment. The caller must authenticate. + /// - `salt`: A 32-byte unique salt used for token deployment. + /// - `destination_chain`: The name of the destination chain where the token will be deployed. + /// - `gas_token`: The token used to pay for the gas cost of the cross-chain call. + /// + /// # Returns + /// - `Result, ContractError>`: On success, returns the token ID (`BytesN<32>`). + /// On failure, returns a `ContractError`. + /// + /// # Errors + /// - `ContractError::InvalidTokenId`: If the token ID does not exist in the persistent storage. + /// - Any error propagated from `pay_gas_and_call_contract`. fn deploy_remote_interchain_token( - _env: &Env, - _caller: Address, - _salt: BytesN<32>, - _minter: Option, - _destination_chain: String, - _gas_token: Token, + env: &Env, + caller: Address, + salt: BytesN<32>, + destination_chain: String, + gas_token: Token, ) -> Result, ContractError> { - // TODO: implementation + caller.require_auth(); - todo!() + 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); + let token_address = env + .storage() + .persistent() + .get::<_, TokenIdConfigValue>(&DataKey::TokenIdConfigKey(token_id.clone())) + .ok_or(ContractError::InvalidTokenId)? + .token_address; + let token = token::Client::new(env, &token_address); + let token_metadata = TokenMetadata { + name: token.name(), + decimal: token.decimals(), + symbol: token.symbol(), + }; + + assert_ok!(validate_token_metadata(token_metadata.clone())); + + let message = Message::DeployInterchainToken(DeployInterchainToken { + token_id: token_id.clone(), + name: token_metadata.name.clone(), + symbol: token_metadata.symbol.clone(), + decimals: token_metadata.decimal as u8, + minter: None, + }); + + InterchainTokenDeploymentStartedEvent { + token_id: token_id.clone(), + token_address, + destination_chain: destination_chain.clone(), + name: token_metadata.name, + symbol: token_metadata.symbol, + decimals: token_metadata.decimal, + minter: None, + } + .emit(env); + + Self::pay_gas_and_call_contract(env, caller, destination_chain, message, gas_token)?; + + Ok(token_id) } fn interchain_transfer( diff --git a/contracts/interchain-token-service/src/error.rs b/contracts/interchain-token-service/src/error.rs index be8681bb..42018cfc 100644 --- a/contracts/interchain-token-service/src/error.rs +++ b/contracts/interchain-token-service/src/error.rs @@ -19,4 +19,6 @@ pub enum ContractError { InvalidDestinationAddress = 13, InvalidHubChain = 14, TokenAlreadyRegistered = 15, + InvalidTokenMetaData = 16, + InvalidTokenId = 17, } diff --git a/contracts/interchain-token-service/src/event.rs b/contracts/interchain-token-service/src/event.rs index 696fa031..88252c60 100644 --- a/contracts/interchain-token-service/src/event.rs +++ b/contracts/interchain-token-service/src/event.rs @@ -13,6 +13,27 @@ pub struct TrustedChainRemovedEvent { pub chain: String, } +#[derive(Debug, PartialEq, Eq)] +pub struct InterchainTokenDeployedEvent { + pub token_id: BytesN<32>, + pub token_address: Address, + pub name: String, + pub symbol: String, + pub decimals: u32, + pub minter: Option
, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct InterchainTokenDeploymentStartedEvent { + pub token_id: BytesN<32>, + pub token_address: Address, + pub destination_chain: String, + pub name: String, + pub symbol: String, + pub decimals: u32, + pub minter: Option
, +} + #[derive(Debug, PartialEq, Eq)] pub struct InterchainTokenIdClaimedEvent { pub token_id: BytesN<32>, @@ -63,6 +84,43 @@ impl Event for TrustedChainRemovedEvent { } } +impl Event for InterchainTokenDeployedEvent { + fn topics(&self, env: &Env) -> impl Topics + Debug { + ( + Symbol::new(env, "interchain_token_deployed"), + self.token_id.to_val(), + self.token_address.to_val(), + self.name.to_val(), + self.symbol.to_val(), + self.decimals, + self.minter.clone(), + ) + } + + fn data(&self, env: &Env) -> impl IntoVal + Debug { + Vec::::new(env) + } +} + +impl Event for InterchainTokenDeploymentStartedEvent { + fn topics(&self, env: &Env) -> impl Topics + Debug { + ( + Symbol::new(env, "token_deployment_started"), + self.token_id.to_val(), + self.token_address.to_val(), + self.destination_chain.to_val(), + self.name.to_val(), + self.symbol.to_val(), + self.decimals, + self.minter.clone(), + ) + } + + fn data(&self, env: &Env) -> impl IntoVal + Debug { + Vec::::new(env) + } +} + impl Event for InterchainTokenIdClaimedEvent { fn topics(&self, env: &Env) -> impl Topics + Debug { ( @@ -121,6 +179,37 @@ impl_event_testutils!(TrustedChainSetEvent, (Symbol, String), ()); #[cfg(any(test, feature = "testutils"))] impl_event_testutils!(TrustedChainRemovedEvent, (Symbol, String), ()); +#[cfg(any(test, feature = "testutils"))] +impl_event_testutils!( + InterchainTokenDeployedEvent, + ( + Symbol, + BytesN<32>, + Address, + String, + String, + u32, + Option
+ ), + () +); + +#[cfg(any(test, feature = "testutils"))] +impl_event_testutils!( + InterchainTokenDeploymentStartedEvent, + ( + Symbol, + BytesN<32>, + Address, + String, + String, + String, + u32, + Option
+ ), + () +); + #[cfg(any(test, feature = "testutils"))] impl_event_testutils!( InterchainTokenIdClaimedEvent, diff --git a/contracts/interchain-token-service/src/interface.rs b/contracts/interchain-token-service/src/interface.rs index 87f7f96d..6f881d0e 100644 --- a/contracts/interchain-token-service/src/interface.rs +++ b/contracts/interchain-token-service/src/interface.rs @@ -36,18 +36,17 @@ pub trait InterchainTokenServiceInterface: AxelarExecutableInterface { env: &Env, deployer: Address, salt: BytesN<32>, - token_meta_data: TokenMetadata, + token_metadata: TokenMetadata, initial_supply: i128, minter: Option
, ) -> Result, ContractError>; fn deploy_remote_interchain_token( - _env: &Env, + env: &Env, caller: Address, - _salt: BytesN<32>, - _minter: Option, - _destination_chain: String, - _gas_token: Token, + salt: BytesN<32>, + destination_chain: String, + gas_token: Token, ) -> Result, ContractError>; fn interchain_transfer( diff --git a/contracts/interchain-token-service/tests/canonical_token.rs b/contracts/interchain-token-service/tests/canonical_token.rs index 4d998687..c864726b 100644 --- a/contracts/interchain-token-service/tests/canonical_token.rs +++ b/contracts/interchain-token-service/tests/canonical_token.rs @@ -11,7 +11,7 @@ const PREFIX_CANONICAL_TOKEN_SALT: &str = "canonical-token-salt"; #[test] fn register_canonical_token_succeeds() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let token_address = Address::generate(&env); let chain_name = client.chain_name(); @@ -45,7 +45,7 @@ fn register_canonical_token_succeeds() { #[test] fn register_canonical_token_fails_if_already_registered() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let token_address = Address::generate(&env); client.register_canonical_token(&token_address); @@ -58,7 +58,7 @@ fn register_canonical_token_fails_if_already_registered() { #[test] fn canonical_token_id_derivation() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let token_address = Address::generate(&env); let chain_name = client.chain_name(); diff --git a/contracts/interchain-token-service/tests/deploy_interchain_token.rs b/contracts/interchain-token-service/tests/deploy_interchain_token.rs index cc306d6a..9155b6c1 100644 --- a/contracts/interchain-token-service/tests/deploy_interchain_token.rs +++ b/contracts/interchain-token-service/tests/deploy_interchain_token.rs @@ -3,38 +3,59 @@ mod utils; use axelar_soroban_std::address::AddressExt; use axelar_soroban_std::assert_contract_err; use axelar_soroban_std::assert_invoke_auth_err; +use axelar_soroban_std::events; use interchain_token::InterchainTokenClient; use interchain_token_service::error::ContractError; +use interchain_token_service::event::InterchainTokenDeployedEvent; use interchain_token_service::types::TokenManagerType; +use soroban_sdk::testutils::Address as _; use soroban_sdk::Address; use soroban_sdk::BytesN; -use soroban_sdk::IntoVal; -use soroban_sdk::{testutils::Address as _, Env}; use soroban_token_sdk::metadata::TokenMetadata; use utils::setup_env; +use utils::TokenMetadataExt; -fn setup_token_metadata(env: &Env, name: &str, symbol: &str, decimal: u32) -> TokenMetadata { - TokenMetadata { - decimal, - name: name.into_val(env), - symbol: symbol.into_val(env), - } +#[test] +fn deploy_interchain_token_succeeds() { + let (env, client, _, _, _) = setup_env(); + + let sender = Address::generate(&env); + let minter: Option
= None; + let salt = BytesN::<32>::from_array(&env, &[1; 32]); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); + let initial_supply = 100; + + client.mock_all_auths().deploy_interchain_token( + &sender, + &salt, + &token_metadata, + &initial_supply, + &minter, + ); + + goldie::assert!(events::fmt_emitted_event_at_idx::< + InterchainTokenDeployedEvent, + >(&env, -2)); } #[test] fn deploy_interchain_token_with_initial_supply_no_minter() { - let (env, client, _, _) = setup_env(); - env.mock_all_auths(); + let (env, client, _, _, _) = setup_env(); let sender = Address::generate(&env); let minter: Option
= None; let salt = BytesN::<32>::from_array(&env, &[1; 32]); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); let initial_supply = 100; - let token_id = - client.deploy_interchain_token(&sender, &salt, &token_meta_data, &initial_supply, &minter); + let token_id = client.mock_all_auths().deploy_interchain_token( + &sender, + &salt, + &token_metadata, + &initial_supply, + &minter, + ); let token_address = client.token_address(&token_id); let token = InterchainTokenClient::new(&env, &token_address); @@ -46,19 +67,19 @@ fn deploy_interchain_token_with_initial_supply_no_minter() { #[test] fn deploy_interchain_token_with_initial_supply_valid_minter() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); env.mock_all_auths(); let sender = Address::generate(&env); let minter = Address::generate(&env); let salt = BytesN::<32>::from_array(&env, &[1; 32]); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); let initial_supply = 100; let token_id = client.deploy_interchain_token( &sender, &salt, - &token_meta_data, + &token_metadata, &initial_supply, &Some(minter.clone()), ); @@ -74,13 +95,13 @@ fn deploy_interchain_token_with_initial_supply_valid_minter() { #[test] fn deploy_interchain_token_check_token_id_and_token_manager_type() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); env.mock_all_auths(); let sender = Address::generate(&env); let minter = Address::generate(&env); let salt = BytesN::<32>::from_array(&env, &[1; 32]); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); let initial_supply = 100; let deploy_salt = client.interchain_token_deploy_salt(&sender, &salt); @@ -89,7 +110,7 @@ fn deploy_interchain_token_check_token_id_and_token_manager_type() { let token_id = client.deploy_interchain_token( &sender, &salt, - &token_meta_data, + &token_metadata, &initial_supply, &Some(minter), ); @@ -103,19 +124,19 @@ fn deploy_interchain_token_check_token_id_and_token_manager_type() { #[test] fn deploy_interchain_token_zero_initial_supply_and_valid_minter() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); env.mock_all_auths(); let sender = Address::generate(&env); let minter = Address::generate(&env); let salt = BytesN::<32>::from_array(&env, &[1; 32]); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); let initial_supply = 0; let token_id = client.deploy_interchain_token( &sender, &salt, - &token_meta_data, + &token_metadata, &initial_supply, &Some(minter.clone()), ); @@ -132,20 +153,20 @@ fn deploy_interchain_token_zero_initial_supply_and_valid_minter() { #[test] fn deploy_interchain_token_falis_zero_initial_supply_and_invalid_minter() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); env.mock_all_auths(); let sender = Address::generate(&env); let minter: Option
= Some(client.address.clone()); let salt = BytesN::<32>::from_array(&env, &[1; 32]); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); let initial_supply = 0; assert_contract_err!( client.try_deploy_interchain_token( &sender, &salt, - &token_meta_data, + &token_metadata, &initial_supply, &minter ), @@ -155,17 +176,17 @@ fn deploy_interchain_token_falis_zero_initial_supply_and_invalid_minter() { #[test] fn deploy_interchain_token_zero_initial_supply_no_minter() { - let (env, client, _, _) = setup_env(); + 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 token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); let initial_supply = 0; let token_id = - client.deploy_interchain_token(&sender, &salt, &token_meta_data, &initial_supply, &minter); + client.deploy_interchain_token(&sender, &salt, &token_metadata, &initial_supply, &minter); let token_address = client.token_address(&token_id); let token = InterchainTokenClient::new(&env, &token_address); @@ -176,16 +197,33 @@ fn deploy_interchain_token_zero_initial_supply_no_minter() { assert_eq!(token.balance(&sender), initial_supply); } +#[test] +#[should_panic(expected = "HostError: Error(Context, InvalidAction)")] +fn deploy_interchain_token_fails_with_invalid_decimals() { + 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, "name", "symbol", invalid_decimals); + let initial_supply = 0; + + client.deploy_interchain_token(&sender, &salt, &token_metadata, &initial_supply, &minter); +} + #[test] fn deploy_interchain_token_fails_with_invalid_auth() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); env.mock_all_auths(); let sender = Address::generate(&env); let user = Address::generate(&env); let minter: Option
= None; let salt = BytesN::<32>::from_array(&env, &[1; 32]); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); + let initial_supply = 100; assert_invoke_auth_err!( @@ -193,7 +231,7 @@ fn deploy_interchain_token_fails_with_invalid_auth() { client.try_deploy_interchain_token( &sender, &salt, - &token_meta_data, + &token_metadata, &initial_supply, &minter, ) diff --git a/contracts/interchain-token-service/tests/deploy_remote_interchain_token.rs b/contracts/interchain-token-service/tests/deploy_remote_interchain_token.rs new file mode 100644 index 00000000..b1fdaaa2 --- /dev/null +++ b/contracts/interchain-token-service/tests/deploy_remote_interchain_token.rs @@ -0,0 +1,196 @@ +mod utils; + +use axelar_soroban_std::assert_contract_err; +use axelar_soroban_std::auth_invocation; +use axelar_soroban_std::events; +use interchain_token_service::error::ContractError; +use interchain_token_service::event::InterchainTokenDeploymentStartedEvent; +use interchain_token_service::types::DeployInterchainToken; +use interchain_token_service::types::HubMessage; +use interchain_token_service::types::Message; +use soroban_sdk::testutils::AuthorizedFunction; +use soroban_sdk::testutils::AuthorizedInvocation; +use soroban_sdk::Bytes; +use soroban_sdk::IntoVal; +use soroban_sdk::Symbol; +use soroban_sdk::{testutils::Address as _, Address, BytesN, String}; +use soroban_token_sdk::metadata::TokenMetadata; +use utils::setup_env; +use utils::setup_gas_token; +use utils::TokenMetadataExt; + +#[test] +fn deploy_remote_interchain_token_succeeds() { + let (env, client, _, _, _) = setup_env(); + + let sender = Address::generate(&env); + let gas_token = setup_gas_token(&env, &sender); + let minter: Option
= None; + let salt = BytesN::<32>::from_array(&env, &[1; 32]); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); + let initial_supply = 1; + + let token_id = client.mock_all_auths().deploy_interchain_token( + &sender, + &salt, + &token_metadata, + &initial_supply, + &minter, + ); + + let destination_chain = String::from_str(&env, "ethereum"); + client + .mock_all_auths() + .set_trusted_chain(&destination_chain); + + let deployed_token_id = client.mock_all_auths().deploy_remote_interchain_token( + &sender, + &salt, + &destination_chain, + &gas_token, + ); + assert_eq!(token_id, deployed_token_id); + + goldie::assert!(events::fmt_emitted_event_at_idx::< + InterchainTokenDeploymentStartedEvent, + >(&env, -4)); +} + +#[test] +fn deploy_remote_interchain_token_auth_test() { + let (env, client, _, gas_service, _) = setup_env(); + + let sender = Address::generate(&env); + let gas_token = setup_gas_token(&env, &sender); + let minter: Option
= None; + let salt = BytesN::<32>::from_array(&env, &[1; 32]); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); + let initial_supply = 1; + + let token_id = client.mock_all_auths().deploy_interchain_token( + &sender, + &salt, + &token_metadata, + &initial_supply, + &minter, + ); + + let destination_chain = String::from_str(&env, "ethereum"); + let its_hub_chain = String::from_str(&env, "axelar"); + let its_hub_address = String::from_str(&env, "its_hub_address"); + + client + .mock_all_auths() + .set_trusted_chain(&destination_chain); + + let deployed_token_id = client.mock_all_auths().deploy_remote_interchain_token( + &sender, + &salt, + &destination_chain, + &gas_token, + ); + assert_eq!(token_id, deployed_token_id); + + let message = Message::DeployInterchainToken(DeployInterchainToken { + token_id, + name: token_metadata.name.clone(), + symbol: token_metadata.symbol.clone(), + decimals: token_metadata.decimal as u8, + minter: None, + }); + + let payload = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message, + } + .abi_encode(&env); + + let transfer_auth = auth_invocation!(&env, + "transfer", + gas_token.clone().address => + ( + &sender, + gas_service.address.clone(), + gas_token.amount + ) + ); + + let pay_gas_auth = auth_invocation!(&env, + "pay_gas", + gas_service.address => + ( + client.address.clone(), + its_hub_chain, + its_hub_address, + payload, + &sender, + gas_token.clone(), + &Bytes::new(&env) + ), + transfer_auth + ); + + let deploy_remote_interchain_token_auth = auth_invocation!(&env, + sender, + "deploy_remote_interchain_token", + client.address => + ( + &sender, + salt, + destination_chain, + gas_token + ), + pay_gas_auth + ); + + assert_eq!(env.auths(), deploy_remote_interchain_token_auth); +} + +#[test] +fn deploy_remote_interchain_token_fails_untrusted_chain() { + let (env, client, _, _, _) = setup_env(); + + let sender = Address::generate(&env); + let gas_token = setup_gas_token(&env, &sender); + let minter: Option
= None; + let salt = BytesN::<32>::from_array(&env, &[1; 32]); + let token_metadata = TokenMetadata::new(&env, "name", "symbol", 6); + let initial_supply = 1; + + client.mock_all_auths().deploy_interchain_token( + &sender, + &salt, + &token_metadata, + &initial_supply, + &minter, + ); + + let destination_chain = String::from_str(&env, "ethereum"); + + assert_contract_err!( + client.mock_all_auths().try_deploy_remote_interchain_token( + &sender, + &salt, + &destination_chain, + &gas_token, + ), + ContractError::UntrustedChain + ); +} + +#[test] +fn deploy_remote_interchain_token_fails_with_invalid_token_id() { + let (env, client, _, _, _) = setup_env(); + env.mock_all_auths(); + + let sender = Address::generate(&env); + let gas_token = setup_gas_token(&env, &sender); + let salt = BytesN::<32>::from_array(&env, &[1; 32]); + + let destination_chain = String::from_str(&env, "ethereum"); + + assert_contract_err!( + client.try_deploy_remote_interchain_token(&sender, &salt, &destination_chain, &gas_token), + ContractError::InvalidTokenId + ); +} diff --git a/contracts/interchain-token-service/tests/executable.rs b/contracts/interchain-token-service/tests/executable.rs index 1d986915..a676c235 100644 --- a/contracts/interchain-token-service/tests/executable.rs +++ b/contracts/interchain-token-service/tests/executable.rs @@ -118,7 +118,7 @@ mod test { #[test] fn interchain_transfer_execute_succeeds() { - let (env, client, gateway_client, signers) = setup_env(); + let (env, client, gateway_client, _, signers) = setup_env(); register_chains(&env, &client); let executable_id = env.register(test::ExecutableContract, (client.address.clone(),)); @@ -175,7 +175,7 @@ fn interchain_transfer_execute_succeeds() { #[test] fn executable_fails_if_not_executed_from_its() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let executable_id = env.register(test::ExecutableContract, (client.address.clone(),)); let executable_client = test::ExecutableContractClient::new(&env, &executable_id); diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index 2581efb2..d9f99243 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -8,7 +8,7 @@ use utils::setup_env; #[test] #[should_panic(expected = "Error(Contract, #1)")] // ExecutableError::NotApproved fn execute_fails_without_gateway_approval() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let source_chain = String::from_str(&env, "chain"); let message_id = String::from_str(&env, "test"); @@ -21,7 +21,7 @@ fn execute_fails_without_gateway_approval() { #[test] #[should_panic(expected = "Error(Contract, #8)")] // ContractError::InsufficientMessageLength fn execute_fails_with_invalid_message() { - let (env, client, gateway_client, signers) = setup_env(); + let (env, client, gateway_client, _, signers) = setup_env(); let source_chain = client.its_hub_chain_name(); let message_id = String::from_str(&env, "test"); diff --git a/contracts/interchain-token-service/tests/interchain_transfer.rs b/contracts/interchain-token-service/tests/interchain_transfer.rs index 6cbd5673..3e64eadd 100644 --- a/contracts/interchain-token-service/tests/interchain_transfer.rs +++ b/contracts/interchain-token-service/tests/interchain_transfer.rs @@ -14,7 +14,7 @@ use utils::{register_chains, setup_env, setup_gas_token, setup_its_token, HUB_CH #[test] fn interchain_transfer_send_succeeds() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let sender: Address = Address::generate(&env); let gas_token = setup_gas_token(&env, &sender); @@ -47,7 +47,7 @@ fn interchain_transfer_send_succeeds() { #[test] #[should_panic(expected = "burn, Error(Contract, #9)")] fn interchain_transfer_send_fails_on_insufficient_balance() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); register_chains(&env, &client); let sender: Address = Address::generate(&env); @@ -72,7 +72,7 @@ fn interchain_transfer_send_fails_on_insufficient_balance() { #[test] fn interchain_transfer_receive_succeeds() { - let (env, client, gateway_client, signers) = setup_env(); + let (env, client, gateway_client, _, signers) = setup_env(); register_chains(&env, &client); let sender = Address::generate(&env).to_xdr(&env); diff --git a/contracts/interchain-token-service/tests/message_routing.rs b/contracts/interchain-token-service/tests/message_routing.rs index b3187f19..86424548 100644 --- a/contracts/interchain-token-service/tests/message_routing.rs +++ b/contracts/interchain-token-service/tests/message_routing.rs @@ -6,7 +6,7 @@ use utils::{register_chains, setup_env, setup_gas_token, setup_its_token}; #[test] fn send_directly_to_hub_chain_fails() { - let (env, client, _gateway_client, _) = setup_env(); + let (env, client, _gateway_client, _, _) = setup_env(); let sender: Address = Address::generate(&env); let amount = 1; @@ -29,7 +29,7 @@ fn send_directly_to_hub_chain_fails() { #[test] fn send_to_untrusted_chain_fails() { - let (env, client, _gateway_client, _) = setup_env(); + let (env, client, _gateway_client, _, _) = setup_env(); register_chains(&env, &client); let sender: Address = Address::generate(&env); diff --git a/contracts/interchain-token-service/tests/testdata/deploy_interchain_token_succeeds.golden b/contracts/interchain-token-service/tests/testdata/deploy_interchain_token_succeeds.golden new file mode 100644 index 00000000..1f090518 --- /dev/null +++ b/contracts/interchain-token-service/tests/testdata/deploy_interchain_token_succeeds.golden @@ -0,0 +1,3 @@ +contract: Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5) +topics: (Symbol(interchain_token_deployed), BytesN<32>(127, 197, 48, 155, 232, 144, 233, 5, 104, 135, 130, 118, 76, 97, 230, 164, 117, 144, 45, 242, 69, 90, 124, 72, 93, 71, 115, 62, 188, 31, 2, 167), Contract(CBXOPW23I3THDRTZG2QKYC3WFGQ2Y5BEU2OAPTNLCLFFXQE3BEUJKBOQ), String(name), String(symbol), 6, Some(Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5))) +data: () \ No newline at end of file diff --git a/contracts/interchain-token-service/tests/testdata/deploy_remote_interchain_token_succeeds.golden b/contracts/interchain-token-service/tests/testdata/deploy_remote_interchain_token_succeeds.golden new file mode 100644 index 00000000..23dcca6d --- /dev/null +++ b/contracts/interchain-token-service/tests/testdata/deploy_remote_interchain_token_succeeds.golden @@ -0,0 +1,3 @@ +contract: Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5) +topics: (Symbol(token_deployment_started), BytesN<32>(127, 197, 48, 155, 232, 144, 233, 5, 104, 135, 130, 118, 76, 97, 230, 164, 117, 144, 45, 242, 69, 90, 124, 72, 93, 71, 115, 62, 188, 31, 2, 167), Contract(CBXOPW23I3THDRTZG2QKYC3WFGQ2Y5BEU2OAPTNLCLFFXQE3BEUJKBOQ), String(ethereum), String(name), String(symbol), 6, None) +data: () \ No newline at end of file diff --git a/contracts/interchain-token-service/tests/testdata/interchain_token.wasm b/contracts/interchain-token-service/tests/testdata/interchain_token.wasm index b7e0927a..20da8008 100755 Binary files a/contracts/interchain-token-service/tests/testdata/interchain_token.wasm and b/contracts/interchain-token-service/tests/testdata/interchain_token.wasm differ diff --git a/contracts/interchain-token-service/tests/trusted_chain.rs b/contracts/interchain-token-service/tests/trusted_chain.rs index 1a7ad828..6faf8939 100644 --- a/contracts/interchain-token-service/tests/trusted_chain.rs +++ b/contracts/interchain-token-service/tests/trusted_chain.rs @@ -12,7 +12,7 @@ use soroban_sdk::{Address, String}; #[test] fn set_trusted_address() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let chain = String::from_str(&env, "chain"); @@ -25,7 +25,7 @@ fn set_trusted_address() { #[test] fn set_trusted_chain_fails_if_not_owner() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let not_owner = Address::generate(&env); let chain = String::from_str(&env, "chain"); @@ -35,7 +35,7 @@ fn set_trusted_chain_fails_if_not_owner() { #[test] fn set_trusted_chain_fails_if_already_set() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); env.mock_all_auths(); let chain = String::from_str(&env, "chain"); @@ -49,7 +49,7 @@ fn set_trusted_chain_fails_if_already_set() { #[test] fn remove_trusted_chain() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); let chain = String::from_str(&env, "chain"); @@ -66,7 +66,7 @@ fn remove_trusted_chain() { #[test] fn remove_trusted_chain_fails_if_not_set() { - let (env, client, _, _) = setup_env(); + let (env, client, _, _, _) = setup_env(); env.mock_all_auths(); let chain = String::from_str(&env, "chain"); diff --git a/contracts/interchain-token-service/tests/utils/mod.rs b/contracts/interchain-token-service/tests/utils/mod.rs index 86b39b27..c60b97c0 100644 --- a/contracts/interchain-token-service/tests/utils/mod.rs +++ b/contracts/interchain-token-service/tests/utils/mod.rs @@ -3,8 +3,8 @@ use axelar_gateway::testutils::{setup_gateway, TestSignerSet}; use axelar_gateway::AxelarGatewayClient; use axelar_soroban_std::types::Token; use interchain_token_service::{InterchainTokenService, InterchainTokenServiceClient}; -use soroban_sdk::BytesN; use soroban_sdk::{testutils::Address as _, token::StellarAssetClient, Address, Env, String}; +use soroban_sdk::{BytesN, IntoVal}; use soroban_token_sdk::metadata::TokenMetadata; pub const HUB_CHAIN: &str = "axelar"; @@ -54,16 +54,17 @@ pub fn setup_env<'a>() -> ( Env, InterchainTokenServiceClient<'a>, AxelarGatewayClient<'a>, + AxelarGasServiceClient<'a>, TestSignerSet, ) { let env = Env::default(); let (signers, gateway_client) = setup_gateway(&env, 0, 5); - let gas_service_client = setup_gas_service(&env); + let gas_service_client: AxelarGasServiceClient<'_> = setup_gas_service(&env); let client = setup_its(&env, &gateway_client, &gas_service_client); - (env, client, gateway_client, signers) + (env, client, gateway_client, gas_service_client, signers) } #[allow(dead_code)] @@ -90,7 +91,7 @@ pub fn setup_its_token( supply: i128, ) -> BytesN<32> { let salt = BytesN::from_array(env, &[1u8; 32]); - let token_meta_data = TokenMetadata { + let token_metadata = TokenMetadata { name: String::from_str(env, "Test"), symbol: String::from_str(env, "TEST"), decimal: 18, @@ -99,7 +100,7 @@ pub fn setup_its_token( let token_id = client.mock_all_auths().deploy_interchain_token( sender, &salt, - &token_meta_data, + &token_metadata, &supply, &None, ); @@ -112,3 +113,18 @@ pub fn register_chains(env: &Env, client: &InterchainTokenServiceClient) { let chain = String::from_str(env, HUB_CHAIN); client.mock_all_auths().set_trusted_chain(&chain); } + +#[allow(dead_code)] +pub trait TokenMetadataExt { + fn new(env: &Env, name: &str, symbol: &str, decimal: u32) -> Self; +} + +impl TokenMetadataExt for TokenMetadata { + fn new(env: &Env, name: &str, symbol: &str, decimal: u32) -> Self { + Self { + decimal, + name: name.into_val(env), + symbol: symbol.into_val(env), + } + } +} diff --git a/contracts/interchain-token/src/contract.rs b/contracts/interchain-token/src/contract.rs index 5acbb0bf..81f82844 100644 --- a/contracts/interchain-token/src/contract.rs +++ b/contracts/interchain-token/src/contract.rs @@ -1,3 +1,4 @@ +use axelar_soroban_std::token::validate_token_metadata; use axelar_soroban_std::ttl::{ extend_instance_ttl, INSTANCE_TTL_EXTEND_TO, INSTANCE_TTL_THRESHOLD, }; @@ -31,15 +32,15 @@ impl InterchainToken { owner: Address, minter: Option
, token_id: BytesN<32>, - token_meta_data: TokenMetadata, + token_metadata: TokenMetadata, ) { interfaces::set_owner(&env, &owner); - if let Err(err) = Self::validate_token_metadata(token_meta_data.clone()) { + if let Err(err) = validate_token_metadata(token_metadata.clone()) { panic_with_error!(env, err); } - Self::write_metadata(&env, token_meta_data); + Self::write_metadata(&env, token_metadata); env.storage().instance().set(&DataKey::TokenId, &token_id); @@ -234,19 +235,6 @@ impl InterchainToken { assert_with_error!(env, amount >= 0, ContractError::InvalidAmount); } - fn validate_token_metadata( - TokenMetadata { - decimal, - name, - symbol, - }: TokenMetadata, - ) -> Result<(), ContractError> { - ensure!(decimal <= u8::MAX.into(), ContractError::InvalidDecimal); - ensure!(!name.is_empty(), ContractError::InvalidTokenName); - ensure!(!symbol.is_empty(), ContractError::InvalidTokenSymbol); - Ok(()) - } - fn extend_balance_ttl(env: &Env, key: &DataKey) { env.storage() .persistent() diff --git a/contracts/interchain-token/tests/test.rs b/contracts/interchain-token/tests/test.rs index 371857ca..8f05dce2 100644 --- a/contracts/interchain-token/tests/test.rs +++ b/contracts/interchain-token/tests/test.rs @@ -24,11 +24,11 @@ fn setup_token<'a>(env: &Env) -> (InterchainTokenClient<'a>, Address, Address) { let owner = Address::generate(env); let minter = Address::generate(env); let token_id: BytesN<32> = BytesN::<32>::random(env); - let token_meta_data = setup_token_metadata(env, "name", "symbol", 6); + let token_metadata = setup_token_metadata(env, "name", "symbol", 6); let contract_id = env.register( InterchainToken, - (owner.clone(), minter.clone(), &token_id, token_meta_data), + (owner.clone(), minter.clone(), &token_id, token_metadata), ); let token = InterchainTokenClient::new(env, &contract_id); @@ -42,9 +42,9 @@ fn register_token_with_invalid_decimals_fails() { let owner = Address::generate(&env); let minter = Address::generate(&env); let token_id: BytesN<32> = BytesN::<32>::random(&env); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", u32::from(u8::MAX) + 1); + let token_metadata = setup_token_metadata(&env, "name", "symbol", u32::from(u8::MAX) + 1); - env.register(InterchainToken, (owner, minter, &token_id, token_meta_data)); + env.register(InterchainToken, (owner, minter, &token_id, token_metadata)); } #[test] @@ -54,9 +54,9 @@ fn register_token_with_invalid_name_fails() { let owner = Address::generate(&env); let minter = Address::generate(&env); let token_id: BytesN<32> = BytesN::<32>::random(&env); - let token_meta_data = setup_token_metadata(&env, "", "symbol", 1); + let token_metadata = setup_token_metadata(&env, "", "symbol", 1); - env.register(InterchainToken, (owner, minter, &token_id, token_meta_data)); + env.register(InterchainToken, (owner, minter, &token_id, token_metadata)); } #[test] @@ -66,9 +66,9 @@ fn register_token_with_invalid_symbol_fails() { let owner = Address::generate(&env); let minter = Address::generate(&env); let token_id: BytesN<32> = BytesN::<32>::random(&env); - let token_meta_data = setup_token_metadata(&env, "name", "", 1); + let token_metadata = setup_token_metadata(&env, "name", "", 1); - env.register(InterchainToken, (owner, minter, &token_id, token_meta_data)); + env.register(InterchainToken, (owner, minter, &token_id, token_metadata)); } #[test] @@ -88,12 +88,12 @@ fn register_interchain_token_without_minter() { let owner = Address::generate(&env); let token_id: BytesN<32> = BytesN::<32>::random(&env); - let token_meta_data = setup_token_metadata(&env, "name", "symbol", 6); + let token_metadata = setup_token_metadata(&env, "name", "symbol", 6); let minter: Option
= None; let contract_id = env.register( InterchainToken, - (owner.clone(), minter, &token_id, token_meta_data), + (owner.clone(), minter, &token_id, token_metadata), ); let token = InterchainTokenClient::new(&env, &contract_id); diff --git a/packages/axelar-soroban-std/Cargo.toml b/packages/axelar-soroban-std/Cargo.toml index 62568a3b..9f80c7c9 100644 --- a/packages/axelar-soroban-std/Cargo.toml +++ b/packages/axelar-soroban-std/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["rlib"] axelar-soroban-std-derive = { workspace = true, optional = true } hex = { workspace = true, optional = true } soroban-sdk = { workspace = true } +soroban-token-sdk = { workspace = true } [dev-dependencies] axelar-soroban-std-derive = { workspace = true } diff --git a/packages/axelar-soroban-std/src/lib.rs b/packages/axelar-soroban-std/src/lib.rs index ee0ec193..ef13850d 100644 --- a/packages/axelar-soroban-std/src/lib.rs +++ b/packages/axelar-soroban-std/src/lib.rs @@ -24,5 +24,7 @@ pub mod interfaces; pub mod address; +pub mod token; + #[cfg(feature = "derive")] pub use axelar_soroban_std_derive::*; diff --git a/packages/axelar-soroban-std/src/token.rs b/packages/axelar-soroban-std/src/token.rs new file mode 100644 index 00000000..dca3b01d --- /dev/null +++ b/packages/axelar-soroban-std/src/token.rs @@ -0,0 +1,28 @@ +use crate::ensure; +use soroban_sdk::contracterror; +use soroban_token_sdk::metadata::TokenMetadata; + +#[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(()) +}