odra_modules/
wrapped_native.rs

1//! Wrapped CSPR token implementation
2use crate::cep18_token::Cep18;
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    token: SubModule<Cep18>
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.token.init(symbol, name, 9, U256::zero());
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.token.raw_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.token.raw_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    /// Withdraws native tokens from the contract to a specific recipient.
86    /// This allows the caller to burn their wrapped tokens and send the native tokens
87    /// directly to a different address, avoiding the need for intermediate transfers.
88    pub fn withdraw_to(&mut self, recipient: &Address, amount: &U256) {
89        let caller = self.env().caller();
90
91        // Burn tokens from caller
92        self.token.raw_burn(&caller, amount);
93
94        // Send CSPR directly to recipient
95        if recipient.is_contract() {
96            CsprDepositContractRef::new(self.env(), *recipient)
97                .with_tokens(amount.to_u512())
98                .deposit();
99        } else {
100            self.env().transfer_tokens(recipient, &amount.to_u512());
101        }
102
103        self.env().emit_event(Withdrawal {
104            account: caller,
105            value: *amount
106        });
107    }
108
109    /// Sets the allowance for `spender` to spend `amount` of the caller's tokens.
110    pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 {
111        self.token.allowance(owner, spender)
112    }
113
114    /// Returns the balance of `address`.
115    pub fn balance_of(&self, address: &Address) -> U256 {
116        self.token.balance_of(address)
117    }
118
119    /// Returns the total supply of the token.
120    pub fn total_supply(&self) -> U256 {
121        self.token.total_supply()
122    }
123
124    /// Returns the number of decimals used by the token.
125    pub fn decimals(&self) -> u8 {
126        self.token.decimals()
127    }
128
129    /// Returns the symbol of the token.
130    pub fn symbol(&self) -> String {
131        self.token.symbol()
132    }
133
134    /// Returns the name of the token.
135    pub fn name(&self) -> String {
136        self.token.name()
137    }
138
139    /// Approves `spender` to spend `amount` of the caller's tokens.
140    pub fn approve(&mut self, spender: &Address, amount: &U256) {
141        self.token.approve(spender, amount)
142    }
143
144    /// Transfers `amount` of the owners tokens to `recipient` using allowance.
145    pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) {
146        self.token.transfer_from(owner, recipient, amount)
147    }
148
149    /// Transfers `amount` of the caller's tokens to `recipient`.
150    pub fn transfer(&mut self, recipient: &Address, amount: &U256) {
151        self.token.transfer(recipient, amount)
152    }
153}
154
155/// Events emitted by the WrappedNativeToken module.
156pub mod events {
157    use odra::casper_event_standard;
158    use odra::casper_types::U256;
159    use odra::prelude::*;
160
161    /// Event emitted when native tokens are deposited into the contract.
162    #[odra::event]
163    pub struct Deposit {
164        /// An Address of the account that deposited the tokens.
165        pub account: Address,
166        /// The amount of tokens deposited.
167        pub value: U256
168    }
169
170    /// Event emitted when native tokens are withdrawn from the contract.
171    #[odra::event]
172    pub struct Withdrawal {
173        /// An Address of the account that withdrew the tokens.
174        pub account: Address,
175        /// The amount of tokens withdrawn.
176        pub value: U256
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use crate::cep18::errors::Error::InsufficientBalance;
183    use crate::cep18::events::{Burn, Mint};
184    use crate::wrapped_native::events::{Deposit, Withdrawal};
185    use crate::wrapped_native::WrappedNativeTokenHostRef;
186    use odra::casper_event_standard::EventInstance;
187    use odra::casper_types::{U256, U512};
188    use odra::host::{Deployer, HostEnv, HostRef, NoArgs};
189    use odra::prelude::*;
190    use odra::uints::{ToU256, ToU512};
191    use odra::VmError::BalanceExceeded;
192
193    use super::WrappedNativeToken;
194
195    fn setup() -> (
196        HostEnv,
197        WrappedNativeTokenHostRef,
198        Address,
199        U512,
200        Address,
201        U512
202    ) {
203        let env = odra_test::env();
204        let token = WrappedNativeToken::deploy(&env, NoArgs);
205        let account_1 = env.get_account(0);
206        let account_1_balance = env.balance_of(&account_1);
207        let account_2 = env.get_account(1);
208        let account_2_balance = env.balance_of(&account_2);
209
210        (
211            env,
212            token,
213            account_1,
214            account_1_balance,
215            account_2,
216            account_2_balance
217        )
218    }
219
220    #[test]
221    fn test_init() {
222        // When deploy a contract.
223        let (_, token, _, _, _, _) = setup();
224
225        // Then the contract has correct metadata.
226        assert_eq!(token.total_supply(), U256::zero());
227        assert_eq!(token.name(), "Wrapped CSPR".to_string());
228        assert_eq!(token.symbol(), "WCSPR".to_string());
229        assert_eq!(token.decimals(), 9);
230    }
231
232    #[test]
233    fn test_deposit() {
234        // Given a fresh contract.
235        let (env, token, account, account_balance, _, _) = setup();
236
237        // When deposit tokens.
238        let deposit_amount = 1_000u32;
239        token.with_tokens(deposit_amount.into()).deposit();
240
241        // Then native tokens are correctly deducted.
242        assert_eq!(account_balance - deposit_amount, env.balance_of(&account));
243
244        // Then the contract balance is updated.
245        assert_eq!(token.balance_of(&account), deposit_amount.into());
246
247        // The events were emitted.
248        assert!(env.emitted_event(
249            &token,
250            Mint {
251                recipient: account,
252                amount: deposit_amount.into()
253            }
254        ));
255
256        assert!(env.emitted_event(
257            &token,
258            Deposit {
259                account,
260                value: deposit_amount.into()
261            }
262        ));
263    }
264
265    #[test]
266    fn test_minting() {
267        // Given a fresh contract.
268        let (env, token, account_1, _, account_2, _) = setup();
269
270        // When two users deposit some tokens.
271        let deposit_amount = 1_000.into();
272
273        env.set_caller(account_1);
274        token.with_tokens(deposit_amount).deposit();
275
276        env.set_caller(account_2);
277        token.with_tokens(deposit_amount).deposit();
278
279        // Then the total supply in the sum of deposits.
280        assert_eq!(
281            token.total_supply(),
282            (deposit_amount + deposit_amount)
283                .to_u256()
284                .expect("Valid U256")
285        );
286        // Then events were emitted.
287        assert!(env.event_names(&token).ends_with(
288            vec![Mint::name(), Deposit::name(), Mint::name(), Deposit::name()].as_slice()
289        ));
290    }
291
292    #[test]
293    fn test_deposit_amount_exceeding_account_balance() {
294        // Given a new contract.
295        let (_, token, _, balance, _, _) = setup();
296        // When the deposit amount exceeds the user's balance
297        // Then an error occurs.
298        assert_eq!(
299            token.with_tokens(balance + U512::one()).try_deposit(),
300            Err(OdraError::VmError(BalanceExceeded))
301        );
302    }
303
304    #[test]
305    fn test_withdrawal() {
306        // Deposit some tokens in the contract.
307        let (env, mut token, account, _, _, _) = setup();
308        let deposit_amount: U512 = 3_000.into();
309        token.with_tokens(deposit_amount).deposit();
310        let account_balance = env.balance_of(&account);
311
312        // When withdraw some tokens.
313        let withdrawal_amount: U256 = 1_000.into();
314        token.withdraw(&withdrawal_amount);
315
316        // Then the user has the withdrawn tokens back.
317        assert_eq!(
318            account_balance + withdrawal_amount.to_u512(),
319            env.balance_of(&account)
320        );
321        // Then the balance in the contract is deducted.
322        assert_eq!(
323            token.balance_of(&account),
324            deposit_amount.to_u256().expect("Valid U256") - withdrawal_amount
325        );
326
327        // Then events were emitted.
328        assert!(env.emitted_event(
329            &token,
330            Burn {
331                owner: account,
332                amount: withdrawal_amount
333            }
334        ));
335        assert!(env.emitted_event(
336            &token,
337            Withdrawal {
338                account,
339                value: withdrawal_amount
340            }
341        ));
342    }
343
344    #[test]
345    fn test_withdrawal_amount_exceeding_account_deposit() {
346        // Given a new contract.
347        let (_, mut token, _, _, _, _) = setup();
348        // When the user withdraws amount exceeds the user's deposit
349        // Then an error occurs.
350        assert_eq!(
351            token.try_withdraw(&U256::one()),
352            Err(InsufficientBalance.into())
353        );
354    }
355}