在《Arbitrum Stylus 深入解析与 Rust 合约部署实战》篇中,我们深入探讨了 Arbitrum Stylus 的核心技术架构,包括其 MultiVM 机制、Rust 合约开发环境搭建,以及通过 cargo stylus 实现简单计数器合约的部署与测试。Stylus 作为 Arbitrum Nitro 的升级,允许开发者使用 Rust、C++ 等语言编写高效的 WebAssembly(WASM)合约,显著降低了 Gas 成本并提升了性能。本文将更进一步,使用 Rust 在 Stylus 上实现 ERC20 标准合约,并在 Arbitrum Sepolia 上完成部署实战,带您从代码到上链一步到位
1. 前置准备:开发环境与工具链
在开始编写 ERC20 合约之前,确保开发环境已正确配置,在我的《Arbitrum Stylus 深入解析与rust合约部署实战》中,已经有这段内容了,也可以移步到那里先配置好环境:
- Rust 工具链:安装 Rust 和 Cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
- cargo-stylus 是 Stylus 合约开发的 CLI 工具,用于编译、检查和部署
cargo install --force cargo-stylus
- WASM (WebAssembly) 设置 WASM 作为 Rust 编译器的构建目标,可以看到这里执行的第三个和第四个命令有重叠的地方,官方文档是只需要执行第四个命令,但是经过我的实践,有可能会报错,提示说需要执行第三个命令,看过我上一篇《Arbitrum Stylus 深入解析与rust合约部署实战》的观众就会知道有这个问题, 所以为了保险起见,这里一并执行了
rustup install 1.81
rustup default 1.81
rustup target add wasm32-unknown-unknown
rustup target add wasm32-unknown-unknown --toolchain 1.81
- 安装好docker并启动
2. ERC20 合约:设计与实现
ERC20 是代币标准,支持转账、余额查询等功能,关于ERC协议,我会专门出一期来讲,这里就不展开讲了。这里我们将实现一个简单的 ERC20 代币,我们先来创建项目:
cargo stylus new stylus-tokens
然后在 vscode 中打开项目,我是在 wsl 中,直接执行 code . 就OK。然后,我们修改rust-toolchain.toml 中的版本 为 1.81.0
接下来我们在src 下面创建 erc20.rs 文件,并复制或者手敲一遍我给的代码,在代码中,每一行我都加上了详细的注释:
// 引入 alloc 模块中的 String 类型,用于动态字符串
use alloc::string::String;
// 引入 alloy_primitives 库中的 Address 和 U256 类型,用于处理以太坊地址和256位无符号整数
use alloy_primitives::{Address, U256};
// 引入 alloy_sol_types 库中的 sol 宏,用于定义 Solidity 风格的数据结构和事件
use alloy_sol_types::sol;
// 引入 PhantomData,用于在泛型中占位,标记类型但不实际存储数据
use core::marker::PhantomData;
// 引入 stylus_sdk 的 msg evm 和 prelude 模块,提供以太坊虚拟机交互和消息处理功能
use stylus_sdk::{evm, msg, prelude::*};
// 定义 Erc20Params 特质,用于指定 ERC20 代币的静态参数
pub trait Erc20Params {
const NAME: &'static str; // 代币名称,静态字符串
const SYMBOL: &'static str; // 代币符号,静态字符串
const DECIMALS: u8; // 代币小数位数
}
// 使用 sol_storage 宏定义 Solidity 风格的存储结构
sol_storage! {
// 定义泛型结构体 Erc20,T 需实现 Erc20Params 特质
pub struct Erc20<T> {
// 地址到余额的映射,存储每个地址的代币余额
mapping(address => uint256) balances;
// 地址到授权额度的映射,记录每个地址对其他地址的代币授权
mapping(address => mapping(address => uint256)) allowances;
// 代币总供应量
uint256 total_supply;
// 占位符,确保泛型 T 被使用但不占用存储空间
PhantomData<T> phantom;
}
}
// 使用 sol 宏定义 Solidity 风格的事件和错误
sol! {
// 定义 Transfer 事件,记录代币转账信息
event Transfer(address indexed from, address indexed to, uint256 value);
// 定义 Approval 事件,记录代币授权信息
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);
}
// 标记 Erc20Error 为 Solidity 风格的错误类型
#[derive(SolidityError)]
// 定义 ERC20 错误枚举
pub enum Erc20Error {
// 余额不足错误
InsufficientBalance(InsufficientBalance),
// 授权额度不足错误
InsufficientAllowance(InsufficientAllowance),
}
// 为 Erc20 结构体实现方法,T 需实现 Erc20Params 特质
impl<T: Erc20Params> Erc20<T> {
// 内部转账函数,执行代币转账逻辑
pub fn _transfer(&mut self, from: Address, to: Address, value: U256) -> Result<(), Erc20Error> {
// 获取发送者余额的 setter
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);
// 获取接收者余额的 setter
let mut to_balance = self.balances.setter(to);
// 计算接收者的新余额
let new_to_balance = to_balance.get() + value;
// 更新接收者余额
to_balance.set(new_to_balance);
// 记录转账事件到 EVM 日志
evm::log(Transfer { from, to, value });
Ok(())
}
// 铸造代币函数
pub fn mint(&mut self, address: Address, value: U256) -> Result<(), Erc20Error> {
// 获取目标地址余额的 setter
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> {
// 获取目标地址余额的 setter
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(())
}
}
// 标记以下方法为公开,暴露给外部调用
#[public]
// 为 Erc20 实现公开方法
impl<T: Erc20Params> Erc20<T> {
// 返回代币名称
pub fn name() -> String {
// 将静态名称转换为 String
T::NAME.into()
}
// 返回代币符号
pub fn symbol() -> String {
// 将静态符号转换为 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)
}
// 授权转账函数,允许 spender 从 from 地址转账
pub fn transfer_from(
&mut self,
from: Address,
to: Address,
value: U256,
) -> Result<bool, Erc20Error> {
// 获取 from 地址的授权映射
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)
}
// 授权函数,允许 spender 花费指定金额
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:
// 条件编译属性:除非启用 export-abi 或 test 功能,否则不生成 main 函数
#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
// 引入 alloc 模块,支持动态内存分配
extern crate alloc;
// 引入 erc20 模块,包含 ERC20 代币逻辑
mod erc20;
// 从 erc20 模块导入 Erc20 结构体、错误类型和参数特质
use crate::erc20::{Erc20, Erc20Error, Erc20Params};
// 引入 Address 和 U256 类型
use alloy_primitives::{Address, U256};
// 引入 stylus_sdk 的消息处理和预定义功能
use stylus_sdk::{msg, prelude::*};
// 定义 StylusTokenParams 结构体,用于指定代币参数
struct StylusTokenParams;
// 为 StylusTokenParams 实现 Erc20Params 特质
impl Erc20Params for StylusTokenParams {
const NAME: &'static str = "StylusToken";
const SYMBOL: &'static str = "STK";
// 代币小数位数:18
const DECIMALS: u8 = 18;
}
// 使用 sol_storage 宏定义存储结构
sol_storage! {
// 标记 StylusToken 为合约入口点
#[entrypoint]
// 定义 StylusToken 结构体
struct StylusToken {
// 标记 erc20 字段为借用,继承 Erc20 功能
#[borrow]
// 嵌入 Erc20 结构体,使用 StylusTokenParams 参数
Erc20<StylusTokenParams> erc20;
}
}
// 标记以下方法为公开
#[public]
// 继承 Erc20<StylusTokenParams> 的方法
#[inherit(Erc20<StylusTokenParams>)]
// 为 StylusToken 实现方法
impl StylusToken {
// 铸造代币到调用者地址
pub fn mint(&mut self, value: U256) -> Result<(), Erc20Error> {
// 调用 Erc20 的 mint 方法
self.erc20.mint(msg::sender(), value)?;
Ok(())
}
// 铸造代币到指定地址
pub fn mint_to(&mut self, to: Address, value: U256) -> Result<(), Erc20Error> {
// 调用 Erc20 的 mint 方法
self.erc20.mint(to, value)?;
Ok(())
}
// 销毁调用者的代币
pub fn burn(&mut self, value: U256) -> Result<(), Erc20Error> {
// 调用 Erc20 的 burn 方法
self.erc20.burn(msg::sender(), value)?;
Ok(())
}
}
接着是 main.rs 中的内容:
// 条件编译属性:除非启用 test 或 export-abi 功能,否则不生成 main 函数
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
// 条件编译:当 test 和 export-abi 均未启用时
#[cfg(not(any(test, feature = "export-abi")))]
// 禁止名称修饰,确保函数名在编译后保持不变
#[no_mangle]
// 定义空的 main 函数,用于合约入口
pub extern "C" fn main() {}
// 条件编译:当启用 export-abi 功能时
#[cfg(feature = "export-abi")]
// 定义 main 函数,用于导出 ABI
fn main() {
// 调用 print_abi 函数,生成 Solidity ABI,指定许可证和 Solidity 版本
stylus_tokens::print_abi("MIT-OR-APACHE-2.0", "pragma solidity ^0.8.23;");
}
Cargo.toml 中的配置:
[package]
name = "stylus_tokens"
version = "0.1.11"
edition = "2021"
license = "MIT OR Apache-2.0"
homepage = "https://github.com/OffchainLabs/stylus-hello-world"
repository = "https://github.com/OffchainLabs/stylus-hello-world"
keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
description = "Stylus tokens example"
[dependencies]
alloy-primitives = "=0.8.20"
alloy-sol-types = "=0.8.20"
mini-alloc = "0.4.2"
stylus-sdk = "0.8.0"
hex = "0.4.3"
dotenv = "0.15.0"
[dev-dependencies]
tokio = { version = "1.12.0", features = ["full"] }
ethers = "2.0"
eyre = "0.6.8"
[features]
export-abi = ["stylus-sdk/export-abi"]
debug = ["stylus-sdk/debug"]
[[bin]]
name = "stylus_tokens"
path = "src/main.rs"
[lib]
crate-type = ["lib", "cdylib"]
[profile.release]
codegen-units = 1
strip = true
lto = true
panic = "abort"
opt-level = "s"
3. 合约部署上链并mint代币
一切准备就绪之后,我们来编译并且在链上验证我们的代码:
cargo stylus check -e https://sepolia-rollup.arbitrum.io/rpc
我们将一些参数导出成变量
export ARB_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
export PRIVATE_KEY=你的私钥
然后我们 可以来估算部署合约所需的 gas,这一个步骤不是必需的:
cargo stylus deploy --endpoint=$ARB_RPC_URL --private-key=$PRIVATE_KEY --estimate-gas
OK,开始部署:
cargo stylus deploy --endpoint=$ARB_RPC_URL --private-key=$PRIVATE_KEY
到这里已经部署成功,可以看到合约地址与交易hash,接下来我们开始铸造代币,如果你没有安装 foundry,(foundry 我会出一期详细的教程),请参考我的《Arbitrum Stylus 深入解析与rust合约部署实战》中的方式,导出ABI,然后在 remix 中去操作,这里我使用 foundry cast 命令去mint 代币:
cast send --rpc-url $ARB_RPC_URL --private-key $PRIVATE_KEY 0xb032fb53175b9c24ac157f4a7896ad200fd93468 "mint(uint256)" 100000000000000000000000000
可以看到我成功mint了一亿枚代币,因为有18位小数,所以在你想要mint的数量后面,再加上18个0,0xb032fb53175b9c24ac157f4a7896ad200fd93468 是合约的地址,到时候替换成你们部署成功的合约地址,我们去钱包导入代币,看看代币有没有到账:
可以看到我们代币已经到账了,接下来演示使用命令查看某个地址的代币余额:
OK,如果你走到了这里,恭喜你,你已经完成了 使用 Rust 在 Stylus 上实现 ERC20 合约,重复是最好的老师,希望大家多多练习,后面我也会继续更新系列教程,我是红烧6,关注我,带你上车 web3!
stylus 官方示例:stylus-by-example
代码仓库:stylus-tokens