1use 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#[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 #[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 self.symbol.set(symbol);
52 self.name.set(name);
53 self.decimals.set(decimals);
54 self.total_supply.set(initial_supply);
55
56 self.balances.set(&caller, initial_supply);
58 self.env().emit_event(Mint {
59 recipient: caller,
60 amount: initial_supply
61 });
62
63 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 if let Some(modality) = modality {
76 self.modality.set(modality);
77 }
78 }
79
80 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 let caller = self.env().caller();
96 self.assert_is_admin(&caller);
97
98 let mut badges_map = BTreeMap::new();
99
100 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 pub fn name(&self) -> String {
126 self.name.get()
127 }
128
129 pub fn symbol(&self) -> String {
131 self.symbol.get()
132 }
133
134 pub fn decimals(&self) -> u8 {
136 self.decimals.get()
137 }
138
139 pub fn total_supply(&self) -> U256 {
141 self.total_supply.get()
142 }
143
144 pub fn balance_of(&self, address: &Address) -> U256 {
146 self.balances.get(address).unwrap_or_default()
147 }
148
149 pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 {
151 self.allowances.get_or_default(owner, spender)
152 }
153
154 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[inline]
379 pub fn is_burn_and_mint_enabled(&self) -> bool {
380 self.modality.get_or_default().mint_and_burn_enabled()
381 }
382
383 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 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}