odra_modules/
wrapped_native.rs

1//! Wrapped CSPR token implementation
2use crate::erc20::Erc20;
3use crate::wrapped_native::events::{Deposit, Withdrawal};
4use odra::casper_types::{U256, U512};
5use odra::uints::{ToU256, ToU512};
6use odra::{prelude::*, ContractRef};
7
8/// An event emitted when native tokens are deposited into the contract.
9#[odra::event]
10pub struct OnCsprDeposit {
11    /// Address of the account that deposited the tokens.
12    pub account: Address,
13    /// The amount of tokens deposited.
14    pub value: U512
15}
16
17/// The CsprDeposit contract.
18#[odra::module]
19pub struct CsprDeposit {}
20
21/// The CsprDeposit contract implementation.
22#[odra::module]
23impl CsprDeposit {
24    /// Deposits native tokens into the contract.
25    #[odra(payable)]
26    pub fn deposit(&self) {
27        self.env().emit_event(OnCsprDeposit {
28            account: self.env().caller(),
29            value: self.env().attached_value()
30        });
31    }
32}
33
34/// The WrappedNativeToken module.
35#[odra::module(events = [Deposit, Withdrawal])]
36pub struct WrappedNativeToken {
37    erc20: SubModule<Erc20>
38}
39
40/// The WrappedNativeToken module implementation.
41#[odra::module]
42impl WrappedNativeToken {
43    /// Initializes the contract with the metadata.
44    pub fn init(&mut self) {
45        let symbol = "WCSPR".to_string();
46        let name = "Wrapped CSPR".to_string();
47        self.erc20.init(symbol, name, 9, None);
48    }
49
50    /// Deposits native tokens into the contract.
51    #[odra(payable)]
52    pub fn deposit(&mut self) {
53        let caller = self.env().caller();
54
55        let amount = self.env().attached_value();
56
57        let amount = amount.to_u256().unwrap_or_revert(self);
58        self.erc20.mint(&caller, &amount);
59
60        self.env().emit_event(Deposit {
61            account: caller,
62            value: amount
63        });
64    }
65
66    /// Withdraws native tokens from the contract.
67    pub fn withdraw(&mut self, amount: &U256) {
68        let caller = self.env().caller();
69
70        self.erc20.burn(&caller, amount);
71        if caller.is_contract() {
72            CsprDepositContractRef::new(self.env(), caller)
73                .with_tokens(amount.to_u512())
74                .deposit();
75        } else {
76            self.env().transfer_tokens(&caller, &amount.to_u512());
77        }
78
79        self.env().emit_event(Withdrawal {
80            account: caller,
81            value: *amount
82        });
83    }
84
85    /// Sets the allowance for `spender` to spend `amount` of the caller's tokens.
86    pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 {
87        self.erc20.allowance(owner, spender)
88    }
89
90    /// Returns the balance of `address`.
91    pub fn balance_of(&self, address: &Address) -> U256 {
92        self.erc20.balance_of(address)
93    }
94
95    /// Returns the total supply of the token.
96    pub fn total_supply(&self) -> U256 {
97        self.erc20.total_supply()
98    }
99
100    /// Returns the number of decimals used by the token.
101    pub fn decimals(&self) -> u8 {
102        self.erc20.decimals()
103    }
104
105    /// Returns the symbol of the token.
106    pub fn symbol(&self) -> String {
107        self.erc20.symbol()
108    }
109
110    /// Returns the name of the token.
111    pub fn name(&self) -> String {
112        self.erc20.name()
113    }
114
115    /// Approves `spender` to spend `amount` of the caller's tokens.
116    pub fn approve(&mut self, spender: &Address, amount: &U256) {
117        self.erc20.approve(spender, amount)
118    }
119
120    /// Transfers `amount` of the owners tokens to `recipient` using allowance.
121    pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) {
122        self.erc20.transfer_from(owner, recipient, amount)
123    }
124
125    /// Transfers `amount` of the caller's tokens to `recipient`.
126    pub fn transfer(&mut self, recipient: &Address, amount: &U256) {
127        self.erc20.transfer(recipient, amount)
128    }
129}
130
131/// Events emitted by the WrappedNativeToken module.
132pub mod events {
133    use odra::casper_event_standard;
134    use odra::casper_types::U256;
135    use odra::prelude::*;
136
137    /// Event emitted when native tokens are deposited into the contract.
138    #[odra::event]
139    pub struct Deposit {
140        /// An Address of the account that deposited the tokens.
141        pub account: Address,
142        /// The amount of tokens deposited.
143        pub value: U256
144    }
145
146    /// Event emitted when native tokens are withdrawn from the contract.
147    #[odra::event]
148    pub struct Withdrawal {
149        /// An Address of the account that withdrew the tokens.
150        pub account: Address,
151        /// The amount of tokens withdrawn.
152        pub value: U256
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::erc20::errors::Error::InsufficientBalance;
159    use crate::erc20::events::Transfer;
160    use crate::wrapped_native::events::{Deposit, Withdrawal};
161    use crate::wrapped_native::WrappedNativeTokenHostRef;
162    use odra::casper_event_standard::EventInstance;
163    use odra::casper_types::{U256, U512};
164    use odra::host::{Deployer, HostEnv, HostRef, NoArgs};
165    use odra::prelude::*;
166    use odra::uints::{ToU256, ToU512};
167    use odra::VmError::BalanceExceeded;
168
169    use super::WrappedNativeToken;
170
171    fn setup() -> (
172        HostEnv,
173        WrappedNativeTokenHostRef,
174        Address,
175        U512,
176        Address,
177        U512
178    ) {
179        let env = odra_test::env();
180        let token = WrappedNativeToken::deploy(&env, NoArgs);
181        let account_1 = env.get_account(0);
182        let account_1_balance = env.balance_of(&account_1);
183        let account_2 = env.get_account(1);
184        let account_2_balance = env.balance_of(&account_2);
185
186        (
187            env,
188            token,
189            account_1,
190            account_1_balance,
191            account_2,
192            account_2_balance
193        )
194    }
195
196    #[test]
197    fn test_init() {
198        // When deploy a contract.
199        let (_, token, _, _, _, _) = setup();
200
201        // Then the contract has correct metadata.
202        assert_eq!(token.total_supply(), U256::zero());
203        assert_eq!(token.name(), "Wrapped CSPR".to_string());
204        assert_eq!(token.symbol(), "WCSPR".to_string());
205        assert_eq!(token.decimals(), 9);
206    }
207
208    #[test]
209    fn test_deposit() {
210        // Given a fresh contract.
211        let (env, token, account, account_balance, _, _) = setup();
212
213        // When deposit tokens.
214        let deposit_amount = 1_000u32;
215        token.with_tokens(deposit_amount.into()).deposit();
216
217        // Then native tokens are correctly deducted.
218        assert_eq!(account_balance - deposit_amount, env.balance_of(&account));
219
220        // Then the contract balance is updated.
221        assert_eq!(token.balance_of(&account), deposit_amount.into());
222
223        // The events were emitted.
224        assert!(env.emitted_event(
225            token.address(),
226            &Transfer {
227                from: None,
228                to: Some(account),
229                amount: deposit_amount.into()
230            }
231        ));
232
233        assert!(env.emitted_event(
234            token.address(),
235            &Deposit {
236                account,
237                value: deposit_amount.into()
238            }
239        ));
240    }
241
242    #[test]
243    fn test_minting() {
244        // Given a fresh contract.
245        let (env, token, account_1, _, account_2, _) = setup();
246
247        // When two users deposit some tokens.
248        let deposit_amount = 1_000.into();
249
250        env.set_caller(account_1);
251        token.with_tokens(deposit_amount).deposit();
252
253        env.set_caller(account_2);
254        token.with_tokens(deposit_amount).deposit();
255
256        // Then the total supply in the sum of deposits.
257        assert_eq!(
258            token.total_supply(),
259            (deposit_amount + deposit_amount).to_u256().unwrap()
260        );
261        // Then events were emitted.
262        assert!(env.event_names(token.address()).ends_with(
263            vec![
264                Transfer::name(),
265                Deposit::name(),
266                Transfer::name(),
267                Deposit::name()
268            ]
269            .as_slice()
270        ));
271    }
272
273    #[test]
274    fn test_deposit_amount_exceeding_account_balance() {
275        // Given a new contract.
276        let (_, token, _, balance, _, _) = setup();
277        // When the deposit amount exceeds the user's balance
278        // Then an error occurs.
279        assert_eq!(
280            token.with_tokens(balance + U512::one()).try_deposit(),
281            Err(OdraError::VmError(BalanceExceeded))
282        );
283    }
284
285    #[test]
286    fn test_withdrawal() {
287        // Deposit some tokens in the contract.
288        let (env, mut token, account, _, _, _) = setup();
289        let deposit_amount: U512 = 3_000.into();
290        token.with_tokens(deposit_amount).deposit();
291        let account_balance = env.balance_of(&account);
292
293        // When withdraw some tokens.
294        let withdrawal_amount: U256 = 1_000.into();
295        token.withdraw(&withdrawal_amount);
296
297        // Then the user has the withdrawn tokens back.
298        assert_eq!(
299            account_balance + withdrawal_amount.to_u512(),
300            env.balance_of(&account)
301        );
302        // Then the balance in the contract is deducted.
303        assert_eq!(
304            token.balance_of(&account),
305            deposit_amount.to_u256().unwrap() - withdrawal_amount
306        );
307
308        // Then events were emitted.
309        assert!(env.emitted_event(
310            token.address(),
311            &Transfer {
312                from: Some(account),
313                to: None,
314                amount: withdrawal_amount
315            }
316        ));
317        assert!(env.emitted_event(
318            token.address(),
319            &Withdrawal {
320                account,
321                value: withdrawal_amount
322            }
323        ));
324    }
325
326    #[test]
327    fn test_withdrawal_amount_exceeding_account_deposit() {
328        // Given a new contract.
329        let (_, mut token, _, _, _, _) = setup();
330        // When the user withdraws amount exceeds the user's deposit
331        // Then an error occurs.
332        assert_eq!(
333            token.try_withdraw(&U256::one()),
334            Err(InsufficientBalance.into())
335        );
336    }
337}