casper_contract_sdk/contrib/
cep18.rs

1//! CEP-18 token standard.
2//!
3//! This module implements the CEP-18 token standard, which is a fungible token standard
4//! for the Casper blockchain. It provides a set of functions and traits for creating, transferring,
5//! and managing fungible tokens.
6//!
7//! The CEP-18 standard is designed to be simple and efficient, allowing developers to easily
8//! create and manage fungible tokens on the Casper blockchain. It includes support for
9//! minting, burning, and transferring tokens, as well as managing allowances and balances.
10//!
11//! The standard also includes support for events, allowing developers to emit events
12//! when tokens are transferred, minted, or burned. This allows for easy tracking
13//! and monitoring of token activity on the blockchain.
14//!
15//! It only requires implementation of `CEP18` trait for your contract to receive already
16//! implemented entry points.
17//!
18//! # Example CEP18 token contract
19//!
20//! ```rust
21//! use casper_contract_sdk::prelude::*;
22//! use casper_contract_sdk::contrib::cep18::{CEP18, CEP18State, CEP18Ext, Mintable, Burnable};
23//! # use casper_contract_sdk::collections::Map;
24//! # use casper_contract_sdk::macros::casper;
25//! # use casper_contract_sdk::types::U256;
26//!
27//! #[casper(contract_state)]
28//! struct MyToken {
29//!    state: CEP18State,
30//! }
31//!
32//! impl Default for MyToken {
33//!   fn default() -> Self {
34//!     Self {
35//!       state: CEP18State::new("MyToken", "MTK", 18, U256::from(10_000_000_000u64)),
36//!     }
37//!   }
38//! }
39//!
40//! #[casper]
41//! impl MyToken {
42//!   #[casper(constructor)]
43//!   pub fn new() -> Self {
44//!     let my_token = Self::default();
45//!     // Perform extra initialization if needed i.e. mint tokens, set genesis balance holders etc.
46//!     my_token
47//!   }
48//! }
49//!
50//! #[casper(path = casper_contract_sdk::contrib::cep18)]
51//! impl CEP18 for MyToken {
52//!   fn state(&self) -> &CEP18State {
53//!     &self.state
54//!   }
55//!
56//!   fn state_mut(&mut self) -> &mut CEP18State {
57//!     &mut self.state
58//!   }
59//! }
60//! ```
61use bnum::types::U256;
62use borsh::{BorshDeserialize, BorshSerialize};
63use casper_contract_macros::CasperABI;
64
65use super::access_control::{AccessControl, AccessControlError, Role};
66#[allow(unused_imports)]
67use crate as casper_contract_sdk;
68use crate::{collections::Map, macros::blake2b256, prelude::*};
69
70/// While the code consuming this contract needs to define further error variants, it can
71/// return those via the `Error::User` variant or equivalently via the `ApiError::User`
72/// variant.
73#[derive(Debug, PartialEq, Eq, CasperABI, BorshSerialize, BorshDeserialize)]
74#[casper]
75pub enum Cep18Error {
76    /// CEP-18 contract called from within an invalid context.
77    InvalidContext,
78    /// Spender does not have enough balance.
79    InsufficientBalance,
80    /// Spender does not have enough allowance approved.
81    InsufficientAllowance,
82    /// Operation would cause an integer overflow.
83    Overflow,
84    /// A required package hash was not specified.
85    PackageHashMissing,
86    /// The package hash specified does not represent a package.
87    PackageHashNotPackage,
88    /// An invalid event mode was specified.
89    InvalidEventsMode,
90    /// The event mode required was not specified.
91    MissingEventsMode,
92    /// An unknown error occurred.
93    Phantom,
94    /// Failed to read the runtime arguments provided.
95    FailedToGetArgBytes,
96    /// The caller does not have sufficient security access.
97    InsufficientRights,
98    /// The list of Admin accounts provided is invalid.
99    InvalidAdminList,
100    /// The list of accounts that can mint tokens is invalid.
101    InvalidMinterList,
102    /// The list of accounts with no access rights is invalid.
103    InvalidNoneList,
104    /// The flag to enable the mint and burn mode is invalid.
105    InvalidEnableMBFlag,
106    /// This contract instance cannot be initialized again.
107    AlreadyInitialized,
108    ///  The mint and burn mode is disabled.
109    MintBurnDisabled,
110    CannotTargetSelfUser,
111    InvalidBurnTarget,
112}
113
114impl From<AccessControlError> for Cep18Error {
115    fn from(error: AccessControlError) -> Self {
116        match error {
117            AccessControlError::NotAuthorized => Cep18Error::InsufficientRights,
118        }
119    }
120}
121
122#[casper(message, path = crate)]
123pub struct Transfer {
124    pub from: Option<Entity>,
125    pub to: Entity,
126    pub amount: U256,
127}
128
129#[casper(message, path = crate)]
130pub struct Approve {
131    pub owner: Entity,
132    pub spender: Entity,
133    pub amount: U256,
134}
135
136pub const ADMIN_ROLE: Role = blake2b256!("admin");
137pub const MINTER_ROLE: Role = blake2b256!("minter");
138
139#[casper(path = crate)]
140pub struct CEP18State {
141    pub name: String,
142    pub symbol: String,
143    pub decimals: u8,
144    pub total_supply: U256,
145    pub balances: Map<Entity, U256>,
146    pub allowances: Map<(Entity, Entity), U256>,
147    pub enable_mint_burn: bool,
148}
149
150impl CEP18State {
151    fn transfer_balance(
152        &mut self,
153        sender: &Entity,
154        recipient: &Entity,
155        amount: U256,
156    ) -> Result<(), Cep18Error> {
157        if amount.is_zero() {
158            return Ok(());
159        }
160
161        let sender_balance = self.balances.get(sender).unwrap_or_default();
162
163        let new_sender_balance = sender_balance
164            .checked_sub(amount)
165            .ok_or(Cep18Error::InsufficientBalance)?;
166
167        let recipient_balance = self.balances.get(recipient).unwrap_or_default();
168
169        let new_recipient_balance = recipient_balance
170            .checked_add(amount)
171            .ok_or(Cep18Error::Overflow)?;
172
173        self.balances.insert(sender, &new_sender_balance);
174        self.balances.insert(recipient, &new_recipient_balance);
175        Ok(())
176    }
177}
178
179impl CEP18State {
180    pub fn new(name: &str, symbol: &str, decimals: u8, total_supply: U256) -> CEP18State {
181        CEP18State {
182            name: name.to_string(),
183            symbol: symbol.to_string(),
184            decimals,
185            total_supply,
186            balances: Map::new("balances"),
187            allowances: Map::new("allowances"),
188            enable_mint_burn: false,
189        }
190    }
191}
192
193#[casper(path = crate, export = true)]
194pub trait CEP18 {
195    #[casper(private)]
196    fn state(&self) -> &CEP18State;
197
198    #[casper(private)]
199    fn state_mut(&mut self) -> &mut CEP18State;
200
201    fn name(&self) -> &str {
202        &self.state().name
203    }
204
205    fn symbol(&self) -> &str {
206        &self.state().symbol
207    }
208
209    fn decimals(&self) -> u8 {
210        self.state().decimals
211    }
212
213    fn total_supply(&self) -> U256 {
214        self.state().total_supply
215    }
216
217    fn balance_of(&self, address: Entity) -> U256 {
218        self.state().balances.get(&address).unwrap_or_default()
219    }
220
221    fn allowance(&self, spender: Entity, owner: Entity) {
222        self.state()
223            .allowances
224            .get(&(spender, owner))
225            .unwrap_or_default();
226    }
227
228    #[casper(revert_on_error)]
229    fn approve(&mut self, spender: Entity, amount: U256) -> Result<(), Cep18Error> {
230        let owner = casper::get_caller();
231        if owner == spender {
232            return Err(Cep18Error::CannotTargetSelfUser);
233        }
234        let lookup_key = (owner, spender);
235        self.state_mut().allowances.insert(&lookup_key, &amount);
236        casper::emit(Approve {
237            owner,
238            spender,
239            amount,
240        })
241        .expect("failed to emit message");
242        Ok(())
243    }
244
245    #[casper(revert_on_error)]
246    fn decrease_allowance(&mut self, spender: Entity, amount: U256) -> Result<(), Cep18Error> {
247        let owner = casper::get_caller();
248        if owner == spender {
249            return Err(Cep18Error::CannotTargetSelfUser);
250        }
251        let lookup_key = (owner, spender);
252        let allowance = self.state().allowances.get(&lookup_key).unwrap_or_default();
253        let allowance = allowance.saturating_sub(amount);
254        self.state_mut().allowances.insert(&lookup_key, &allowance);
255        Ok(())
256    }
257
258    #[casper(revert_on_error)]
259    fn increase_allowance(&mut self, spender: Entity, amount: U256) -> Result<(), Cep18Error> {
260        let owner = casper::get_caller();
261        if owner == spender {
262            return Err(Cep18Error::CannotTargetSelfUser);
263        }
264        let lookup_key = (owner, spender);
265        let allowance = self.state().allowances.get(&lookup_key).unwrap_or_default();
266        let allowance = allowance.saturating_add(amount);
267        self.state_mut().allowances.insert(&lookup_key, &allowance);
268        Ok(())
269    }
270
271    #[casper(revert_on_error)]
272    fn transfer(&mut self, recipient: Entity, amount: U256) -> Result<(), Cep18Error> {
273        let sender = casper::get_caller();
274        if sender == recipient {
275            return Err(Cep18Error::CannotTargetSelfUser);
276        }
277        self.state_mut()
278            .transfer_balance(&sender, &recipient, amount)?;
279
280        // NOTE: This is operation is fallible, although it's not expected to fail under any
281        // circumstances (number of topics per contract, payload size, topic size, number of
282        // messages etc. are all under control).
283        casper::emit(Transfer {
284            from: Some(sender),
285            to: recipient,
286            amount,
287        })
288        .expect("failed to emit message");
289
290        Ok(())
291    }
292
293    #[casper(revert_on_error)]
294    fn transfer_from(
295        &mut self,
296        owner: Entity,
297        recipient: Entity,
298        amount: U256,
299    ) -> Result<(), Cep18Error> {
300        let spender = casper::get_caller();
301        if owner == recipient {
302            return Err(Cep18Error::CannotTargetSelfUser);
303        }
304
305        if amount.is_zero() {
306            return Ok(());
307        }
308
309        let spender_allowance = self
310            .state()
311            .allowances
312            .get(&(owner, spender))
313            .unwrap_or_default();
314        let new_spender_allowance = spender_allowance
315            .checked_sub(amount)
316            .ok_or(Cep18Error::InsufficientAllowance)?;
317
318        self.state_mut()
319            .transfer_balance(&owner, &recipient, amount)?;
320
321        self.state_mut()
322            .allowances
323            .insert(&(owner, spender), &new_spender_allowance);
324
325        casper::emit(Transfer {
326            from: Some(owner),
327            to: recipient,
328            amount,
329        })
330        .expect("failed to emit message");
331
332        Ok(())
333    }
334}
335
336#[casper(path = crate, export = true)]
337pub trait Mintable: CEP18 + AccessControl {
338    #[casper(revert_on_error)]
339    fn mint(&mut self, owner: Entity, amount: U256) -> Result<(), Cep18Error> {
340        if !CEP18::state(self).enable_mint_burn {
341            return Err(Cep18Error::MintBurnDisabled);
342        }
343
344        AccessControl::require_any_role(self, &[ADMIN_ROLE, MINTER_ROLE])?;
345
346        let balance = CEP18::state(self).balances.get(&owner).unwrap_or_default();
347        let new_balance = balance.checked_add(amount).ok_or(Cep18Error::Overflow)?;
348        CEP18::state_mut(self).balances.insert(&owner, &new_balance);
349        CEP18::state_mut(self).total_supply = CEP18::state(self)
350            .total_supply
351            .checked_add(amount)
352            .ok_or(Cep18Error::Overflow)?;
353
354        casper::emit(Transfer {
355            from: None,
356            to: owner,
357            amount,
358        })
359        .expect("failed to emit message");
360
361        Ok(())
362    }
363}
364
365#[casper(path = crate, export = true)]
366pub trait Burnable: CEP18 {
367    #[casper(revert_on_error)]
368    fn burn(&mut self, owner: Entity, amount: U256) -> Result<(), Cep18Error> {
369        if !self.state().enable_mint_burn {
370            return Err(Cep18Error::MintBurnDisabled);
371        }
372
373        if owner != casper::get_caller() {
374            return Err(Cep18Error::InvalidBurnTarget);
375        }
376
377        let balance = self.state().balances.get(&owner).unwrap_or_default();
378        let new_balance = balance.checked_add(amount).ok_or(Cep18Error::Overflow)?;
379        self.state_mut().balances.insert(&owner, &new_balance);
380        self.state_mut().total_supply = self
381            .state()
382            .total_supply
383            .checked_sub(amount)
384            .ok_or(Cep18Error::Overflow)?;
385        Ok(())
386    }
387}