Skip to main content

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, DecreaseAllowance, IncreaseAllowance, Mint, SetAllowance, Transfer, TransferFrom
9};
10use crate::cep18::storage::{
11    Cep18AllowancesStorage, Cep18BalancesStorage, Cep18DecimalsStorage, Cep18NameStorage,
12    Cep18SymbolStorage, Cep18TotalSupplyStorage
13};
14
15/// CEP-18 token module
16#[odra::module(
17    events = [
18        Mint, Burn, SetAllowance, IncreaseAllowance, DecreaseAllowance, Transfer, TransferFrom
19    ],
20    errors = Error
21)]
22pub struct Cep18 {
23    decimals: SubModule<Cep18DecimalsStorage>,
24    symbol: SubModule<Cep18SymbolStorage>,
25    name: SubModule<Cep18NameStorage>,
26    total_supply: SubModule<Cep18TotalSupplyStorage>,
27    balances: SubModule<Cep18BalancesStorage>,
28    allowances: SubModule<Cep18AllowancesStorage>
29}
30
31#[odra::module]
32impl Cep18 {
33    /// Initializes the contract with the given metadata, initial supply.
34    pub fn init(&mut self, symbol: String, name: String, decimals: u8, initial_supply: U256) {
35        let caller = self.env().caller();
36
37        // Set the metadata
38        self.symbol.set(symbol);
39        self.name.set(name);
40        self.decimals.set(decimals);
41        self.total_supply.set(initial_supply);
42
43        if !initial_supply.is_zero() {
44            // If the initial supply is not zero:
45            // - mint the initial supply to the caller,
46            // - emit the `Mint` event.
47            self.balances.set(&caller, initial_supply);
48            self.env().emit_event(Mint {
49                recipient: caller,
50                amount: initial_supply
51            });
52        } else {
53            // If the initial supply is zero, initialize `balances`.
54            self.balances.init();
55        }
56
57        // Initialize allowances.
58        self.allowances.init();
59    }
60
61    /// Returns the name of the token.
62    pub fn name(&self) -> String {
63        self.name.get()
64    }
65
66    /// Returns the symbol of the token.
67    pub fn symbol(&self) -> String {
68        self.symbol.get()
69    }
70
71    /// Returns the number of decimals the token uses.
72    pub fn decimals(&self) -> u8 {
73        self.decimals.get()
74    }
75
76    /// Returns the total supply of the token.
77    pub fn total_supply(&self) -> U256 {
78        self.total_supply.get()
79    }
80
81    /// Returns the balance of the given address.
82    pub fn balance_of(&self, address: &Address) -> U256 {
83        self.balances.get(address).unwrap_or_default()
84    }
85
86    /// Returns the amount of tokens the owner has allowed the spender to spend.
87    pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 {
88        self.allowances.get_or_default(owner, spender)
89    }
90
91    /// Approves the spender to spend the given amount of tokens on behalf of the caller.
92    pub fn approve(&mut self, spender: &Address, amount: &U256) {
93        let owner = self.env().caller();
94        if owner == *spender {
95            self.env().revert(Error::CannotTargetSelfUser);
96        }
97        self.raw_approve(&owner, spender, amount);
98    }
99
100    /// Decreases the allowance of the spender by the given amount.
101    pub fn decrease_allowance(&mut self, spender: &Address, decr_by: &U256) {
102        let owner = self.env().caller();
103        let allowance = self.allowance(&owner, spender);
104        self.allowances
105            .set(&owner, spender, allowance.saturating_sub(*decr_by));
106        self.env().emit_event(DecreaseAllowance {
107            owner,
108            spender: *spender,
109            allowance,
110            decr_by: *decr_by
111        });
112    }
113
114    /// Increases the allowance of the spender by the given amount.
115    pub fn increase_allowance(&mut self, spender: &Address, inc_by: &U256) {
116        let owner = self.env().caller();
117        if owner == *spender {
118            self.env().revert(Error::CannotTargetSelfUser);
119        }
120        let allowance = self.allowances.get_or_default(&owner, spender);
121
122        self.allowances
123            .set(&owner, spender, allowance.saturating_add(*inc_by));
124        self.env().emit_event(IncreaseAllowance {
125            owner,
126            spender: *spender,
127            allowance,
128            inc_by: *inc_by
129        });
130    }
131
132    /// Transfers tokens from the caller to the recipient.
133    pub fn transfer(&mut self, recipient: &Address, amount: &U256) {
134        let caller = self.env().caller();
135        if caller == *recipient {
136            self.env().revert(Error::CannotTargetSelfUser);
137        }
138
139        self.raw_transfer(&caller, recipient, amount);
140
141        self.env().emit_event(Transfer {
142            sender: caller,
143            recipient: *recipient,
144            amount: *amount
145        });
146    }
147
148    /// Transfers tokens from the owner to the recipient using the spender's allowance.
149    pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) {
150        let spender = self.env().caller();
151
152        if owner == recipient {
153            self.env().revert(Error::CannotTargetSelfUser);
154        }
155
156        if amount.is_zero() {
157            return;
158        }
159
160        let allowance = self.allowance(owner, &spender);
161
162        self.allowances.set(
163            owner,
164            recipient,
165            allowance
166                .checked_sub(*amount)
167                .unwrap_or_revert_with(self, Error::InsufficientAllowance)
168        );
169        self.raw_transfer(owner, recipient, amount);
170
171        self.env().emit_event(TransferFrom {
172            spender,
173            owner: *owner,
174            recipient: *recipient,
175            amount: *amount
176        });
177    }
178}
179
180impl Cep18 {
181    /// Transfers tokens from the sender to the recipient without checking the permissions.
182    pub fn raw_transfer(&mut self, sender: &Address, recipient: &Address, amount: &U256) {
183        if amount > &self.balance_of(sender) {
184            self.env().revert(Error::InsufficientBalance)
185        }
186
187        if amount > &U256::zero() {
188            self.balances.subtract(sender, *amount);
189            self.balances.add(recipient, *amount);
190        }
191    }
192
193    /// Mints new tokens and assigns them to the given address without checking the permissions.
194    pub fn raw_mint(&mut self, owner: &Address, amount: &U256) {
195        self.total_supply.add(*amount);
196        self.balances.add(owner, *amount);
197
198        self.env().emit_event(Mint {
199            recipient: *owner,
200            amount: *amount
201        });
202    }
203
204    /// Approves the spender to spend the given amount of tokens on behalf of the owner without checking the permissions.
205    pub fn raw_approve(&mut self, owner: &Address, spender: &Address, amount: &U256) {
206        self.allowances.set(owner, spender, *amount);
207        self.env().emit_event(SetAllowance {
208            owner: *owner,
209            spender: *spender,
210            allowance: *amount
211        });
212    }
213
214    /// Burns the given amount of tokens from the given address without checking the permissions.
215    pub fn raw_burn(&mut self, owner: &Address, amount: &U256) {
216        if &self.balance_of(owner) < amount {
217            self.env().revert(Error::InsufficientBalance);
218        }
219
220        self.total_supply.subtract(*amount);
221        self.balances.subtract(owner, *amount);
222
223        self.env().emit_event(Burn {
224            owner: *owner,
225            amount: *amount
226        });
227    }
228
229    /// Set name of the token.
230    pub fn set_name(&mut self, name: String) {
231        self.name.set(name);
232    }
233
234    /// Set symbol of the token.
235    pub fn set_symbol(&mut self, symbol: String) {
236        self.symbol.set(symbol);
237    }
238
239    /// Set decimals of the token.
240    pub fn set_decimals(&mut self, decimals: u8) {
241        self.decimals.set(decimals);
242    }
243}
244
245pub(crate) mod utils {
246    #![allow(missing_docs)]
247    #![allow(dead_code)]
248
249    use crate::access::Ownable;
250
251    use super::*;
252
253    #[odra::odra_error]
254    pub enum Error {
255        CantMint = 99,
256        CantBurn = 100
257    }
258
259    #[odra::module]
260    pub struct Cep18Example {
261        token: SubModule<Cep18>,
262        ownable: SubModule<Ownable>
263    }
264
265    #[odra::module]
266    impl Cep18Example {
267        delegate! {
268            to self.token {
269                fn name(&self) -> String;
270                fn symbol(&self) -> String;
271                fn decimals(&self) -> u8;
272                fn total_supply(&self) -> U256;
273                fn balance_of(&self, address: &Address) -> U256;
274                fn allowance(&self, owner: &Address, spender: &Address) -> U256;
275                fn approve(&mut self, spender: &Address, amount: &U256);
276                fn decrease_allowance(&mut self, spender: &Address, decr_by: &U256);
277                fn increase_allowance(&mut self, spender: &Address, inc_by: &U256);
278                fn transfer(&mut self, recipient: &Address, amount: &U256);
279                fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256);
280            }
281        }
282
283        pub fn init(&mut self, symbol: String, name: String, decimals: u8, initial_supply: U256) {
284            let caller = self.env().caller();
285            self.ownable.init(caller);
286            self.token.init(symbol, name, decimals, initial_supply);
287        }
288
289        pub fn mint(&mut self, owner: &Address, amount: &U256) {
290            if self.env().caller() != self.ownable.get_owner() {
291                self.env().revert(Error::CantMint);
292            }
293            self.token.raw_mint(owner, amount);
294        }
295
296        pub fn burn(&mut self, owner: &Address, amount: &U256) {
297            if self.env().caller() != *owner {
298                self.env().revert(Error::CantBurn);
299            }
300            self.token.raw_burn(owner, amount);
301        }
302    }
303}
304
305#[cfg(test)]
306pub(crate) mod tests {
307    use alloc::string::ToString;
308
309    use odra::casper_types::account::AccountHash;
310    use odra::casper_types::contracts::ContractPackageHash;
311    use odra::host::{Deployer, HostEnv, HostRef};
312    use odra::prelude::*;
313
314    use super::utils::{Cep18Example, Cep18ExampleHostRef, Cep18ExampleInitArgs};
315
316    pub const TOKEN_NAME: &str = "Plascoin";
317    pub const TOKEN_SYMBOL: &str = "PLS";
318    pub const TOKEN_DECIMALS: u8 = 100;
319    pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000;
320    pub const TOKEN_OWNER_AMOUNT_1: u64 = 1_000_000;
321    pub const TOKEN_OWNER_AMOUNT_2: u64 = 2_000_000;
322    pub const TRANSFER_AMOUNT_1: u64 = 200_001;
323    pub const ALLOWANCE_AMOUNT_1: u64 = 456_789;
324    pub const ALLOWANCE_AMOUNT_2: u64 = 87_654;
325
326    pub fn setup() -> Cep18ExampleHostRef {
327        let env = odra_test::env();
328        let init_args = Cep18ExampleInitArgs {
329            symbol: TOKEN_SYMBOL.to_string(),
330            name: TOKEN_NAME.to_string(),
331            decimals: TOKEN_DECIMALS,
332            initial_supply: TOKEN_TOTAL_SUPPLY.into()
333        };
334        setup_with_args(&env, init_args)
335    }
336
337    pub fn setup_with_args(env: &HostEnv, args: Cep18ExampleInitArgs) -> Cep18ExampleHostRef {
338        Cep18Example::deploy(env, args)
339    }
340
341    pub fn invert_address(address: Address) -> Address {
342        match address {
343            Address::Account(hash) => Address::Contract(ContractPackageHash::new(hash.value())),
344            Address::Contract(hash) => Address::Account(AccountHash(hash.value()))
345        }
346    }
347
348    #[test]
349    fn should_have_queryable_properties() {
350        let cep18_token = setup();
351
352        assert_eq!(cep18_token.name(), TOKEN_NAME);
353        assert_eq!(cep18_token.symbol(), TOKEN_SYMBOL);
354        assert_eq!(cep18_token.decimals(), TOKEN_DECIMALS);
355        assert_eq!(cep18_token.total_supply(), TOKEN_TOTAL_SUPPLY.into());
356
357        let owner_key = cep18_token.env().caller();
358        let owner_balance = cep18_token.balance_of(&owner_key);
359        assert_eq!(owner_balance, TOKEN_TOTAL_SUPPLY.into());
360
361        let contract_balance = cep18_token.balance_of(&cep18_token.address());
362        assert_eq!(contract_balance, 0.into());
363
364        // Ensures that Account and Contract ownership is respected, and we're not keying ownership under
365        // the raw bytes regardless of variant.
366        let inverted_owner_key = invert_address(owner_key);
367        let inverted_owner_balance = cep18_token.balance_of(&inverted_owner_key);
368        assert_eq!(inverted_owner_balance, 0.into());
369    }
370}