Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

权限控制进阶

权限判断需要同时明确调用者身份、执行地址、状态归属和操作生命周期。代理委托、签名转发与瞬态存储不会自动提供业务鉴权。基础权限模式见权限控制。

调用上下文与 tx.origin

普通 CALL 中,msg.sender 是直接调用者;DELEGATECALL 保留当前帧的 msg.sender 和 msg.value,并在调用者的地址及存储上下文中执行。tx.origin 是顶层交易的发送账户,在嵌套调用中不变。

若特权 EOA 先调用攻击合约,攻击合约再调用受害合约,受害合约中的 tx.origin 仍是特权 EOA,但 msg.sender 为攻击合约。只检查 tx.origin 会把未经授权的下游调用视为合法操作。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract OriginWallet {
    address public immutable owner;

    constructor() payable {
        owner = msg.sender;
    }

    function transferTo(address payable to, uint256 amount) external {
        require(tx.origin == owner, "Unauthorized"); // 存在钓鱼授权风险
        (bool success, ) = to.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

contract OriginForwarder {
    OriginWallet public immutable wallet;

    constructor(OriginWallet target) {
        wallet = target;
    }

    function trigger() external {
        wallet.transferTo(payable(address(this)), address(wallet).balance);
    }

    receive() external payable {}
}

当 owner 调用 OriginForwarder.trigger(),钱包的校验通过,资金转入转发合约。直接权限操作应验证相应调用主体;元交易则需要可信转发器或经过验证的签名来确定授权者,不能把任意传入的地址当成身份。

EOA 与签名授权

EIP-7702 允许 EOA 委托执行代码,账户仍保留私钥。tx.origin == msg.sender 或代码长度检查不能完整界定账户能力。ERC-4337 的账户验证由具体实现决定,也不意味着所有账户都使用同一种签名算法。

消息签名授权需要绑定操作参数、域、nonce 和有效期等业务约束。ERC-1271 表达合约账户的签名验证接口;验证返回值并不能替代转账接收者、执行者或额度的业务检查。

代理初始化的权限归属

实现的构造函数仅初始化实现自己的状态。代理 owner 和角色需要通过代理上下文的初始化建立。未初始化代理可能被抢先设置权限;直接初始化实现只改变实现自身,二者是不同攻击面。

OpenZeppelin 5.4.0 的 UUPS 实现使用 _disableInitializers() 锁定实现初始化,使用 onlyProxy 约束升级调用上下文,并由 _authorizeUpgrade 判断权限。代理部署附带初始化数据可避免独立初始化交易之间的抢占窗口。

旧版本未初始化实现与委托自毁的风险见代理进阶。msg.sender == address(this) 只允许自调用,不能作为通用代理升级保护。

默认值与隐式授权

Solidity mapping 中未写入的键返回默认零值。如果零值同时被解释为有效根、有效角色或已验证状态,未经证明的输入可能通过判断。

约束是让缺省状态与有效授权保持可区分,并验证完整的状态转换;并非禁止所有零地址、零哈希或零数值。Nomad 默认根问题属于这类状态语义冲突。

瞬态存储的权限边界

EIP-1153 的瞬态存储在交易结束时清除,同一合约上下文的多个调用帧可访问同一份瞬态状态。不同合约不能仅靠相同槽号读写彼此的瞬态存储。

普通调用使用被调用合约的瞬态空间,委托调用使用调用者的瞬态空间。调用帧回滚也会回滚该帧及其内部调用产生的瞬态修改。STATICCALL 上下文允许 TLOAD,禁止 TSTORE。

若公开入口能修改费率、授权标记或回调预期值,后续函数又将其视为可信状态,攻击者可能在同一交易内伪造执行条件。风险取决于可达的写入路径与业务含义,不能由“交易结束自动清除”消除。

重入锁一般在操作退出时清除,允许同一交易的后续合法调用。持续整笔交易的结算标记则可保留到交易结束。清理策略应对应生命周期,鉴权条件也不必一律是管理员角色修饰器。

权限检查维度

  • 确认每个入口使用的身份来源及转发器信任关系。
  • 分别验证代理初始化、实现锁定、升级授权与新版本迁移。
  • 确认默认状态不会隐式进入有效授权集合。
  • 对回调绑定调用者、预期参数和操作阶段,而非只检查槽位是否非零。
  • 检查委托执行模块的瞬态槽和持久槽是否存在业务冲突。

参考资料