odra_modules/
cep18_token.rs

1//! CEP-18 Casper Fungible Token standard implementation.
2use odra::casper_types::U256;
3use odra::prelude::*;
4
5use crate::cep18::errors::Error;
6
7use crate::cep18::events::{
8    Burn, ChangeSecurity, DecreaseAllowance, IncreaseAllowance, Mint, SetAllowance, Transfer,
9    TransferFrom
10};
11use crate::cep18::storage::{
12    Cep18AllowancesStorage, Cep18BalancesStorage, Cep18DecimalsStorage, Cep18NameStorage,
13    Cep18SymbolStorage, Cep18TotalSupplyStorage
14};
15use crate::cep18::utils::{Cep18Modality, SecurityBadge};
16
17/// CEP-18 token module
18#[odra::module(
19    events = [
20        Mint, Burn, SetAllowance, IncreaseAllowance, DecreaseAllowance, Transfer,TransferFrom, ChangeSecurity
21    ],
22    errors = Error
23)]
24pub struct Cep18 {
25    decimals: SubModule<Cep18DecimalsStorage>,
26    symbol: SubModule<Cep18SymbolStorage>,
27    name: SubModule<Cep18NameStorage>,
28    total_supply: SubModule<Cep18TotalSupplyStorage>,
29    balances: SubModule<Cep18BalancesStorage>,
30    allowances: SubModule<Cep18AllowancesStorage>,
31    security_badges: Mapping<Address, SecurityBadge>,
32    modality: Var<Cep18Modality>
33}
34
35#[odra::module]
36impl Cep18 {
37    /// Initializes the contract with the given metadata, initial supply, security and modality.
38    #[allow(clippy::too_many_arguments)]
39    pub fn init(
40        &mut self,
41        symbol: String,
42        name: String,
43        decimals: u8,
44        initial_supply: U256,
45        admin_list: Vec<Address>,
46        minter_list: Vec<Address>,
47        modality: Option<Cep18Modality>
48    ) {
49        let caller = self.env().caller();
50        // set the metadata
51        self.symbol.set(symbol);
52        self.name.set(name);
53        self.decimals.set(decimals);
54        self.total_supply.set(initial_supply);
55
56        // mint the initial supply for the caller
57        self.balances.set(&caller, initial_supply);
58        self.env().emit_event(Mint {
59            recipient: caller,
60            amount: initial_supply
61        });
62
63        // set the security badges
64        self.security_badges.set(&caller, SecurityBadge::Admin);
65
66        for admin in admin_list {
67            self.security_badges.set(&admin, SecurityBadge::Admin);
68        }
69
70        for minter in minter_list {
71            self.security_badges.set(&minter, SecurityBadge::Minter);
72        }
73
74        // set the modality
75        if let Some(modality) = modality {
76            self.modality.set(modality);
77        }
78    }
79
80    /// Admin EntryPoint to manipulate the security access granted to users.
81    /// One user can only possess one access group badge.
82    /// Change strength: None > Admin > Minter
83    /// Change strength meaning by example: If user is added to both Minter and Admin, they will be an
84    /// Admin, also if a user is added to Admin and None then they will be removed from having rights.
85    /// Beware: do not remove the last Admin because that will lock out all admin functionality.
86    pub fn change_security(
87        &mut self,
88        admin_list: Vec<Address>,
89        minter_list: Vec<Address>,
90        none_list: Vec<Address>
91    ) {
92        self.assert_burn_and_mint_enabled();
93
94        // check if the caller has the admin badge
95        let caller = self.env().caller();
96        self.assert_is_admin(&caller);
97
98        let mut badges_map = BTreeMap::new();
99
100        // set the security badges
101        for admin in admin_list {
102            self.security_badges.set(&admin, SecurityBadge::Admin);
103            badges_map.insert(admin, SecurityBadge::Admin);
104        }
105
106        for minter in minter_list {
107            self.security_badges.set(&minter, SecurityBadge::Minter);
108            badges_map.insert(minter, SecurityBadge::Minter);
109        }
110
111        for none in none_list {
112            self.security_badges.set(&none, SecurityBadge::None);
113            badges_map.insert(none, SecurityBadge::None);
114        }
115
116        badges_map.remove(&caller);
117
118        self.env().emit_event(ChangeSecurity {
119            admin: caller,
120            sec_change_map: badges_map
121        });
122    }
123
124    /// Returns the name of the token.
125    pub fn name(&self) -> String {
126        self.name.get()
127    }
128
129    /// Returns the symbol of the token.
130    pub fn symbol(&self) -> String {
131        self.symbol.get()
132    }
133
134    /// Returns the number of decimals the token uses.
135    pub fn decimals(&self) -> u8 {
136        self.decimals.get()
137    }
138
139    /// Returns the total supply of the token.
140    pub fn total_supply(&self) -> U256 {
141        self.total_supply.get()
142    }
143
144    /// Returns the balance of the given address.
145    pub fn balance_of(&self, address: &Address) -> U256 {
146        self.balances.get(address).unwrap_or_default()
147    }
148
149    /// Returns the amount of tokens the owner has allowed the spender to spend.
150    pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 {
151        self.allowances.get_or_default(owner, spender)
152    }
153
154    /// Approves the spender to spend the given amount of tokens on behalf of the caller.
155    pub fn approve(&mut self, spender: &Address, amount: &U256) {
156        let owner = self.env().caller();
157        if owner == *spender {
158            self.env().revert(Error::CannotTargetSelfUser);
159        }
160
161        self.allowances.set(&owner, spender, *amount);
162        self.env().emit_event(SetAllowance {
163            owner,
164            spender: *spender,
165            allowance: *amount
166        });
167    }
168
169    /// Decreases the allowance of the spender by the given amount.
170    pub fn decrease_allowance(&mut self, spender: &Address, decr_by: &U256) {
171        let owner = self.env().caller();
172        let allowance = self.allowance(&owner, spender);
173        self.allowances
174            .set(&owner, spender, allowance.saturating_sub(*decr_by));
175        self.env().emit_event(DecreaseAllowance {
176            owner,
177            spender: *spender,
178            allowance,
179            decr_by: *decr_by
180        });
181    }
182
183    /// Increases the allowance of the spender by the given amount.
184    pub fn increase_allowance(&mut self, spender: &Address, inc_by: &U256) {
185        let owner = self.env().caller();
186        if owner == *spender {
187            self.env().revert(Error::CannotTargetSelfUser);
188        }
189        let allowance = self.allowances.get_or_default(&owner, spender);
190
191        self.allowances
192            .set(&owner, spender, allowance.saturating_add(*inc_by));
193        self.env().emit_event(IncreaseAllowance {
194            owner,
195            spender: *spender,
196            allowance,
197            inc_by: *inc_by
198        });
199    }
200
201    /// Transfers tokens from the caller to the recipient.
202    pub fn transfer(&mut self, recipient: &Address, amount: &U256) {
203        let caller = self.env().caller();
204        if caller == *recipient {
205            self.env().revert(Error::CannotTargetSelfUser);
206        }
207
208        self.raw_transfer(&caller, recipient, amount);
209    }
210
211    /// Transfers tokens from the owner to the recipient using the spender's allowance.
212    pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) {
213        let spender = self.env().caller();
214
215        if owner == recipient {
216            self.env().revert(Error::CannotTargetSelfUser);
217        }
218
219        if amount.is_zero() {
220            return;
221        }
222
223        let allowance = self.allowance(owner, &spender);
224
225        self.allowances.set(
226            owner,
227            recipient,
228            allowance
229                .checked_sub(*amount)
230                .unwrap_or_revert_with(self, Error::InsufficientAllowance)
231        );
232        self.env().emit_event(TransferFrom {
233            spender,
234            owner: *owner,
235            recipient: *recipient,
236            amount: *amount
237        });
238
239        self.raw_transfer(owner, recipient, amount);
240    }
241
242    /// Mints new tokens and assigns them to the given address.
243    pub fn mint(&mut self, owner: &Address, amount: &U256) {
244        self.assert_burn_and_mint_enabled();
245        self.assert_is_minter(&self.env().caller());
246        self.raw_mint(owner, amount);
247    }
248
249    /// Burns the given amount of tokens from the given address.
250    pub fn burn(&mut self, owner: &Address, amount: &U256) {
251        self.assert_burn_and_mint_enabled();
252
253        if self.env().caller() != *owner {
254            self.env().revert(Error::InvalidBurnTarget);
255        }
256
257        if self.balance_of(owner) < *amount {
258            self.env().revert(Error::InsufficientBalance);
259        }
260
261        self.raw_burn(owner, amount);
262    }
263}
264
265impl Cep18 {
266    /// Transfers tokens from the sender to the recipient without checking the permissions.
267    pub fn raw_transfer(&mut self, sender: &Address, recipient: &Address, amount: &U256) {
268        if *amount > self.balances.get(sender).unwrap_or_default() {
269            self.env().revert(Error::InsufficientBalance)
270        }
271
272        if amount > &U256::zero() {
273            self.balances.subtract(sender, *amount);
274            self.balances.add(recipient, *amount);
275        }
276
277        self.env().emit_event(Transfer {
278            sender: *sender,
279            recipient: *recipient,
280            amount: *amount
281        });
282    }
283
284    /// Mints new tokens and assigns them to the given address without checking the permissions.
285    pub fn raw_mint(&mut self, owner: &Address, amount: &U256) {
286        self.total_supply.add(*amount);
287        self.balances.add(owner, *amount);
288
289        self.env().emit_event(Mint {
290            recipient: *owner,
291            amount: *amount
292        });
293    }
294
295    /// Burns the given amount of tokens from the given address without checking the permissions.
296    pub fn raw_burn(&mut self, owner: &Address, amount: &U256) {
297        self.total_supply.subtract(*amount);
298        self.balances.subtract(owner, *amount);
299
300        self.env().emit_event(Burn {
301            owner: *owner,
302            amount: *amount
303        });
304    }
305
306    /// Changes the security access granted to users without checking the permissions.
307    pub fn raw_change_security(
308        &mut self,
309        admin_list: Vec<Address>,
310        minter_list: Vec<Address>,
311        none_list: Vec<Address>
312    ) {
313        let mut badges_map = BTreeMap::new();
314
315        // set the security badges
316        for admin in admin_list {
317            self.security_badges.set(&admin, SecurityBadge::Admin);
318            badges_map.insert(admin, SecurityBadge::Admin);
319        }
320
321        for minter in minter_list {
322            self.security_badges.set(&minter, SecurityBadge::Minter);
323            badges_map.insert(minter, SecurityBadge::Minter);
324        }
325
326        for none in none_list {
327            self.security_badges.set(&none, SecurityBadge::None);
328            badges_map.insert(none, SecurityBadge::None);
329        }
330
331        self.env().emit_event(ChangeSecurity {
332            admin: self.env().caller(),
333            sec_change_map: badges_map
334        });
335    }
336
337    #[inline]
338    /// Returns true if the given address is an admin.
339    pub fn is_admin(&self, address: &Address) -> bool {
340        self.security_badges
341            .get(address)
342            .map_or(false, |badge| badge.can_admin())
343    }
344
345    #[inline]
346    /// Returns true if the given address is a minter.
347    pub fn is_minter(&self, address: &Address) -> bool {
348        self.security_badges
349            .get(address)
350            .map_or(false, |badge| badge.can_mint())
351    }
352
353    /// Asserts that the caller is an admin.
354    pub fn assert_is_admin(&self, address: &Address) {
355        let badge = self
356            .security_badges
357            .get(address)
358            .unwrap_or_revert_with(self, Error::InsufficientRights);
359
360        if !badge.can_admin() {
361            self.env().revert(Error::InsufficientRights);
362        }
363    }
364
365    /// Asserts that the caller is a minter.
366    pub fn assert_is_minter(&self, address: &Address) {
367        let badge = self
368            .security_badges
369            .get(address)
370            .unwrap_or_revert_with(self, Error::InsufficientRights);
371
372        if !badge.can_mint() {
373            self.env().revert(Error::InsufficientRights);
374        }
375    }
376
377    /// Returns true if the mint and burn functionality is enabled.
378    #[inline]
379    pub fn is_burn_and_mint_enabled(&self) -> bool {
380        self.modality.get_or_default().mint_and_burn_enabled()
381    }
382
383    /// Asserts that the mint and burn functionality is enabled.
384    fn assert_burn_and_mint_enabled(&mut self) {
385        if !self.is_burn_and_mint_enabled() {
386            self.env().revert(Error::MintBurnDisabled);
387        }
388    }
389}
390
391#[cfg(test)]
392pub(crate) mod tests {
393    use alloc::string::ToString;
394    use alloc::vec;
395
396    use crate::cep18::utils::Cep18Modality;
397    use crate::cep18_token::{Cep18, Cep18InitArgs};
398    use odra::casper_types::account::AccountHash;
399    use odra::casper_types::ContractPackageHash;
400    use odra::host::{Deployer, HostEnv, HostRef};
401    use odra::prelude::*;
402
403    use super::Cep18HostRef;
404
405    pub const TOKEN_NAME: &str = "Plascoin";
406    pub const TOKEN_SYMBOL: &str = "PLS";
407    pub const TOKEN_DECIMALS: u8 = 100;
408    pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000;
409    pub const TOKEN_OWNER_AMOUNT_1: u64 = 1_000_000;
410    pub const TOKEN_OWNER_AMOUNT_2: u64 = 2_000_000;
411    pub const TRANSFER_AMOUNT_1: u64 = 200_001;
412    pub const ALLOWANCE_AMOUNT_1: u64 = 456_789;
413    pub const ALLOWANCE_AMOUNT_2: u64 = 87_654;
414
415    pub fn setup(enable_mint_and_burn: bool) -> Cep18HostRef {
416        let env = odra_test::env();
417        let modality = if enable_mint_and_burn {
418            Cep18Modality::MintAndBurn
419        } else {
420            Cep18Modality::None
421        };
422        let init_args = Cep18InitArgs {
423            symbol: TOKEN_SYMBOL.to_string(),
424            name: TOKEN_NAME.to_string(),
425            decimals: TOKEN_DECIMALS,
426            initial_supply: TOKEN_TOTAL_SUPPLY.into(),
427            admin_list: vec![],
428            minter_list: vec![],
429            modality: Some(modality)
430        };
431        setup_with_args(&env, init_args)
432    }
433
434    pub fn setup_with_args(env: &HostEnv, args: Cep18InitArgs) -> Cep18HostRef {
435        Cep18::deploy(env, args)
436    }
437
438    pub fn invert_address(address: Address) -> Address {
439        match address {
440            Address::Account(hash) => Address::Contract(ContractPackageHash::new(hash.value())),
441            Address::Contract(hash) => Address::Account(AccountHash(hash.value()))
442        }
443    }
444
445    #[test]
446    fn should_have_queryable_properties() {
447        let cep18_token = setup(false);
448
449        assert_eq!(cep18_token.name(), TOKEN_NAME);
450        assert_eq!(cep18_token.symbol(), TOKEN_SYMBOL);
451        assert_eq!(cep18_token.decimals(), TOKEN_DECIMALS);
452        assert_eq!(cep18_token.total_supply(), TOKEN_TOTAL_SUPPLY.into());
453
454        let owner_key = cep18_token.env().caller();
455        let owner_balance = cep18_token.balance_of(&owner_key);
456        assert_eq!(owner_balance, TOKEN_TOTAL_SUPPLY.into());
457
458        let contract_balance = cep18_token.balance_of(cep18_token.address());
459        assert_eq!(contract_balance, 0.into());
460
461        // Ensures that Account and Contract ownership is respected, and we're not keying ownership under
462        // the raw bytes regardless of variant.
463        let inverted_owner_key = invert_address(owner_key);
464        let inverted_owner_balance = cep18_token.balance_of(&inverted_owner_key);
465        assert_eq!(inverted_owner_balance, 0.into());
466    }
467}