Skip to main content

stellar_interchain_token/
contract.rs

1use soroban_token_sdk::events::{Approve, Burn, MintWithAmountOnly, TransferWithAmountOnly};
2use soroban_token_sdk::metadata::TokenMetadata;
3use soroban_token_sdk::TokenUtils;
4use stellar_axelar_std::events::Event;
5use stellar_axelar_std::interfaces::OwnableInterface;
6use stellar_axelar_std::token::StellarAssetInterface;
7use stellar_axelar_std::{
8    assert_with_error, contract, contractimpl, ensure, interfaces, only_owner, soroban_sdk,
9    Address, BytesN, Env, MuxedAddress, String, Upgradable,
10};
11
12use crate::error::ContractError;
13use crate::event::{MinterAddedEvent, MinterRemovedEvent, SetAdminEvent};
14use crate::interface::InterchainTokenInterface;
15use crate::storage::{self, AllowanceDataKey, AllowanceValue};
16
17#[contract]
18#[derive(Upgradable)]
19pub struct InterchainToken;
20
21#[contractimpl]
22impl InterchainToken {
23    pub fn __constructor(
24        env: Env,
25        owner: Address,
26        minter: Option<Address>,
27        token_id: BytesN<32>,
28        token_metadata: TokenMetadata,
29    ) {
30        interfaces::set_owner(&env, &owner);
31
32        Self::write_metadata(&env, token_metadata);
33
34        storage::set_token_id(&env, &token_id);
35
36        if let Some(minter) = minter {
37            storage::set_minter_status(&env, minter.clone());
38
39            MinterAddedEvent { minter }.emit(&env);
40        }
41    }
42}
43
44#[contractimpl]
45impl OwnableInterface for InterchainToken {
46    #[allow_during_migration]
47    fn owner(env: &Env) -> Address {
48        interfaces::owner(env)
49    }
50
51    fn transfer_ownership(env: &Env, new_owner: Address) {
52        let old_owner = Self::owner(env);
53
54        interfaces::transfer_ownership::<Self>(env, new_owner.clone());
55
56        SetAdminEvent {
57            admin: old_owner,
58            new_admin: new_owner,
59        }
60        .emit(env);
61    }
62}
63
64// Note: Some methods below are intentionally unimplemented as they are not supported by this token
65#[contractimpl]
66impl StellarAssetInterface for InterchainToken {
67    fn allowance(env: Env, from: Address, spender: Address) -> i128 {
68        Self::read_allowance(&env, from, spender).amount
69    }
70
71    fn approve(env: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32) {
72        from.require_auth();
73
74        Self::validate_amount(&env, amount);
75
76        Self::write_allowance(
77            &env,
78            from.clone(),
79            spender.clone(),
80            amount,
81            expiration_ledger,
82        );
83
84        Approve {
85            from,
86            spender,
87            amount,
88            expiration_ledger,
89        }
90        .publish(&env);
91    }
92
93    fn balance(env: Env, id: Address) -> i128 {
94        storage::try_balance(&env, id).unwrap_or_default()
95    }
96
97    fn transfer(env: Env, from: Address, to: MuxedAddress, amount: i128) {
98        from.require_auth();
99
100        Self::validate_amount(&env, amount);
101        Self::spend_balance(&env, from.clone(), amount);
102        let to_address = to.address();
103        Self::receive_balance(&env, to_address.clone(), amount);
104
105        TransferWithAmountOnly {
106            from,
107            to: to_address,
108            amount,
109        }
110        .publish(&env);
111    }
112
113    fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128) {
114        spender.require_auth();
115
116        Self::validate_amount(&env, amount);
117        Self::spend_allowance(&env, from.clone(), spender, amount);
118        Self::spend_balance(&env, from.clone(), amount);
119        Self::receive_balance(&env, to.clone(), amount);
120
121        TransferWithAmountOnly { from, to, amount }.publish(&env);
122    }
123
124    fn burn(env: Env, from: Address, amount: i128) {
125        from.require_auth();
126
127        Self::validate_amount(&env, amount);
128        Self::spend_balance(&env, from.clone(), amount);
129
130        Burn { from, amount }.publish(&env);
131    }
132
133    fn burn_from(env: Env, spender: Address, from: Address, amount: i128) {
134        spender.require_auth();
135
136        Self::validate_amount(&env, amount);
137        Self::spend_allowance(&env, from.clone(), spender, amount);
138        Self::spend_balance(&env, from.clone(), amount);
139
140        Burn { from, amount }.publish(&env);
141    }
142
143    fn decimals(env: Env) -> u32 {
144        TokenUtils::new(&env).metadata().get_metadata().decimal
145    }
146
147    fn name(env: Env) -> String {
148        TokenUtils::new(&env).metadata().get_metadata().name
149    }
150
151    fn symbol(env: Env) -> String {
152        TokenUtils::new(&env).metadata().get_metadata().symbol
153    }
154
155    fn set_admin(env: Env, admin: Address) {
156        Self::transfer_ownership(&env, admin);
157    }
158
159    fn admin(env: Env) -> Address {
160        Self::owner(&env)
161    }
162
163    fn set_authorized(_env: Env, _id: Address, _authorize: bool) {
164        unimplemented!()
165    }
166
167    fn authorized(_env: Env, _id: Address) -> bool {
168        unimplemented!()
169    }
170
171    fn mint(env: Env, to: Address, amount: i128) {
172        let owner = Self::owner(&env);
173        owner.require_auth();
174
175        Self::validate_amount(&env, amount);
176
177        Self::receive_balance(&env, to.clone(), amount);
178
179        MintWithAmountOnly { to, amount }.publish(&env);
180    }
181
182    fn clawback(_env: Env, _from: Address, _amount: i128) {
183        unimplemented!()
184    }
185}
186
187#[contractimpl]
188impl InterchainTokenInterface for InterchainToken {
189    fn token_id(env: &Env) -> BytesN<32> {
190        storage::token_id(env)
191    }
192
193    fn is_minter(env: &Env, minter: Address) -> bool {
194        storage::is_minter(env, minter)
195    }
196
197    fn mint_from(
198        env: &Env,
199        minter: Address,
200        to: Address,
201        amount: i128,
202    ) -> Result<(), ContractError> {
203        minter.require_auth();
204
205        ensure!(Self::is_minter(env, minter), ContractError::NotMinter);
206
207        Self::validate_amount(env, amount);
208
209        Self::receive_balance(env, to.clone(), amount);
210
211        MintWithAmountOnly { to, amount }.publish(env);
212
213        Ok(())
214    }
215
216    #[only_owner]
217    fn add_minter(env: &Env, minter: Address) {
218        assert_with_error!(
219            env,
220            !Self::is_minter(env, minter.clone()),
221            ContractError::MinterAlreadyExists
222        );
223
224        storage::set_minter_status(env, minter.clone());
225
226        MinterAddedEvent { minter }.emit(env);
227    }
228
229    #[only_owner]
230    fn remove_minter(env: &Env, minter: Address) {
231        assert_with_error!(
232            env,
233            Self::is_minter(env, minter.clone()),
234            ContractError::NotMinter
235        );
236
237        storage::remove_minter_status(env, minter.clone());
238
239        MinterRemovedEvent { minter }.emit(env);
240    }
241}
242
243impl InterchainToken {
244    fn validate_amount(env: &Env, amount: i128) {
245        assert_with_error!(env, amount >= 0, ContractError::InvalidAmount);
246    }
247
248    fn read_allowance(env: &Env, from: Address, spender: Address) -> AllowanceValue {
249        let key = AllowanceDataKey { from, spender };
250        storage::try_allowance(env, key).map_or(
251            AllowanceValue {
252                amount: 0,
253                expiration_ledger: 0,
254            },
255            |allowance| {
256                if allowance.expiration_ledger < env.ledger().sequence() {
257                    AllowanceValue {
258                        amount: 0,
259                        expiration_ledger: allowance.expiration_ledger,
260                    }
261                } else {
262                    allowance
263                }
264            },
265        )
266    }
267
268    fn write_allowance(
269        env: &Env,
270        from: Address,
271        spender: Address,
272        amount: i128,
273        expiration_ledger: u32,
274    ) {
275        let allowance = AllowanceValue {
276            amount,
277            expiration_ledger,
278        };
279
280        assert_with_error!(
281            env,
282            !(amount > 0 && expiration_ledger < env.ledger().sequence()),
283            ContractError::InvalidExpirationLedger
284        );
285
286        let key = AllowanceDataKey { from, spender };
287        storage::set_allowance(env, key.clone(), &allowance);
288
289        if amount > 0 {
290            let live_for = expiration_ledger
291                .checked_sub(env.ledger().sequence())
292                .unwrap();
293
294            storage::extend_allowance_ttl(env, key, live_for, live_for);
295        }
296    }
297
298    fn spend_allowance(env: &Env, from: Address, spender: Address, amount: i128) {
299        let allowance = Self::read_allowance(env, from.clone(), spender.clone());
300
301        assert_with_error!(
302            env,
303            allowance.amount >= amount,
304            ContractError::InsufficientAllowance
305        );
306
307        if amount > 0 {
308            Self::write_allowance(
309                env,
310                from,
311                spender,
312                allowance
313                    .amount
314                    .checked_sub(amount)
315                    .expect("insufficient allowance"),
316                allowance.expiration_ledger,
317            );
318        }
319    }
320
321    fn receive_balance(env: &Env, addr: Address, amount: i128) {
322        let current_balance = storage::try_balance(env, addr.clone()).unwrap_or_default();
323
324        storage::set_balance(env, addr, &(current_balance + amount));
325    }
326
327    fn spend_balance(env: &Env, addr: Address, amount: i128) {
328        let balance = storage::try_balance(env, addr.clone()).unwrap_or_default();
329
330        assert_with_error!(env, balance >= amount, ContractError::InsufficientBalance);
331
332        storage::set_balance(env, addr, &(balance - amount));
333    }
334
335    fn write_metadata(env: &Env, metadata: TokenMetadata) {
336        TokenUtils::new(env).metadata().set_metadata(&metadata);
337    }
338}