Arbitrum Stylus logo

Stylus by Example

ERC-20

Any contract that follows the ERC-20 standard is an ERC-20 token.

ERC-20 tokens provide functionalities to

  • transfer tokens
  • allow others to transfer tokens on behalf of the token holder

Here is the interface for ERC-20.

1interface IERC20 {
2    function totalSupply() external view returns (uint256);
3    function balanceOf(address account) external view returns (uint256);
4    function transfer(address recipient, uint256 amount)
5        external
6        returns (bool);
7    function allowance(address owner, address spender)
8        external
9        view
10        returns (uint256);
11    function approve(address spender, uint256 amount) external returns (bool);
12    function transferFrom(address sender, address recipient, uint256 amount)
13        external
14        returns (bool);
15}
1interface IERC20 {
2    function totalSupply() external view returns (uint256);
3    function balanceOf(address account) external view returns (uint256);
4    function transfer(address recipient, uint256 amount)
5        external
6        returns (bool);
7    function allowance(address owner, address spender)
8        external
9        view
10        returns (uint256);
11    function approve(address spender, uint256 amount) external returns (bool);
12    function transferFrom(address sender, address recipient, uint256 amount)
13        external
14        returns (bool);
15}

Example implementation of an ERC-20 token contract written in Rust.

src/erc20.rs

1//! Implementation of the ERC-20 standard
2//!
3//! The eponymous [`Erc20`] type provides all the standard methods,
4//! and is intended to be inherited by other contract types.
5//!
6//! You can configure the behavior of [`Erc20`] via the [`Erc20Params`] trait,
7//! which allows specifying the name, symbol, and decimals of the token.
8//!
9//! Note that this code is unaudited and not fit for production use.
10
11// Imported packages
12use alloc::string::String;
13use alloy_primitives::{Address, U256};
14use alloy_sol_types::sol;
15use core::marker::PhantomData;
16use stylus_sdk::{
17    evm,
18    msg,
19    prelude::*,
20};
21
22pub trait Erc20Params {
23    /// Immutable token name
24    const NAME: &'static str;
25
26    /// Immutable token symbol
27    const SYMBOL: &'static str;
28
29    /// Immutable token decimals
30    const DECIMALS: u8;
31}
32
33sol_storage! {
34    /// Erc20 implements all ERC-20 methods.
35    pub struct Erc20<T> {
36        /// Maps users to balances
37        mapping(address => uint256) balances;
38        /// Maps users to a mapping of each spender's allowance
39        mapping(address => mapping(address => uint256)) allowances;
40        /// The total supply of the token
41        uint256 total_supply;
42        /// Used to allow [`Erc20Params`]
43        PhantomData<T> phantom;
44    }
45}
46
47// Declare events and Solidity error types
48sol! {
49    event Transfer(address indexed from, address indexed to, uint256 value);
50    event Approval(address indexed owner, address indexed spender, uint256 value);
51
52    error InsufficientBalance(address from, uint256 have, uint256 want);
53    error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
54}
55
56/// Represents the ways methods may fail.
57#[derive(SolidityError)]
58pub enum Erc20Error {
59    InsufficientBalance(InsufficientBalance),
60    InsufficientAllowance(InsufficientAllowance),
61}
62
63// These methods aren't exposed to other contracts
64// Methods marked as "pub" here are usable outside of the erc20 module (i.e. they're callable from lib.rs)
65// Note: modifying storage will become much prettier soon
66impl<T: Erc20Params> Erc20<T> {
67    /// Movement of funds between 2 accounts
68    /// (invoked by the external transfer() and transfer_from() functions )
69    pub fn _transfer(
70        &mut self,
71        from: Address,
72        to: Address,
73        value: U256,
74    ) -> Result<(), Erc20Error> {
75        // Decreasing sender balance
76        let mut sender_balance = self.balances.setter(from);
77        let old_sender_balance = sender_balance.get();
78        if old_sender_balance < value {
79            return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
80                from,
81                have: old_sender_balance,
82                want: value,
83            }));
84        }
85        sender_balance.set(old_sender_balance - value);
86
87        // Increasing receiver balance
88        let mut to_balance = self.balances.setter(to);
89        let new_to_balance = to_balance.get() + value;
90        to_balance.set(new_to_balance);
91
92        // Emitting the transfer event
93        evm::log(Transfer { from, to, value });
94        Ok(())
95    }
96
97    /// Mints `value` tokens to `address`
98    pub fn mint(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
99        // Increasing balance
100        let mut balance = self.balances.setter(address);
101        let new_balance = balance.get() + value;
102        balance.set(new_balance);
103
104        // Increasing total supply
105        self.total_supply.set(self.total_supply.get() + value);
106
107        // Emitting the transfer event
108        evm::log(Transfer {
109            from: Address::ZERO,
110            to: address,
111            value,
112        });
113
114        Ok(())
115    }
116
117    /// Burns `value` tokens from `address`
118    pub fn burn(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
119        // Decreasing balance
120        let mut balance = self.balances.setter(address);
121        let old_balance = balance.get();
122        if old_balance < value {
123            return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
124                from: address,
125                have: old_balance,
126                want: value,
127            }));
128        }
129        balance.set(old_balance - value);
130
131        // Decreasing the total supply
132        self.total_supply.set(self.total_supply.get() - value);
133
134        // Emitting the transfer event
135        evm::log(Transfer {
136            from: address,
137            to: Address::ZERO,
138            value,
139        });
140
141        Ok(())
142    }
143}
144
145// These methods are external to other contracts
146// Note: modifying storage will become much prettier soon
147#[public]
148impl<T: Erc20Params> Erc20<T> {
149    /// Immutable token name
150    pub fn name() -> String {
151        T::NAME.into()
152    }
153
154    /// Immutable token symbol
155    pub fn symbol() -> String {
156        T::SYMBOL.into()
157    }
158
159    /// Immutable token decimals
160    pub fn decimals() -> u8 {
161        T::DECIMALS
162    }
163
164    /// Total supply of tokens
165    pub fn total_supply(&self) -> U256 {
166        self.total_supply.get()
167    }
168
169    /// Balance of `address`
170    pub fn balance_of(&self, owner: Address) -> U256 {
171        self.balances.get(owner)
172    }
173
174    /// Transfers `value` tokens from msg::sender() to `to`
175    pub fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Erc20Error> {
176        self._transfer(msg::sender(), to, value)?;
177        Ok(true)
178    }
179
180    /// Transfers `value` tokens from `from` to `to`
181    /// (msg::sender() must be able to spend at least `value` tokens from `from`)
182    pub fn transfer_from(
183        &mut self,
184        from: Address,
185        to: Address,
186        value: U256,
187    ) -> Result<bool, Erc20Error> {
188        // Check msg::sender() allowance
189        let mut sender_allowances = self.allowances.setter(from);
190        let mut allowance = sender_allowances.setter(msg::sender());
191        let old_allowance = allowance.get();
192        if old_allowance < value {
193            return Err(Erc20Error::InsufficientAllowance(InsufficientAllowance {
194                owner: from,
195                spender: msg::sender(),
196                have: old_allowance,
197                want: value,
198            }));
199        }
200
201        // Decreases allowance
202        allowance.set(old_allowance - value);
203
204        // Calls the internal transfer function
205        self._transfer(from, to, value)?;
206
207        Ok(true)
208    }
209
210    /// Approves the spenditure of `value` tokens of msg::sender() to `spender`
211    pub fn approve(&mut self, spender: Address, value: U256) -> bool {
212        self.allowances.setter(msg::sender()).insert(spender, value);
213        evm::log(Approval {
214            owner: msg::sender(),
215            spender,
216            value,
217        });
218        true
219    }
220
221    /// Returns the allowance of `spender` on `owner`'s tokens
222    pub fn allowance(&self, owner: Address, spender: Address) -> U256 {
223        self.allowances.getter(owner).get(spender)
224    }
225}
1//! Implementation of the ERC-20 standard
2//!
3//! The eponymous [`Erc20`] type provides all the standard methods,
4//! and is intended to be inherited by other contract types.
5//!
6//! You can configure the behavior of [`Erc20`] via the [`Erc20Params`] trait,
7//! which allows specifying the name, symbol, and decimals of the token.
8//!
9//! Note that this code is unaudited and not fit for production use.
10
11// Imported packages
12use alloc::string::String;
13use alloy_primitives::{Address, U256};
14use alloy_sol_types::sol;
15use core::marker::PhantomData;
16use stylus_sdk::{
17    evm,
18    msg,
19    prelude::*,
20};
21
22pub trait Erc20Params {
23    /// Immutable token name
24    const NAME: &'static str;
25
26    /// Immutable token symbol
27    const SYMBOL: &'static str;
28
29    /// Immutable token decimals
30    const DECIMALS: u8;
31}
32
33sol_storage! {
34    /// Erc20 implements all ERC-20 methods.
35    pub struct Erc20<T> {
36        /// Maps users to balances
37        mapping(address => uint256) balances;
38        /// Maps users to a mapping of each spender's allowance
39        mapping(address => mapping(address => uint256)) allowances;
40        /// The total supply of the token
41        uint256 total_supply;
42        /// Used to allow [`Erc20Params`]
43        PhantomData<T> phantom;
44    }
45}
46
47// Declare events and Solidity error types
48sol! {
49    event Transfer(address indexed from, address indexed to, uint256 value);
50    event Approval(address indexed owner, address indexed spender, uint256 value);
51
52    error InsufficientBalance(address from, uint256 have, uint256 want);
53    error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
54}
55
56/// Represents the ways methods may fail.
57#[derive(SolidityError)]
58pub enum Erc20Error {
59    InsufficientBalance(InsufficientBalance),
60    InsufficientAllowance(InsufficientAllowance),
61}
62
63// These methods aren't exposed to other contracts
64// Methods marked as "pub" here are usable outside of the erc20 module (i.e. they're callable from lib.rs)
65// Note: modifying storage will become much prettier soon
66impl<T: Erc20Params> Erc20<T> {
67    /// Movement of funds between 2 accounts
68    /// (invoked by the external transfer() and transfer_from() functions )
69    pub fn _transfer(
70        &mut self,
71        from: Address,
72        to: Address,
73        value: U256,
74    ) -> Result<(), Erc20Error> {
75        // Decreasing sender balance
76        let mut sender_balance = self.balances.setter(from);
77        let old_sender_balance = sender_balance.get();
78        if old_sender_balance < value {
79            return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
80                from,
81                have: old_sender_balance,
82                want: value,
83            }));
84        }
85        sender_balance.set(old_sender_balance - value);
86
87        // Increasing receiver balance
88        let mut to_balance = self.balances.setter(to);
89        let new_to_balance = to_balance.get() + value;
90        to_balance.set(new_to_balance);
91
92        // Emitting the transfer event
93        evm::log(Transfer { from, to, value });
94        Ok(())
95    }
96
97    /// Mints `value` tokens to `address`
98    pub fn mint(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
99        // Increasing balance
100        let mut balance = self.balances.setter(address);
101        let new_balance = balance.get() + value;
102        balance.set(new_balance);
103
104        // Increasing total supply
105        self.total_supply.set(self.total_supply.get() + value);
106
107        // Emitting the transfer event
108        evm::log(Transfer {
109            from: Address::ZERO,
110            to: address,
111            value,
112        });
113
114        Ok(())
115    }
116
117    /// Burns `value` tokens from `address`
118    pub fn burn(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
119        // Decreasing balance
120        let mut balance = self.balances.setter(address);
121        let old_balance = balance.get();
122        if old_balance < value {
123            return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
124                from: address,
125                have: old_balance,
126                want: value,
127            }));
128        }
129        balance.set(old_balance - value);
130
131        // Decreasing the total supply
132        self.total_supply.set(self.total_supply.get() - value);
133
134        // Emitting the transfer event
135        evm::log(Transfer {
136            from: address,
137            to: Address::ZERO,
138            value,
139        });
140
141        Ok(())
142    }
143}
144
145// These methods are external to other contracts
146// Note: modifying storage will become much prettier soon
147#[public]
148impl<T: Erc20Params> Erc20<T> {
149    /// Immutable token name
150    pub fn name() -> String {
151        T::NAME.into()
152    }
153
154    /// Immutable token symbol
155    pub fn symbol() -> String {
156        T::SYMBOL.into()
157    }
158
159    /// Immutable token decimals
160    pub fn decimals() -> u8 {
161        T::DECIMALS
162    }
163
164    /// Total supply of tokens
165    pub fn total_supply(&self) -> U256 {
166        self.total_supply.get()
167    }
168
169    /// Balance of `address`
170    pub fn balance_of(&self, owner: Address) -> U256 {
171        self.balances.get(owner)
172    }
173
174    /// Transfers `value` tokens from msg::sender() to `to`
175    pub fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Erc20Error> {
176        self._transfer(msg::sender(), to, value)?;
177        Ok(true)
178    }
179
180    /// Transfers `value` tokens from `from` to `to`
181    /// (msg::sender() must be able to spend at least `value` tokens from `from`)
182    pub fn transfer_from(
183        &mut self,
184        from: Address,
185        to: Address,
186        value: U256,
187    ) -> Result<bool, Erc20Error> {
188        // Check msg::sender() allowance
189        let mut sender_allowances = self.allowances.setter(from);
190        let mut allowance = sender_allowances.setter(msg::sender());
191        let old_allowance = allowance.get();
192        if old_allowance < value {
193            return Err(Erc20Error::InsufficientAllowance(InsufficientAllowance {
194                owner: from,
195                spender: msg::sender(),
196                have: old_allowance,
197                want: value,
198            }));
199        }
200
201        // Decreases allowance
202        allowance.set(old_allowance - value);
203
204        // Calls the internal transfer function
205        self._transfer(from, to, value)?;
206
207        Ok(true)
208    }
209
210    /// Approves the spenditure of `value` tokens of msg::sender() to `spender`
211    pub fn approve(&mut self, spender: Address, value: U256) -> bool {
212        self.allowances.setter(msg::sender()).insert(spender, value);
213        evm::log(Approval {
214            owner: msg::sender(),
215            spender,
216            value,
217        });
218        true
219    }
220
221    /// Returns the allowance of `spender` on `owner`'s tokens
222    pub fn allowance(&self, owner: Address, spender: Address) -> U256 {
223        self.allowances.getter(owner).get(spender)
224    }
225}

lib.rs

1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5// Modules and imports
6mod erc20;
7
8use alloy_primitives::{Address, U256};
9use stylus_sdk::{
10    msg,
11    prelude::*
12};
13use crate::erc20::{Erc20, Erc20Params, Erc20Error};
14
15/// Immutable definitions
16struct StylusTokenParams;
17impl Erc20Params for StylusTokenParams {
18    const NAME: &'static str = "StylusToken";
19    const SYMBOL: &'static str = "STK";
20    const DECIMALS: u8 = 18;
21}
22
23// Define the entrypoint as a Solidity storage object. The sol_storage! macro
24// will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
25// storage slots and types.
26sol_storage! {
27    #[entrypoint]
28    struct StylusToken {
29        // Allows erc20 to access StylusToken's storage and make calls
30        #[borrow]
31        Erc20<StylusTokenParams> erc20;
32    }
33}
34
35#[public]
36#[inherit(Erc20<StylusTokenParams>)]
37impl StylusToken {
38    /// Mints tokens
39    pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> {
40        self.erc20.mint(msg::sender(), value)?;
41        Ok(())
42    }
43
44    /// Mints tokens to another address
45    pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> {
46        self.erc20.mint(to, value)?;
47        Ok(())
48    }
49
50    /// Burns tokens
51    pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> {
52        self.erc20.burn(msg::sender(), value)?;
53        Ok(())
54    }
55}
1// Only run this as a WASM if the export-abi feature is not set.
2#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
3extern crate alloc;
4
5// Modules and imports
6mod erc20;
7
8use alloy_primitives::{Address, U256};
9use stylus_sdk::{
10    msg,
11    prelude::*
12};
13use crate::erc20::{Erc20, Erc20Params, Erc20Error};
14
15/// Immutable definitions
16struct StylusTokenParams;
17impl Erc20Params for StylusTokenParams {
18    const NAME: &'static str = "StylusToken";
19    const SYMBOL: &'static str = "STK";
20    const DECIMALS: u8 = 18;
21}
22
23// Define the entrypoint as a Solidity storage object. The sol_storage! macro
24// will generate Rust-equivalent structs with all fields mapped to Solidity-equivalent
25// storage slots and types.
26sol_storage! {
27    #[entrypoint]
28    struct StylusToken {
29        // Allows erc20 to access StylusToken's storage and make calls
30        #[borrow]
31        Erc20<StylusTokenParams> erc20;
32    }
33}
34
35#[public]
36#[inherit(Erc20<StylusTokenParams>)]
37impl StylusToken {
38    /// Mints tokens
39    pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> {
40        self.erc20.mint(msg::sender(), value)?;
41        Ok(())
42    }
43
44    /// Mints tokens to another address
45    pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> {
46        self.erc20.mint(to, value)?;
47        Ok(())
48    }
49
50    /// Burns tokens
51    pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> {
52        self.erc20.burn(msg::sender(), value)?;
53        Ok(())
54    }
55}

Cargo.toml

1[package]
2name = "stylus_erc20_example"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14
15[dev-dependencies]
16tokio = { version = "1.12.0", features = ["full"] }
17ethers = "2.0"
18eyre = "0.6.8"
19
20[features]
21export-abi = ["stylus-sdk/export-abi"]
22
23[lib]
24crate-type = ["lib", "cdylib"]
25
26[profile.release]
27codegen-units = 1
28strip = true
29lto = true
30panic = "abort"
31opt-level = "s"
1[package]
2name = "stylus_erc20_example"
3version = "0.1.7"
4edition = "2021"
5license = "MIT OR Apache-2.0"
6keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
7
8[dependencies]
9alloy-primitives = "=0.7.6"
10alloy-sol-types = "=0.7.6"
11mini-alloc = "0.4.2"
12stylus-sdk = "0.6.0"
13hex = "0.4.3"
14
15[dev-dependencies]
16tokio = { version = "1.12.0", features = ["full"] }
17ethers = "2.0"
18eyre = "0.6.8"
19
20[features]
21export-abi = ["stylus-sdk/export-abi"]
22
23[lib]
24crate-type = ["lib", "cdylib"]
25
26[profile.release]
27codegen-units = 1
28strip = true
29lto = true
30panic = "abort"
31opt-level = "s"