Sway 合约所有权与访问控制:基于 msg_sender() 实现 Owner 权限管理的完整实战
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
导读
在去中心化应用中,大量合约功能需要限制为只有特定账户(如合约部署者、DAO 管理员)才能调用,例如提现、升级、修改配置等敏感操作。本文基于 Sway 语言官方文档中的contract-ownership示例,从零实现一个标准的"合约所有权(Ownership)"访问控制模式:通过Option<Identity>存储记录合约所有者,并借助标准库msg_sender()校验调用者身份,最终实现"仅所有者可执行"的权限门禁。读完本文,你将掌握 Sway 合约中Identity的使用、msg_sender()的底层原理、assert防御式校验的写法,以及完整的可运行合约代码。
一、核心设计思想:合约所有权模式
合约所有权(Contract Ownership)是最基础、最常用的访问控制模式之一。其思路非常直观:
- 在合约存储中记录一个"所有者"(owner)标识;
- 在需要保护的函数入口处,将当前调用者(
msg_sender()的返回值)与存储中的所有者进行比较; - 身份不匹配时通过
assert直接让交易回滚(revert),阻止未授权调用继续执行。
该示例对应的完整可运行源码位于 docs/reference/src/code/examples/access-control/ownership/src/main.sw,其工程清单见 docs/reference/src/code/examples/access-control/ownership/Forc.toml。
Sway 合约由"ABI(接口)"与"ABI 实现"两部分构成,详见 Smart Contracts:ABI 定义了合约对外暴露的端点,impl <ABI名> for Contract则负责给出具体实现。下面我们严格按此结构展开。
二、定义 ABI:暴露"设置所有者"与"受限动作"两个端点
ABI 是合约对外调用的接口层。在本例中,合约暴露两个函数:
set_owner:设置(或转移)所有者,需要读写存储;action:只有所有者才能调用的受保护动作,仅读取存储。
abi Ownership { #[storage(read, write)] fn set_owner(owner: Option<Identity>); #[storage(read)] fn action(); }代码要点:
#[storage(read, write)]与#[storage(read)]是存储访问注解,告知编译器该函数对存储的访问模式。set_owner因为要写入新的所有者,所以声明为read, write;action只需要比对存储中的所有者,声明为read即可。存储注解不匹配时编译器会报错,这是 Sway 强制显式声明存储行为的体现。set_owner的参数类型是Option<Identity>而非Identity。之所以允许"空",是为了支持"清除所有者"或"初始尚无所有者"的场景,这与下文存储初始值的设置保持一致。
三、Identity 与存储:记录所有者
所有者是谁?Sway 用Identity统一表达"外部地址(Address)"与"合约(ContractId)"两类调用者身份,从而让访问控制同时适用于"EOA 账户"和"合约账户"。
我们必须在存储中持久化记录所有者,并在每次调用时与调用者对比。由于合约刚部署时并不存在所有者,初始值被设置为None:
storage { owner: Option<Identity> = None, }代码要点:
storage块声明了链上存储变量owner,类型为Option<Identity>,初始值为None;- 在实现函数中,通过
storage.owner.read()读取、storage.owner.write(...)写入; Option在 Sway 标准库中定义(sway-lib-std/src/option.sw),None表示"尚无所有者",这正是"首次设置所有者"这一特殊分支能被正确处理的根基。
四、实现访问控制:set_owner 与 action
ABI 只是接口声明,真正的权限逻辑在impl Ownership for Contract中实现。与 Rust 中 trait 的实现语法一致,所有 ABI 中声明的函数都必须在实现中给出(见 Smart Contracts):
impl Ownership for Contract { #[storage(read, write)] fn set_owner(owner: Option<Identity>) { assert(storage.owner.read().is_none() || storage.owner.read().unwrap() == msg_sender().unwrap()); storage.owner.write(owner); } #[storage(read)] fn action() { assert(storage.owner.read().unwrap() == msg_sender().unwrap()); // code } }4.1 set_owner:满足任一条件才允许设置所有者
设置所有者时,必须满足以下两个条件之一,否则assert触发回滚:
- 当前没有所有者(
storage.owner.read().is_none()):合约刚部署、所有者尚未初始化时的首次授权路径; - 当前调用者就是所有者(
storage.owner.read().unwrap() == msg_sender().unwrap()):所有者本人调用,用于后续"转移所有权"或"重新授权"。
条件通过后,将新值写入存储:storage.owner.write(owner)。注意这里写入的就是调用者传入的Option<Identity>,因此调用方也可以传None来"清除"所有者(具体业务是否允许清除由开发者自行约定)。
4.2 action:仅所有者可调用
action()代表任何需要权限保护的合约功能。它的校验只有一条:
assert(storage.owner.read().unwrap() == msg_sender().unwrap());- 若存储中还没有所有者,
unwrap()会在断言之前直接使调用失败(None不可unwrap),保证"无主合约"无法执行受保护操作; - 若调用者不是所有者,
assert中的相等比较为false,交易立即回滚,// code处的业务逻辑不会执行; - 只有所有者本人调用时,校验通过,后续代码才继续运行。
在实际项目中,action()内的业务代码可以是提现、暂停、参数更新等任意敏感操作。这套模式即通用"Ownable"风格的权限门禁。
五、深入底层:msg_sender() 是如何判定调用者身份的
示例中反复出现的msg_sender()由标准库自动导入(见 prelude),其实现位于 sway-lib-std/src/auth.sw。理解它的判定逻辑,才能明白为什么所有权校验是可信的。
msg_sender()的返回类型是Result<Identity, AuthError>,其核心逻辑分两条路径:
pub fn msg_sender() -> Result<Identity, AuthError> { if caller_is_external() { match caller_address() { Err(err) => Err(err), Ok(owner) => Ok(Identity::Address(owner)), } } else { // Get caller's `ContractId`. Ok(Identity::ContractId(caller_contract_id())) } }关键机制逐层拆解(依据 sway-lib-std/src/auth.sw):
- 判断调用是否来自外部:
caller_is_external()通过内联汇编读取虚拟机元数据指令gm r1 i1,返回true表示调用者是外部脚本(EOA 地址),false表示调用者是另一个合约; - 外部调用者 → 解析地址:调用
caller_address(),它遍历交易的全部输入(Input::Coin与Input::Message),要求所有输入属于同一个所有者,否则返回AuthError::InputsNotAllOwnedBySameAddress。这正是防止"一个交易里混入多个签名者"从而混淆身份的防护; - 合约调用者 → 解析合约 ID:
caller_contract_id()通过gm r1 i2指令获取调用方合约的ContractId; - 最终统一包装为
Identity::Address(...)或Identity::ContractId(...),使外部账户与合约在访问控制上等价对待。
错误类型AuthError同样定义在 sway-lib-std/src/auth.sw:InputsNotAllOwnedBySameAddress表示外部输入并非同属一个地址;CallerIsInternal表示在内部上下文错误地调用了caller_address。由于msg_sender()返回的是Result,示例中使用.unwrap()直接取Identity,一旦出错整个调用回滚——在权限校验场景中这是可接受的"fail-closed(默认拒绝)"策略。
六、工程配置与运行方式
示例合约以独立 Forc 工程组织,其清单 docs/reference/src/code/examples/access-control/ownership/Forc.toml 内容如下:
[project] authors = ["Fuel Labs <contact@fuel.sh>"] entry = "main.sw" license = "Apache-2.0" name = "ownership" [dependencies] std = { path = "../../../../../../../sway-lib-std" }配置说明:
name = "ownership":工程名,也是生成 ABI 文件的基础名;entry = "main.sw":入口源文件;std依赖通过相对路径指向仓库根目录下的 sway-lib-std(真实工程中一般替换为已发布的std版本号)。
在已安装forc(Fuel Orchestrator)的前提下,可在此目录执行以下命令进行构建与测试:
forc build # 编译合约,生成 ABI 与字节码 forc test # 运行合约测试(如配置了测试目标)构建产物包括 JSON 格式的 ABI 文件与二进制字节码,可供 SDK(如 fuels-rs / fuels-ts)加载后部署与调用。
七、与本主题相关的进一步阅读
围绕"访问控制"这一主题,仓库内还有以下资料可深入:
- Message Sender 官方文档:单独讲解
msg_sender()的用途与访问控制应用,并给出将调用者与OWNER常量比较的最小示例; - Smart Contracts 文档:ABI 与
impl ... for Contract的完整语法约定; - 标准库实现 sway-lib-std/src/auth.sw:
msg_sender、caller_address、caller_contract_id、caller_is_external等全套调用者身份 API; - 类型定义 sway-lib-std/src/identity.sw 与 sway-lib-std/src/address.sw:
Identity、Address的构造与比较方式。
八、总结
本文完整还原了 Sway 官方contract-ownership示例:通过Option<Identity>存储所有者、以msg_sender()获取调用者身份、用assert强制权限校验,实现了set_owner(设置/转移所有权)与action(仅所有者可执行)两个核心函数。结合 sway-lib-std/src/auth.sw 的源码可以看到,msg_sender()在底层依赖 FuelVM 的gm元数据指令与输入所有者一致性检查,为访问控制提供了可靠的运行时依据。这套"所有权 + 调用者校验"的模式是 Sway 合约实现管理员功能、DAO 治理与合约自托管的基础设施,可直接复用到生产合约中。
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考