Arbitrum Stylus 合约实战 :Rust 实现 ERC20

article/2025/9/6 18:10:11

在《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> {// 获取发送者余额的 setterlet 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);// 获取接收者余额的 setterlet 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> {// 获取目标地址余额的 setterlet 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> {// 获取目标地址余额的 setterlet 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 {// 将静态名称转换为 StringT::NAME.into()}// 返回代币符号pub fn symbol() -> String {// 将静态符号转换为 StringT::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";// 代币小数位数:18const 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!

 Arbitrum官方文档:官方文档

stylus 官方示例:stylus-by-example

代码仓库:stylus-tokens


http://www.hkcw.cn/article/jdWuEEMYVN.shtml

相关文章

ADQ36-2通道2.5G,4通道5G采样PXIE

ADQ36是一款高端12位四通道灵活数据采集板&#xff0c;针对高通道数科学应用进行了优化。ADQ36具有以下特性: 4 / 2模拟输入通道每通道2.5 / 5 GSPS7gb/秒的持续数据传输速率两个外部触发器通用输入/输出&#xff08;GPIO&#xff09;ADQ36数字化仪包括固件FWDAQ ADQ36简介 特…

20中数组去重的方法20种数组去重的方法

开始 本文有很多问题&#xff0c;并没有直接给出答案&#xff0c;大伙有自己思考的可以评论区留言。关于时间复杂度只是一个大体的估计。20种只能说保守了&#xff0c;20种都是单论思路而已&#xff0c;暂时没想到更多的思路&#xff0c;有其他方法的可以评论区留言。 easy模式…

工厂模式 vs 策略模式:设计模式中的 “创建者” 与 “决策者”

在日常工作里&#xff0c;需求变动或者新增功能是再常见不过的事情了。而面对这种情况时&#xff0c;那些耦合度较高的代码就会给我们带来不少麻烦&#xff0c;因为在这样的代码基础上添加新需求往往困难重重。为了保证系统的稳定性&#xff0c;我们在添加新需求时&#xff0c;…

Emacs 折腾日记(二十六)——buffer与窗口管理

本节我们将介绍如何在Emacs中的buffer与窗口管理&#xff0c;目标是快速管理窗口&#xff0c;以及快速在不同buffer中进行切换 基本概念介绍 Emacs与vim相比的一个特点是&#xff0c;Emacs是一个窗口程序&#xff0c;或者说是一个gui程序。而vim是一个终端字符界面程序(当然E…

强化学习(十三)DQN

传统的强化学习算法会使用表格的形式存储状态价值函数 V ( s ) V(s) V(s) 或动作价值函数 Q ( s ) Q(s) Q(s) &#xff0c;但是这样的方法存在很大的局限性。例如&#xff0c;现实中的强化学习任务所面临的状态空间往往是连续的&#xff0c;存在无穷多个状态&#xff0c;在这…

RapidOCR集成PP-OCRv5_det mobile模型记录

该文章主要摘取记录RapidOCR集成PP-OCRv5_mobile_det记录&#xff0c;涉及模型转换&#xff0c;模型精度测试等步骤。原文请前往官方博客&#xff1a; https://rapidai.github.io/RapidOCRDocs/main/blog/2025/05/26/rapidocr%E9%9B%86%E6%88%90pp-ocrv5_det%E6%A8%A1%E5%9E%8B…

【深度学习】13. 图神经网络GCN,Spatial Approach, Spectral Approach

图神经网络 图结构 vs 网格结构 传统的深度学习&#xff08;如 CNN 和 RNN&#xff09;在处理网格结构数据&#xff08;如图像、语音、文本&#xff09;时表现良好&#xff0c;因为这些数据具有固定的空间结构。然而&#xff0c;真实世界中的很多数据并不遵循网格结构&#x…

从“无差别降噪”到“精准语音保留”:非因果优化技术为助听设备和耳机降噪注入新活力

在复杂环境中保持清晰语音感知一直是助听设备与消费级耳机的核心挑战。传统主动降噪&#xff08;ANC&#xff09;技术虽能抑制环境噪声&#xff0c;但会无差别削弱所有声音&#xff0c;导致用户难以听清目标方向的语音&#xff08;如对话者&#xff09;。近年来&#xff0c;开放…

家庭路由器改装,搭建openwrt旁路由以及手机存储服务器,实现外网节点转发、内网穿透、远程存储、接入满血DeepSeek方案

大家好&#xff0c;也是好久没有发文了&#xff0c;最近在捣鼓一些比较有趣的东西&#xff0c;打算跟大家分享一下&#xff01; 先聊一下我的大致方案嘛&#xff0c;最近感觉家里路由器平时一直就只有无线广播供网的功能&#xff0c;感觉这么好的一下嵌入式设备产品不应该就干这…

【Linux】shell脚本的变量与运算

目录 一.变量 1.1什么是变量 1.2变量的命名 1.3变量的调用 1.4字符的转义 1.5变量的取消 二.变量的类型 2.1函数级变量 2.2环境级变量 2.3用户级变量 2.4系统级变量 2.5常见的系统变量 三..特殊变量及定义 3.1用命令的执行结果定义变量 3.2传参变量 3.3交互式传…

Linux进程概念

一.冯诺依曼体系结构 冯诺依曼体系结构是当代计算机的基本结构&#xff0c;它主要包括几个板块&#xff0c;输入设备&#xff0c;输出设备&#xff0c;存储器&#xff0c;运算器和控制器。 下面是简略版的图解析&#xff1a; 输入设备主要包含鼠标&#xff0c;键盘&#xff0…

[9-2] USART串口外设 江协科技学习笔记(9个知识点)

1 2 3 智能卡、IrDA和LIN是三种不同的通信技术&#xff0c;它们在电子和汽车领域中有着广泛的应用&#xff1a; • 智能卡&#xff08;Smart Card&#xff09;&#xff1a; • 是什么&#xff1a;智能卡是一种带有嵌入式微处理器和存储器的塑料卡片&#xff0c;可以存储和处理数…

低代码——表单生成器以form-generator为例

主要执行流程说明&#xff1a; 初始化阶段 &#xff1a; 接收表单配置对象formConf深拷贝配置&#xff0c;初始化表单数据和验证规则处理每个表单组件的默认值和特殊配置&#xff08;如文件上传&#xff09; 渲染阶段 &#xff1a; 通过render函数创建el-form根组件递归渲染表…

奥威BI+AI——高效智能数据分析工具,引领数据分析新时代

随着数据量的激增&#xff0c;企业对高效、智能的数据分析工具——奥威BIAI的需求日益迫切。奥威BIAI&#xff0c;作为一款颠覆性的数据分析工具&#xff0c;凭借其独特功能&#xff0c;正在引领数据分析领域的新纪元。 一、‌零报表环境下的极致体验‌ 奥威BIAI突破传统报表限…

grid网格布局

使用flex布局的痛点 如果使用justify-content: space-between;让子元素两端对齐&#xff0c;自动分配中间间距&#xff0c;假设一行4个&#xff0c;如果每一行都是4的倍数那没任何问题&#xff0c;但如果最后一行是2、3个的时候就会出现下面的状况&#xff1a; /* flex布局 两…

基于 GitLab CI + Inno Setup 实现 Windows 程序自动化打包发布方案

在 Windows 桌面应用开发中&#xff0c;实现自动化构建与打包发布是一项非常实用的工程实践。本文以我在开发PackTes项目时的为例&#xff0c;介绍如何通过 GitLab CI 配合 Inno Setup、批处理脚本、Qt 构建工具&#xff0c;实现版本化打包并发布到共享目录的完整流程。 项目地…

江西某石灰石矿边坡自动化监测

1. 项目简介 该矿为露天矿山&#xff0c;开采矿种为水泥用石灰岩&#xff0c;许可生产规模200万t/a&#xff0c;矿区面积为1.2264km2&#xff0c;许可开采深度为422m&#xff5e;250m。矿区地形为东西一北东东向带状分布&#xff0c;北高南低&#xff0c;北部为由浅变质岩系组…

星海掘金:校园极客的Token诗篇(蓝耘MaaS平台)——从数据尘埃到智能生命的炼金秘录

hi&#xff0c;我是云边有个稻草人 目录 前言 一、初识蓝耘元生代MaaS平台&#xff1a;零门槛体验AI服务 1.1 从零开始——平台注册与环境搭建 1.2 平台核心功能 1.3 蓝耘平台的优势在哪里&#xff1f; 二、知识库构建新篇章&#xff1a;从零碎资料到智能语义仓库的蜕变…

测试概念 和 bug

一 敏捷模型 在面对在开发项目时会遇到客户变更需求以及合并新的需求带来的高成本和时间 出现的敏捷模型 敏捷宣言 个人与交互重于过程与工具 强调有效的沟通 可用的软件重于完备的文档 强调轻文档重产出 客户协作重于合同谈判 主动及时了解当下的要求 相应变化…

14. 最长公共前缀

以示例1为例 从左到右依次比较每个字符&#xff1a; 第一个字符都是f&#xff0c;当前最长公共前缀为"f"第二个字符都是l&#xff0c;当前最长公共前缀更新为"fl"第三个字符不一致&#xff0c;因此最终最长公共前缀确定为"fl" 具体实现思路&am…