Writing the Token Contract
Let's create an ERC-20 token contract in Rust using Arbitrum Stylus step-by-step, focusing on clarity and conciseness.
Step 1: Create a New Project
Create a new Stylus project:
cargo stylus new stylus_erc20 cd stylus_erc20
Step 2: Write the ERC-20 Token Contract
src/erc20.rs
Create a new file src/erc20.rs
and add the following code:
use alloc::string::String;
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use core::marker::PhantomData;
use stylus_sdk::{evm, msg, prelude::*};
pub trait Erc20Params {
const NAME: &'static str;
const SYMBOL: &'static str;
const DECIMALS: u8;
}
sol_storage! {
pub struct Erc20<T> {
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
uint256 total_supply;
PhantomData<T> phantom;
}
}
sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
error InsufficientBalance(address from, uint256 have, uint256 want);
error InsufficientAllowance(address owner, address spender, uint256 have, uint256 want);
}
#[derive(SolidityError)]
pub enum Erc20Error {
InsufficientBalance(InsufficientBalance),
InsufficientAllowance(InsufficientAllowance),
}
impl<T: Erc20Params> Erc20<T> {
pub fn _transfer(
&mut self,
from: Address,
to: Address,
value: U256,
) -> Result<(), Erc20Error> {
let mut sender_balance = self.balances.setter(from);
let old_sender_balance = sender_balance.get();
if old_sender_balance < value {
return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
from,
have: old_sender_balance,
want: value,
}));
}
sender_balance.set(old_sender_balance - value);
let mut to_balance = self.balances.setter(to);
let new_to_balance = to_balance.get() + value;
to_balance.set(new_to_balance);
evm::log(Transfer { from, to, value });
Ok(())
}
pub fn mint(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
let mut balance = self.balances.setter(address);
let new_balance = balance.get() + value;
balance.set(new_balance);
self.total_supply.set(self.total_supply.get() + value);
evm::log(Transfer {
from: Address::ZERO,
to: address,
value,
});
Ok(())
}
pub fn burn(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
let mut balance = self.balances.setter(address);
let old_balance = balance.get();
if old_balance < value {
return Err(Erc20Error::InsufficientBalance(InsufficientBalance {
from: address,
have: old_balance,
want: value,
}));
}
balance.set(old_balance - value);
self.total_supply.set(self.total_supply.get() - value);
evm::log(Transfer {
from: address,
to: Address::ZERO,
value,
});
Ok(())
}
}
#[external]
impl<T: Erc20Params> Erc20<T> {
pub fn name() -> String {
T::NAME.into()
}
pub fn symbol() -> String {
T::SYMBOL.into()
}
pub fn decimals() -> u8 {
T::DECIMALS
}
pub fn total_supply(&self) -> U256 {
self.total_supply.get()
}
pub fn balance_of(&self, owner: Address) -> U256 {
self.balances.get(owner)
}
pub fn transfer(&mut self, to: Address, value: U256) -> Result<bool, Erc20Error> {
self._transfer(msg::sender(), to, value)?;
Ok(true)
}
pub fn transfer_from(
&mut self,
from: Address,
to: Address,
value: U256,
) -> Result<bool, Erc20Error> {
let mut sender_allowances = self.allowances.setter(from);
let mut allowance = sender_allowances.setter(msg::sender());
let old_allowance = allowance.get();
if old_allowance < value {
return Err(Erc20Error::InsufficientAllowance(InsufficientAllowance {
owner: from,
spender: msg::sender(),
have: old_allowance,
want: value,
}));
}
allowance.set(old_allowance - value);
self._transfer(from, to, value)?;
Ok(true)
}
pub fn approve(&mut self, spender: Address, value: U256) -> bool {
self.allowances.setter(msg::sender()).insert(spender, value);
evm::log(Approval {
owner: msg::sender(),
spender,
value,
});
true
}
pub fn allowance(&self, owner: Address, spender: Address) -> U256 {
self.allowances.getter(owner).get(spender)
}
}
src/lib.rs
This file acts as the main entry point for your contract.
// Only run this as a WASM if the export-abi feature is not set.
#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
extern crate alloc;
mod erc20;
use alloy_primitives::{Address, U256};
use stylus_sdk::{msg, prelude::*};
use crate::erc20::{Erc20, Erc20Params, Erc20Error};
#[global_allocator]
static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
struct StylusTokenParams;
impl Erc20Params for StylusTokenParams {
const NAME: &'static str = "StylusToken";
const SYMBOL: &'static str = "STK";
const DECIMALS: u8 = 18;
}
sol_storage! {
#[entrypoint]
struct StylusToken {
#[borrow]
Erc20<StylusTokenParams> erc20;
}
}
#[external]
#[inherit(Erc20<StylusTokenParams>)]
impl StylusToken {
pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> {
self.erc20.mint(msg::sender(), value)?;
Ok(())
}
pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> {
self.erc20.mint(to, value)?;
Ok(())
}
pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> {
self.erc20.burn(msg::sender(), value)?;
Ok(())
}
}
Continuing from the Explanation of the Code section:
Explanation of the Code
Storage Definition: The
sol_storage!
macro defines the contract’s storage, mapping addresses to balances and allowances, keeping track of the total supply, and including aPhantomData
field to allow for generics.Events and Errors: The
sol!
macro declares events such asTransfer
andApproval
, and errors likeInsufficientBalance
andInsufficientAllowance
, which are used to signal important state changes and conditions.Internal Methods:
_transfer
: Handles the logic for transferring tokens between accounts, updating balances, and emitting theTransfer
event.mint
: Increases the balance of a specified address and the total supply, emitting aTransfer
event from the zero address.burn
: Decreases the balance of a specified address and the total supply, emitting aTransfer
event to the zero address.
External Methods:
name
,symbol
,decimals
: Return the immutable token name, symbol, and decimals as specified by theErc20Params
trait.total_supply
: Returns the total supply of the token.balance_of
: Returns the balance of a specified address.transfer
: Transfers tokens from the sender’s account to another address, updating balances and emitting theTransfer
event.transfer_from
: Transfers tokens from one address to another using an allowance, updating balances, decreasing the allowance, and emitting theTransfer
event.approve
: Approves another address to spend a specified amount of tokens on behalf of the sender, updating allowances and emitting theApproval
event.allowance
: Returns the remaining number of tokens that a spender is allowed to spend on behalf of the owner.
Last updated