Skip to content

Commit

Permalink
feat(erc20): implement Metadata extension (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfertel committed Apr 1, 2024
1 parent a9fe526 commit 3df4d8a
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 4 deletions.
1 change: 1 addition & 0 deletions contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ wavm-shims = { path = "../lib/wavm-shims" }
[features]
default = []
erc20 = []
erc20_metadata = ["erc20"]
erc721 = []

[lib]
Expand Down
160 changes: 160 additions & 0 deletions contracts/src/erc20/extensions/metadata.rs
Original file line number Diff line number Diff line change
@@ -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::<Metadata>(|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::<Metadata>(|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);
})
}
}
2 changes: 2 additions & 0 deletions contracts/src/erc20/extensions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#[cfg(any(test, erc20_metadata))]
pub mod metadata;
8 changes: 4 additions & 4 deletions contracts/src/erc20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -356,17 +358,15 @@ 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)
},
_total_supply: unsafe {
StorageU256::new(root + U256::from(64), 0)
},
};

token
}
}
}

Expand Down

0 comments on commit 3df4d8a

Please sign in to comment.