Sway 钱包智能合约实战:ABI 声明与合约实现的双项目架构
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
本文基于 Sway 官方文档 Wallet Smart Contract 示例,讲解如何构建一个典型的 Sway 钱包合约:将 ABI 接口声明抽离为独立库项目,由合约项目以路径依赖方式引用并实现。读完本文,你可以掌握abi声明、#[payable]/#[storage]属性、std库消息上下文函数与transfer转账机制的配合方式,以及 Checks-Effects-Interactions 防重入模式的落地细节。
一、总体架构:ABI 声明与实现分离
Sway 中 ABI 接口声明与 ABI 实现分属两个独立的 Forc 项目。wallet_abi项目是一个library,只声明接口契约;wallet_smart_contract项目是contract,负责具体实现。官方文档给出的目录结构如下(对应仓库中的 examples/wallet_abi 与 examples/wallet_smart_contract):
. ├── wallet_abi │ ├── Forc.toml │ └── src │ └── main.sw └── wallet_smart_contract ├── Forc.toml └── src └── main.sw其中wallet_abi被当作外部库使用。使用外部库时,必须在项目自身的Forc.toml中声明依赖来源。wallet_smart_contract项目中的声明为:
[dependencies] wallet_abi = { path = "../wallet_abi/" }仓库中实际的 Forc.toml 完整内容如下,可以看到除了wallet_abi路径依赖外,还显式声明了std标准库依赖(指向本仓库的 sway-lib-std 源码目录):
[project] authors = ["Fuel Labs <contact@fuel.sh>"] entry = "main.sw" license = "Apache-2.0" name = "wallet_smart_contract" [dependencies] std = { path = "../../sway-lib-std" } wallet_abi = { path = "../wallet_abi" }而 ABI 库项目自身的 Forc.toml 仅声明std依赖,因为纯接口声明本身不依赖合约逻辑。
二、ABI 声明:定义合约对外契约
ABI 声明位于 examples/wallet_abi/src/main.sw,完整代码如下:
library; abi Wallet { #[storage(read, write), payable] fn receive_funds(); #[storage(read, write)] fn send_funds(amount_to_send: u64, recipient_address: Address); }关键要素说明:
library;:声明该项目为库而非合约/脚本,其产物只被其他项目以依赖方式引用;abi Wallet { ... }:定义名为Wallet的接口契约,其中的函数只有签名,没有函数体。任何合约只要impl Wallet for Contract就必须按签名提供实现;#[storage(read, write), payable]:receive_funds允许读写 storage(入账需要累计余额),且标记为payable,即调用方可以随调用发送资产;#[storage(read, write)]:send_funds同样需要读写 storage(扣减余额),但不是payable——转账本身不依赖随调用发送的资产。
三、ABI 实现:合约完整源码
合约实现位于 examples/wallet_smart_contract/src/main.sw,完整代码如下:
contract; use std::{asset::transfer, call_frames::msg_asset_id, context::msg_amount}; use wallet_abi::Wallet; const OWNER_ADDRESS = Address::from(0x8900c5bec4ca97d4febf9ceb4754a60d782abbf3cd815836c1872116f203f861); storage { balance: u64 = 0, } impl Wallet for Contract { #[storage(read, write), payable] fn receive_funds() { if msg_asset_id() == AssetId::base() { // If we received the base asset then keep track of the balance. // Otherwise, we're receiving other native assets and don't care // about our balance of coins. storage.balance.write(storage.balance.read() + msg_amount()); } } #[storage(read, write)] fn send_funds(amount_to_send: u64, recipient_address: Address) { let sender = msg_sender().unwrap(); match sender { Identity::Address(addr) => assert(addr == OWNER_ADDRESS), _ => revert(0), }; let current_balance = storage.balance.read(); assert(current_balance >= amount_to_send); storage.balance.write(current_balance - amount_to_send); // Note: `transfer()` is not a call and thus not an // interaction. Regardless, this code conforms to // checks-effects-interactions to avoid re-entrancy. transfer( Identity::Address(recipient_address), AssetId::base(), amount_to_send, ); } }3.1 存储与常量
storage { balance: u64 = 0 }:定义一个持久化存储字段balance,初始值为 0。在合约中通过storage.balance.read()/storage.balance.write(...)访问,这正是 ABI 声明中#[storage(read, write)]属性的实现依据——编译器会校验实现侧对 storage 的访问不超出声明范围;OWNER_ADDRESS:以 32 字节十六进制字面量构造Address,作为唯一有权限提取资金的地址。
3.2 receive_funds:入账记账
receive_funds逻辑:
msg_asset_id()来自 sway-lib-std/src/call_frames.sw,返回当前调用随附的资产类型;- 仅当资产是
AssetId::base()(基础资产,即燃料代币)时才记账,其他原生资产直接忽略(注释明确说明:"we're receiving other native assets and don't care about our balance of coins"); msg_amount()来自 sway-lib-std/src/context.sw,其实现是读取当前调用上下文中的balance寄存器,返回本次调用发送过来的资产数量。
记账方式为"读取—累加—写回"三步:storage.balance.write(storage.balance.read() + msg_amount())。
3.3 send_funds:权限校验、余额检查与转账
send_funds按严格的 Checks-Effects-Interactions 顺序执行:
- 身份检查(Check):
msg_sender()来自 sway-lib-std/src/auth.sw,返回Result<Identity, AuthError>,封装了当前调用的发送者身份。实现侧先unwrap(),然后match解构Identity:只有Identity::Address(addr)且addr == OWNER_ADDRESS时通过assert校验;其他任何身份(合约、predicate 等)直接revert(0)中止执行; - 效果(Effect):读取当前
storage.balance,assert(current_balance >= amount_to_send)防止超额提取,随后立即将扣减后的余额写回 storage。先改状态、后转账是防重入的关键; - 交互(Interaction):最后调用
std::asset::transfer完成实际转账。
3.4 std 库中 transfer 的底层实现
transfer定义在 sway-lib-std/src/asset.sw。从源码结构看,它是一个分发函数,按收款方身份选择不同路径:
pub fn transfer(to: Identity, asset_id: AssetId, amount: u64) { match to { Identity::Address(addr) => transfer_to_address(addr, asset_id, amount), Identity::ContractId(id) => force_transfer_to_contract(id, asset_id, amount), }; }- 转给合约时走
force_transfer_to_contract,最终执行内联汇编的tr指令; - 转给地址时走
transfer_to_address(见 asset.sw):由于tro指令需要占用一个"空的变量输出"槽位,实现会遍历交易输出(output_count/output_type/output_amount),找到amount为 0 的Output::Variable后执行tro指令;找不到空闲槽位则以revert(FAILED_TRANSFER_TO_ADDRESS_SIGNAL)失败。
文档中特别强调:"transfer()is not a call and thus not an interaction"——transfer只是向交易输出写资金,不会调用其他合约的代码,因此不构成可被重入的"交互"。即便如此,示例仍严格遵循 Checks-Effects-Interactions 顺序,把余额扣减放在transfer之前,形成纵深防御。
此外,asset.sw的文档注释明确列出了transfer的三种 revert 条件:金额超过合约该资产余额、金额为 0、转给地址时没有空闲变量输出。示例合约中的assert(current_balance >= amount_to_send)正是对第一种条件的业务侧提前拦截。
四、外部调用方视角:如何驱动这个钱包
仓库中配套的 examples/wallet_contract_caller_script/src/main.sw 演示了脚本如何持有同一份wallet_abi并发起跨项目调用:
script; use wallet_abi::Wallet; fn main() { let contract_address = 0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b; let caller = abi(Wallet, contract_address); let amount_to_send = 200; let recipient_address = Address::from(0x9299da6c73e6dc03eeabcce242bb347de3f5f56cd1c70926d76526d7ed199b8b); caller .send_funds { gas: 10000, coins: 0, asset_id: b256::zero(), }(amount_to_send, recipient_address); }这段代码印证了"ABI 声明独立成项目"的价值:调用方不需要知道合约的内部实现,只需依赖wallet_abi,通过abi(Wallet, contract_address)得到带类型的客户端。调用参数块中的gas、coins、asset_id分别是本次跨合约调用预留的 gas 上限、随调用发送的币数和资产 ID;send_funds非payable,这里coins传 0 也符合接口约定。
五、安全设计要点小结
| 机制 | 实现位置 | 作用 |
|---|---|---|
| Owner 白名单 | main.sw 中match sender+revert(0) | 仅OWNER_ADDRESS可提取资金,非地址身份直接回滚 |
| 余额下限断言 | assert(current_balance >= amount_to_send) | 防止超额提取与无资产转账 |
| 先记账后转账 | 扣减 storage 在transfer之前 | 符合 Checks-Effects-Interactions,防重入 |
| 仅记基础资产 | receive_funds中msg_asset_id() == AssetId::base()判断 | 明确记账范围,避免混币歧义 |
综上,这个钱包示例以最小的代码量覆盖了 Sway 合约开发的核心链路:独立 ABI 库项目 →Forc.toml路径依赖 →contract实现 +storage声明 → std 库消息上下文与资产转账 → 脚本侧abi客户端调用,是理解 Sway 合约接口设计与安全编码模式的标准起点。
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考