From 3df4d8ab8bb93b5433285002b2786f9249a293bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Gonz=C3=A1lez?= Date: Mon, 1 Apr 2024 12:39:25 +0200 Subject: [PATCH] feat(erc20): implement Metadata extension (#20) --- contracts/Cargo.toml | 1 + contracts/src/erc20/extensions/metadata.rs | 160 +++++++++++++++++++++ contracts/src/erc20/extensions/mod.rs | 2 + contracts/src/erc20/mod.rs | 8 +- 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 contracts/src/erc20/extensions/metadata.rs create mode 100644 contracts/src/erc20/extensions/mod.rs diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 3dc7753b..92851e04 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -22,6 +22,7 @@ wavm-shims = { path = "../lib/wavm-shims" } [features] default = [] erc20 = [] +erc20_metadata = ["erc20"] erc721 = [] [lib] diff --git a/contracts/src/erc20/extensions/metadata.rs b/contracts/src/erc20/extensions/metadata.rs new file mode 100644 index 00000000..26ec2571 --- /dev/null +++ b/contracts/src/erc20/extensions/metadata.rs @@ -0,0 +1,160 @@ +//! Optional metadata of the ERC-20 standard. +use alloc::string::String; + +use stylus_proc::{external, sol_storage}; + +/// Number of decimals used by default on implementors of [`Metadata`]. +pub const DEFAULT_DECIMALS: u8 = 18; + +sol_storage! { + /// Optional metadata of the ERC-20 standard. + pub struct Metadata { + /// Token name. + string _name; + /// Token symbol. + string _symbol; + /// Initialization marker. If true this means that the constructor was + /// called. + /// + /// This field should be unnecessary once constructors are supported in + /// the SDK. + bool _initialized + } +} + +#[external] +impl Metadata { + /// Initializes a [`Metadata`] instance with the passed `name` and + /// `symbol`. It also sets `decimals` to [`DEFAULT_DECIMALS`]. + /// + /// Note that there are no setters for these fields. This makes them + /// immutable: they can only be set once at construction. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `name` - The name of the token. + /// * `symbol` - The symbol of the token. + pub fn constructor(&mut self, name: String, symbol: String) { + if self._initialized.get() == true { + return; + } + + self._name.set_str(name); + self._symbol.set_str(symbol); + self._initialized.set(true); + } + + /// Returns the name of the token. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + pub fn name(&self) -> String { + self._name.get_string() + } + + /// Returns the symbol of the token, usually a shorter version of the name. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + pub fn symbol(&self) -> String { + self._symbol.get_string() + } + + /// Returns the number of decimals used to get a user-friendly + /// representation of values of this token. + /// + /// For example, if `decimals` equals `2`, a balance of `505` tokens should + /// be displayed to a user as `5.05` (`505 / 10 ** 2`). + /// + /// Tokens usually opt for a value of `18`, imitating the relationship + /// between Ether and Wei. This is the default value returned by this + /// function ([`DEFAULT_DECIMALS`]), unless it's overridden. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// + /// NOTE: This information is only used for *display* purposes: in + /// no way it affects any of the arithmetic of the contract, including + /// [`ERC20::balance_of`] and [`ERC20::transfer`]. + pub fn decimals(&self) -> u8 { + // TODO: Use `U8` an avoid the conversion once https://github.com/OffchainLabs/stylus-sdk-rs/issues/117 + // gets resolved. + DEFAULT_DECIMALS + } +} + +#[cfg(test)] +mod tests { + use alloy_primitives::U256; + use stylus_sdk::storage::{StorageBool, StorageString, StorageType}; + + use super::{Metadata, DEFAULT_DECIMALS}; + #[allow(unused_imports)] + use crate::test_utils; + + impl Default for Metadata { + fn default() -> Self { + let root = U256::ZERO; + Metadata { + _name: unsafe { StorageString::new(root, 0) }, + _symbol: unsafe { + StorageString::new(root + U256::from(32), 0) + }, + _initialized: unsafe { + StorageBool::new(root + U256::from(64), 0) + }, + } + } + } + + #[test] + fn constructs() { + test_utils::with_storage::(|meta| { + let name = meta.name(); + let symbol = meta.symbol(); + let decimals = meta.decimals(); + let initialized = meta._initialized.get(); + assert_eq!(name, ""); + assert_eq!(symbol, ""); + assert_eq!(decimals, DEFAULT_DECIMALS); + assert_eq!(initialized, false); + + const NAME: &str = "Meta"; + const SYMBOL: &str = "Symbol"; + meta.constructor(NAME.to_owned(), SYMBOL.to_owned()); + + let name = meta.name(); + let symbol = meta.symbol(); + let decimals = meta.decimals(); + let initialized = meta._initialized.get(); + assert_eq!(name, NAME); + assert_eq!(symbol, SYMBOL); + assert_eq!(decimals, DEFAULT_DECIMALS); + assert_eq!(initialized, true); + }) + } + + #[test] + fn constructs_only_once() { + test_utils::with_storage::(|meta| { + const NAME: &str = "Meta"; + const SYMBOL: &str = "Symbol"; + meta.constructor(NAME.to_owned(), SYMBOL.to_owned()); + + meta.constructor("Invalid".to_owned(), "Invalid".to_owned()); + + let name = meta.name(); + let symbol = meta.symbol(); + let decimals = meta.decimals(); + let initialized = meta._initialized.get(); + assert_eq!(name, NAME); + assert_eq!(symbol, SYMBOL); + assert_eq!(decimals, DEFAULT_DECIMALS); + assert_eq!(initialized, true); + }) + } +} diff --git a/contracts/src/erc20/extensions/mod.rs b/contracts/src/erc20/extensions/mod.rs new file mode 100644 index 00000000..4638989a --- /dev/null +++ b/contracts/src/erc20/extensions/mod.rs @@ -0,0 +1,2 @@ +#[cfg(any(test, erc20_metadata))] +pub mod metadata; diff --git a/contracts/src/erc20/mod.rs b/contracts/src/erc20/mod.rs index 3508d09b..f8784d24 100644 --- a/contracts/src/erc20/mod.rs +++ b/contracts/src/erc20/mod.rs @@ -12,6 +12,8 @@ use stylus_sdk::{ stylus_proc::{external, sol_storage}, }; +pub mod extensions; + sol! { /// Emitted when `value` tokens are moved from one account (`from`) to /// another (`to`). @@ -356,7 +358,7 @@ mod tests { impl Default for ERC20 { fn default() -> Self { let root = U256::ZERO; - let token = ERC20 { + ERC20 { _balances: unsafe { StorageMap::new(root, 0) }, _allowances: unsafe { StorageMap::new(root + U256::from(32), 0) @@ -364,9 +366,7 @@ mod tests { _total_supply: unsafe { StorageU256::new(root + U256::from(64), 0) }, - }; - - token + } } }