stellar_interchain_token_service/
contract.rs

1use soroban_token_sdk::metadata::TokenMetadata;
2use stellar_axelar_gas_service::AxelarGasServiceClient;
3use stellar_axelar_gateway::executable::{AxelarExecutableInterface, CustomAxelarExecutable};
4use stellar_axelar_gateway::AxelarGatewayMessagingClient;
5use stellar_axelar_std::address::AddressExt;
6use stellar_axelar_std::events::Event;
7use stellar_axelar_std::token::StellarAssetClient;
8use stellar_axelar_std::types::Token;
9use stellar_axelar_std::xdr::ToXdr;
10use stellar_axelar_std::{
11    contract, contractimpl, ensure, interfaces, only_operator, only_owner, soroban_sdk, vec,
12    when_not_paused, Address, AxelarExecutable, Bytes, BytesN, Env, IntoVal, Operatable, Ownable,
13    Pausable, String, Symbol, Upgradable, Val,
14};
15use stellar_interchain_token::InterchainTokenClient;
16
17use crate::error::ContractError;
18use crate::event::{
19    InterchainTokenDeploymentStartedEvent, InterchainTransferReceivedEvent,
20    InterchainTransferSentEvent, TrustedChainRemovedEvent, TrustedChainSetEvent,
21};
22use crate::flow_limit::FlowDirection;
23use crate::interface::InterchainTokenServiceInterface;
24use crate::storage::{self, TokenIdConfigValue};
25use crate::token_metadata::TokenMetadataExt;
26use crate::types::{
27    DeployInterchainToken, HubMessage, InterchainTransfer, Message, TokenManagerType,
28};
29use crate::{deployer, flow_limit, migrate, token_handler, token_id, token_metadata};
30
31const ITS_HUB_CHAIN_NAME: &str = "axelar";
32const EXECUTE_WITH_INTERCHAIN_TOKEN: &str = "execute_with_interchain_token";
33
34#[contract]
35#[derive(Operatable, Ownable, Pausable, Upgradable, AxelarExecutable)]
36#[migratable]
37pub struct InterchainTokenService;
38
39#[contractimpl]
40impl InterchainTokenService {
41    pub fn __constructor(
42        env: Env,
43        owner: Address,
44        operator: Address,
45        gateway: Address,
46        gas_service: Address,
47        its_hub_address: String,
48        chain_name: String,
49        native_token_address: Address,
50        interchain_token_wasm_hash: BytesN<32>,
51        token_manager_wasm_hash: BytesN<32>,
52    ) {
53        interfaces::set_owner(&env, &owner);
54        interfaces::set_operator(&env, &operator);
55        storage::set_gateway(&env, &gateway);
56        storage::set_gas_service(&env, &gas_service);
57        storage::set_its_hub_address(&env, &its_hub_address);
58        storage::set_chain_name(&env, &chain_name);
59        storage::set_native_token_address(&env, &native_token_address);
60        storage::set_interchain_token_wasm_hash(&env, &interchain_token_wasm_hash);
61        storage::set_token_manager_wasm_hash(&env, &token_manager_wasm_hash);
62    }
63}
64
65#[contractimpl]
66impl InterchainTokenServiceInterface for InterchainTokenService {
67    fn gas_service(env: &Env) -> Address {
68        storage::gas_service(env)
69    }
70
71    fn chain_name(env: &Env) -> String {
72        storage::chain_name(env)
73    }
74
75    fn its_hub_chain_name(env: &Env) -> String {
76        String::from_str(env, ITS_HUB_CHAIN_NAME)
77    }
78
79    fn its_hub_address(env: &Env) -> String {
80        storage::its_hub_address(env)
81    }
82
83    fn native_token_address(env: &Env) -> Address {
84        storage::native_token_address(env)
85    }
86
87    fn interchain_token_wasm_hash(env: &Env) -> BytesN<32> {
88        storage::interchain_token_wasm_hash(env)
89    }
90
91    fn token_manager_wasm_hash(env: &Env) -> BytesN<32> {
92        storage::token_manager_wasm_hash(env)
93    }
94
95    fn is_trusted_chain(env: &Env, chain: String) -> bool {
96        storage::is_trusted_chain(env, chain)
97    }
98
99    #[only_operator]
100    fn set_trusted_chain(env: &Env, chain: String) -> Result<(), ContractError> {
101        ensure!(
102            !storage::is_trusted_chain(env, chain.clone()),
103            ContractError::TrustedChainAlreadySet
104        );
105
106        storage::set_trusted_chain_status(env, chain.clone());
107
108        TrustedChainSetEvent { chain }.emit(env);
109
110        Ok(())
111    }
112
113    #[only_operator]
114    fn remove_trusted_chain(env: &Env, chain: String) -> Result<(), ContractError> {
115        ensure!(
116            storage::is_trusted_chain(env, chain.clone()),
117            ContractError::TrustedChainNotSet
118        );
119
120        storage::remove_trusted_chain_status(env, chain.clone());
121
122        TrustedChainRemovedEvent { chain }.emit(env);
123
124        Ok(())
125    }
126
127    fn interchain_token_id(env: &Env, deployer: Address, salt: BytesN<32>) -> BytesN<32> {
128        token_id::interchain_token_id(env, Self::chain_name_hash(env), deployer, salt)
129    }
130
131    fn canonical_interchain_token_id(env: &Env, token_address: Address) -> BytesN<32> {
132        token_id::canonical_interchain_token_id(env, Self::chain_name_hash(env), token_address)
133    }
134
135    fn interchain_token_address(env: &Env, token_id: BytesN<32>) -> Address {
136        deployer::interchain_token_address(env, token_id)
137    }
138
139    fn token_manager_address(env: &Env, token_id: BytesN<32>) -> Address {
140        deployer::token_manager_address(env, token_id)
141    }
142
143    fn registered_token_address(env: &Env, token_id: BytesN<32>) -> Address {
144        storage::token_id_config(env, token_id).token_address
145    }
146
147    fn deployed_token_manager(env: &Env, token_id: BytesN<32>) -> Address {
148        storage::token_id_config(env, token_id).token_manager
149    }
150
151    fn token_manager_type(env: &Env, token_id: BytesN<32>) -> TokenManagerType {
152        storage::token_id_config(env, token_id).token_manager_type
153    }
154
155    fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option<i128> {
156        flow_limit::flow_limit(env, token_id)
157    }
158
159    fn flow_out_amount(env: &Env, token_id: BytesN<32>) -> i128 {
160        flow_limit::flow_out_amount(env, token_id)
161    }
162
163    fn flow_in_amount(env: &Env, token_id: BytesN<32>) -> i128 {
164        flow_limit::flow_in_amount(env, token_id)
165    }
166
167    #[only_operator]
168    fn set_flow_limit(
169        env: &Env,
170        token_id: BytesN<32>,
171        flow_limit: Option<i128>,
172    ) -> Result<(), ContractError> {
173        flow_limit::set_flow_limit(env, token_id, flow_limit)
174    }
175
176    #[when_not_paused]
177    fn deploy_interchain_token(
178        env: &Env,
179        caller: Address,
180        salt: BytesN<32>,
181        token_metadata: TokenMetadata,
182        initial_supply: i128,
183        minter: Option<Address>,
184    ) -> Result<BytesN<32>, ContractError> {
185        caller.require_auth();
186
187        ensure!(initial_supply >= 0, ContractError::InvalidInitialSupply);
188        ensure!(
189            initial_supply > 0 || minter.is_some(),
190            ContractError::InvalidTokenConfig
191        );
192
193        let token_id = Self::interchain_token_id(env, caller.clone(), salt);
194
195        token_metadata.validate()?;
196
197        let token_address = Self::deploy_token(env, token_id.clone(), token_metadata, minter)?;
198
199        if initial_supply > 0 {
200            StellarAssetClient::new(env, &token_address).mint(&caller, &initial_supply);
201        }
202
203        Ok(token_id)
204    }
205
206    #[when_not_paused]
207    fn deploy_remote_interchain_token(
208        env: &Env,
209        caller: Address,
210        salt: BytesN<32>,
211        destination_chain: String,
212        gas_token: Option<Token>,
213    ) -> Result<BytesN<32>, ContractError> {
214        caller.require_auth();
215
216        let token_id = Self::interchain_token_id(env, caller.clone(), salt);
217
218        Self::deploy_remote_token(env, caller, token_id.clone(), destination_chain, gas_token)?;
219
220        Ok(token_id)
221    }
222
223    #[when_not_paused]
224    fn register_canonical_token(
225        env: &Env,
226        token_address: Address,
227    ) -> Result<BytesN<32>, ContractError> {
228        // Validates the token address and it's associated token metadata
229        let _ =
230            token_metadata::token_metadata(env, &token_address, &Self::native_token_address(env))?;
231
232        let token_id = Self::canonical_interchain_token_id(env, token_address.clone());
233
234        Self::ensure_token_not_registered(env, token_id.clone())?;
235
236        let _: Address = Self::deploy_token_manager(
237            env,
238            token_id.clone(),
239            token_address,
240            TokenManagerType::LockUnlock,
241        );
242
243        Ok(token_id)
244    }
245
246    #[when_not_paused]
247    fn deploy_remote_canonical_token(
248        env: &Env,
249        token_address: Address,
250        destination_chain: String,
251        spender: Address,
252        gas_token: Option<Token>,
253    ) -> Result<BytesN<32>, ContractError> {
254        spender.require_auth();
255
256        let token_id = Self::canonical_interchain_token_id(env, token_address);
257
258        Self::deploy_remote_token(env, spender, token_id.clone(), destination_chain, gas_token)?;
259
260        Ok(token_id)
261    }
262
263    #[when_not_paused]
264    fn interchain_transfer(
265        env: &Env,
266        caller: Address,
267        token_id: BytesN<32>,
268        destination_chain: String,
269        destination_address: Bytes,
270        amount: i128,
271        data: Option<Bytes>,
272        gas_token: Option<Token>,
273    ) -> Result<(), ContractError> {
274        ensure!(amount > 0, ContractError::InvalidAmount);
275
276        ensure!(
277            !destination_address.is_empty(),
278            ContractError::InvalidDestinationAddress
279        );
280
281        if let Some(ref data) = data {
282            ensure!(!data.is_empty(), ContractError::InvalidData);
283        }
284
285        caller.require_auth();
286
287        token_handler::take_token(
288            env,
289            &caller,
290            Self::token_id_config(env, token_id.clone())?,
291            amount,
292        )?;
293
294        FlowDirection::Out.add_flow(env, token_id.clone(), amount)?;
295
296        InterchainTransferSentEvent {
297            token_id: token_id.clone(),
298            source_address: caller.clone(),
299            destination_chain: destination_chain.clone(),
300            destination_address: destination_address.clone(),
301            amount,
302            data: data.clone(),
303        }
304        .emit(env);
305
306        let message = Message::InterchainTransfer(InterchainTransfer {
307            token_id,
308            source_address: caller.to_string_bytes(),
309            destination_address,
310            amount,
311            data,
312        });
313
314        Self::pay_gas_and_call_contract(env, caller, destination_chain, message, gas_token)?;
315
316        Ok(())
317    }
318
319    #[only_owner]
320    fn migrate_token(
321        env: &Env,
322        token_id: BytesN<32>,
323        upgrader: Address,
324        new_version: String,
325    ) -> Result<(), ContractError> {
326        migrate::migrate_token(env, token_id, upgrader, new_version)
327    }
328}
329
330impl InterchainTokenService {
331    fn pay_gas_and_call_contract(
332        env: &Env,
333        caller: Address,
334        destination_chain: String,
335        message: Message,
336        gas_token: Option<Token>,
337    ) -> Result<(), ContractError> {
338        ensure!(
339            Self::is_trusted_chain(env, destination_chain.clone()),
340            ContractError::UntrustedChain
341        );
342
343        let gateway = AxelarGatewayMessagingClient::new(env, &Self::gateway(env));
344        let gas_service = AxelarGasServiceClient::new(env, &Self::gas_service(env));
345
346        let payload = HubMessage::SendToHub {
347            destination_chain,
348            message,
349        }
350        .abi_encode(env)?;
351
352        let hub_chain = Self::its_hub_chain_name(env);
353        let hub_address = Self::its_hub_address(env);
354
355        if let Some(gas_token) = gas_token {
356            gas_service.pay_gas(
357                &env.current_contract_address(),
358                &hub_chain,
359                &hub_address,
360                &payload,
361                &caller,
362                &gas_token,
363                &Bytes::new(env),
364            );
365        }
366
367        gateway.call_contract(
368            &env.current_contract_address(),
369            &hub_chain,
370            &hub_address,
371            &payload,
372        );
373
374        Ok(())
375    }
376
377    /// Validate that the message is coming from the ITS Hub and decode the message
378    fn get_execute_params(
379        env: &Env,
380        source_chain: String,
381        source_address: String,
382        payload: Bytes,
383    ) -> Result<(String, Message), ContractError> {
384        ensure!(
385            source_chain == Self::its_hub_chain_name(env),
386            ContractError::NotHubChain
387        );
388        ensure!(
389            source_address == Self::its_hub_address(env),
390            ContractError::NotHubAddress
391        );
392
393        let HubMessage::ReceiveFromHub {
394            source_chain: original_source_chain,
395            message,
396        } = HubMessage::abi_decode(env, &payload)?
397        else {
398            return Err(ContractError::InvalidMessageType);
399        };
400        ensure!(
401            storage::is_trusted_chain(env, original_source_chain.clone()),
402            ContractError::UntrustedChain
403        );
404
405        Ok((original_source_chain, message))
406    }
407
408    fn set_token_id_config(env: &Env, token_id: BytesN<32>, token_data: TokenIdConfigValue) {
409        storage::set_token_id_config(env, token_id, &token_data);
410    }
411
412    /// Retrieves the configuration value for the specified token ID.
413    ///
414    /// # Arguments
415    /// - `token_id`: A 32-byte unique identifier for the token.
416    ///
417    /// # Returns
418    /// - `Ok(TokenIdConfigValue)`: The configuration value if it exists.
419    ///
420    /// # Errors
421    /// - `ContractError::InvalidTokenId`: If the token ID does not exist in storage.
422    fn token_id_config(
423        env: &Env,
424        token_id: BytesN<32>,
425    ) -> Result<TokenIdConfigValue, ContractError> {
426        storage::try_token_id_config(env, token_id).ok_or(ContractError::InvalidTokenId)
427    }
428
429    fn chain_name_hash(env: &Env) -> BytesN<32> {
430        let chain_name = Self::chain_name(env);
431        env.crypto().keccak256(&chain_name.to_xdr(env)).into()
432    }
433
434    /// Deploys a remote token on a specified destination chain.
435    ///
436    /// This function retrieves and validates the token's metadata
437    /// and emits an event indicating the start of the token deployment process.
438    /// It also constructs and sends the deployment message to the remote chain.
439    ///
440    /// # Arguments
441    /// * `caller` - Address of the caller initiating the deployment.
442    /// * `token_id` - The token ID for the remote token being deployed.
443    /// * `destination_chain` - The name of the destination chain where the token will be deployed.
444    /// * `gas_token` - An optional gas token used to pay for gas during the deployment.
445    ///
446    /// # Errors
447    /// - `ContractError::InvalidDestinationChain`: If the `destination_chain` is the current chain.
448    /// - `ContractError::InvalidTokenId`: If the token ID is invalid.
449    /// - Errors propagated from `token_metadata`.
450    /// - Any error propagated from `pay_gas_and_call_contract`.
451    ///
452    /// # Authorization
453    /// - The `caller` must authenticate.
454    fn deploy_remote_token(
455        env: &Env,
456        caller: Address,
457        token_id: BytesN<32>,
458        destination_chain: String,
459        gas_token: Option<Token>,
460    ) -> Result<(), ContractError> {
461        ensure!(
462            destination_chain != Self::chain_name(env),
463            ContractError::InvalidDestinationChain
464        );
465
466        let token_address = Self::token_id_config(env, token_id.clone())?.token_address;
467        let TokenMetadata {
468            name,
469            symbol,
470            decimal,
471        } = token_metadata::token_metadata(env, &token_address, &Self::native_token_address(env))?;
472
473        let message = Message::DeployInterchainToken(DeployInterchainToken {
474            token_id: token_id.clone(),
475            name: name.clone(),
476            symbol: symbol.clone(),
477            decimals: decimal as u8,
478            minter: None,
479        });
480
481        InterchainTokenDeploymentStartedEvent {
482            token_id,
483            token_address,
484            destination_chain: destination_chain.clone(),
485            name,
486            symbol,
487            decimals: decimal,
488            minter: None,
489        }
490        .emit(env);
491
492        Self::pay_gas_and_call_contract(env, caller, destination_chain, message, gas_token)?;
493
494        Ok(())
495    }
496
497    fn execute_transfer_message(
498        env: &Env,
499        source_chain: &String,
500        message_id: String,
501        InterchainTransfer {
502            token_id,
503            source_address,
504            destination_address,
505            amount,
506            data,
507        }: InterchainTransfer,
508    ) -> Result<(), ContractError> {
509        ensure!(amount > 0, ContractError::InvalidAmount);
510
511        let destination_address = Address::from_string_bytes(&destination_address);
512
513        let token_config_value = Self::token_id_config(env, token_id.clone())?;
514        let token_address = token_config_value.token_address.clone();
515
516        FlowDirection::In.add_flow(env, token_id.clone(), amount)?;
517
518        token_handler::give_token(env, &destination_address, token_config_value, amount)?;
519
520        InterchainTransferReceivedEvent {
521            source_chain: source_chain.clone(),
522            token_id: token_id.clone(),
523            source_address: source_address.clone(),
524            destination_address: destination_address.clone(),
525            amount,
526            data_hash: data.as_ref().map(|d| env.crypto().keccak256(d).into()),
527        }
528        .emit(env);
529
530        if let Some(payload) = data {
531            Self::execute_contract_with_token(
532                env,
533                destination_address,
534                source_chain,
535                message_id,
536                source_address,
537                payload,
538                token_id,
539                token_address,
540                amount,
541            );
542        }
543
544        Ok(())
545    }
546
547    fn execute_contract_with_token(
548        env: &Env,
549        destination_address: Address,
550        source_chain: &String,
551        message_id: String,
552        source_address: Bytes,
553        payload: Bytes,
554        token_id: BytesN<32>,
555        token_address: Address,
556        amount: i128,
557    ) {
558        // Due to limitations of the soroban-sdk, there is no type-safe client for contract execution.
559        // The invocation might return a value, so we use Val as the return type to avoid panics
560        env.invoke_contract::<Val>(
561            &destination_address,
562            &Symbol::new(env, EXECUTE_WITH_INTERCHAIN_TOKEN),
563            vec![
564                env,
565                source_chain.to_val(),
566                message_id.to_val(),
567                source_address.to_val(),
568                payload.to_val(),
569                token_id.to_val(),
570                token_address.to_val(),
571                amount.into_val(env),
572            ],
573        );
574    }
575
576    fn execute_deploy_message(
577        env: &Env,
578        DeployInterchainToken {
579            token_id,
580            name,
581            symbol,
582            decimals,
583            minter,
584        }: DeployInterchainToken,
585    ) -> Result<(), ContractError> {
586        let token_metadata = TokenMetadata::new(name, symbol, decimals as u32)?;
587
588        // Note: attempt to convert a byte string which doesn't represent a valid Soroban address fails at the Host level
589        let minter = minter.map(|m| Address::from_string_bytes(&m));
590
591        let _: Address = Self::deploy_token(env, token_id, token_metadata, minter)?;
592
593        Ok(())
594    }
595
596    fn deploy_token_manager(
597        env: &Env,
598        token_id: BytesN<32>,
599        token_address: Address,
600        token_manager_type: TokenManagerType,
601    ) -> Address {
602        let token_manager = deployer::deploy_token_manager(
603            env,
604            Self::token_manager_wasm_hash(env),
605            token_id.clone(),
606            token_address.clone(),
607            token_manager_type,
608        );
609
610        Self::set_token_id_config(
611            env,
612            token_id,
613            TokenIdConfigValue {
614                token_address,
615                token_manager: token_manager.clone(),
616                token_manager_type,
617            },
618        );
619
620        token_manager
621    }
622
623    /// Deploy an interchain token on the current chain and its corresponding token manager.
624    ///
625    /// # Arguments
626    /// * `token_id` - The token ID for the interchain token being deployed.
627    /// * `token_metadata` - The metadata for the interchain token being deployed.
628    /// * `minter` - An optional address of an additional minter for the interchain token being deployed.
629    fn deploy_token(
630        env: &Env,
631        token_id: BytesN<32>,
632        token_metadata: TokenMetadata,
633        minter: Option<Address>,
634    ) -> Result<Address, ContractError> {
635        Self::ensure_token_not_registered(env, token_id.clone())?;
636
637        let token_address = deployer::deploy_interchain_token(
638            env,
639            Self::interchain_token_wasm_hash(env),
640            minter,
641            token_id.clone(),
642            token_metadata,
643        );
644        let interchain_token_client = InterchainTokenClient::new(env, &token_address);
645
646        let token_manager = Self::deploy_token_manager(
647            env,
648            token_id,
649            token_address.clone(),
650            TokenManagerType::NativeInterchainToken,
651        );
652
653        // Give minter role to the token manager
654        // Check if token_manager is already a minter before adding to avoid MinterAlreadyExists error
655        if !interchain_token_client.is_minter(&token_manager) {
656            interchain_token_client.add_minter(&token_manager);
657        }
658
659        Ok(token_address)
660    }
661
662    fn ensure_token_not_registered(env: &Env, token_id: BytesN<32>) -> Result<(), ContractError> {
663        ensure!(
664            storage::try_token_id_config(env, token_id).is_none(),
665            ContractError::TokenAlreadyRegistered
666        );
667
668        Ok(())
669    }
670}
671
672impl CustomAxelarExecutable for InterchainTokenService {
673    type Error = ContractError;
674
675    fn __gateway(env: &Env) -> Address {
676        storage::gateway(env)
677    }
678
679    #[when_not_paused]
680    fn __execute(
681        env: &Env,
682        source_chain: String,
683        message_id: String,
684        source_address: String,
685        payload: Bytes,
686    ) -> Result<(), Self::Error> {
687        let (source_chain, message) =
688            Self::get_execute_params(env, source_chain, source_address, payload)?;
689
690        match message {
691            Message::InterchainTransfer(message) => {
692                Self::execute_transfer_message(env, &source_chain, message_id, message)
693            }
694            Message::DeployInterchainToken(message) => Self::execute_deploy_message(env, message),
695        }?;
696
697        Ok(())
698    }
699}