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