1use 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#[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 pub fn init(&mut self, symbol: String, name: String, decimals: u8, initial_supply: U256) {
35 let caller = self.env().caller();
36
37 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 self.balances.set(&caller, initial_supply);
48 self.env().emit_event(Mint {
49 recipient: caller,
50 amount: initial_supply
51 });
52 } else {
53 self.balances.init();
55 }
56
57 self.allowances.init();
59 }
60
61 pub fn name(&self) -> String {
63 self.name.get()
64 }
65
66 pub fn symbol(&self) -> String {
68 self.symbol.get()
69 }
70
71 pub fn decimals(&self) -> u8 {
73 self.decimals.get()
74 }
75
76 pub fn total_supply(&self) -> U256 {
78 self.total_supply.get()
79 }
80
81 pub fn balance_of(&self, address: &Address) -> U256 {
83 self.balances.get(address).unwrap_or_default()
84 }
85
86 pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 {
88 self.allowances.get_or_default(owner, spender)
89 }
90
91 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 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 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 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 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 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 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 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 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 pub fn set_name(&mut self, name: String) {
231 self.name.set(name);
232 }
233
234 pub fn set_symbol(&mut self, symbol: String) {
236 self.symbol.set(symbol);
237 }
238
239 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 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}