返回 SC 笔记
SC Day 24

Solidity Timelock + 多签逻辑 + 代理模式存储槽原理

### 一、Timelock — 时间锁

2026-04-24
第一阶段:基础构建 (Day 21-24)
SolidityTimelock多签代理模式EIP-1967DeFi治理

日期: 2026-04-24 方向: Solidity 阶段: 第一阶段:基础构建 (Day 21-24) 标签: #Solidity #Timelock #多签 #代理模式 #EIP-1967 #DeFi治理


今日目标

  1. 理解 Timelock 的设计原理和在 DeFi 治理中的作用
  2. 掌握多签钱包的核心逻辑
  3. 深入理解代理模式的存储槽原理(EIP-1967)
  4. 用内联汇编操作存储槽
  5. 编写完整的 Timelock 合约

核心概念

一、Timelock — 时间锁

1.1 为什么 DeFi 需要 Timelock?

在传统公司里,CEO 可以直接做决策。但在 DeFi 协议中,管理员权限过大是巨大的安全风险:

风险场景无 Timelock有 Timelock
管理员私钥泄露攻击者立刻修改合约参数、转走资金社区有时间发现异常,取消交易
管理员作恶偷偷修改费率、增发代币所有操作提前公示,社区可审查
紧急漏洞无法区分正常操作和恶意操作紧急操作需要多签 + 短时间锁

Timelock 的核心逻辑

1. Queue(排队): 将操作提交到队列
2. Wait(等待): 必须等待 delay 时间 (如 24-48 小时)
3. Execute(执行): delay 过后才能执行
4. Cancel(取消): 在执行前可以取消

时间线:
|---queue---|---delay(24h)---|---grace period(14d)---|---expired---|

1.2 主流 DeFi 协议的 Timelock 配置

协议Timelock Delay管理权限
Compound48 小时Governance (COMP holders)
Uniswap2 天 (最小)Governance (UNI holders)
Aave24 小时 (短) / 10 天 (长)Guardian + Governance
MakerDAO48 小时Executive vote

二、多签逻辑 (Multi-Signature)

多签钱包要求 M-of-N 个签名者确认才能执行交易。

3-of-5 多签示例:
  签名者: [Alice, Bob, Carol, Dave, Eve]
  阈值: 3

  Alice 提交交易 → 确认数: 1/3
  Bob 确认 → 确认数: 2/3
  Carol 确认 → 确认数: 3/3 ✓ → 执行!

核心数据结构

struct Transaction {
    address to;           // 目标地址
    uint256 value;        // ETH 数量
    bytes data;           // calldata
    bool executed;        // 是否已执行
    uint256 confirmations; // 确认数
}

mapping(uint256 => mapping(address => bool)) public isConfirmed;
// txId => signer => hasConfirmed

三、代理模式存储槽原理 (EIP-1967)

3.1 问题:存储冲突

代理模式使用 delegatecall,implementation 的代码在 proxy 的存储上下文中执行。如果 proxy 自己也有状态变量,可能和 implementation 的变量在同一个 slot 上产生冲突。

// Proxy 合约
contract Proxy {
    address public implementation; // slot 0
    address public admin;          // slot 1
}

// Implementation 合约
contract TokenV1 {
    uint256 public totalSupply;    // slot 0 — 和 implementation 地址冲突!
    mapping(address => uint256) public balances; // slot 1 — 和 admin 冲突!
}

当通过 Proxy delegatecall TokenV1 的 totalSupply() 时,实际读取的是 Proxy 的 slot 0(implementation 地址),而不是真正的 totalSupply。

3.2 解决方案:EIP-1967 标准存储槽

EIP-1967 定义了固定的、不太可能冲突的存储槽位置:

// Implementation 存储槽
bytes32 constant IMPLEMENTATION_SLOT =
    bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
// = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

// Admin 存储槽
bytes32 constant ADMIN_SLOT =
    bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);
// = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

// Beacon 存储槽
bytes32 constant BEACON_SLOT =
    bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1);

为什么 -1 防止有人故意构造出相同的 slot。keccak256 的结果减 1 后,没有已知的原像能产生这个值。

3.3 用汇编操作存储槽

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

contract StorageSlotDemo {
    // EIP-1967 标准槽
    bytes32 private constant IMPL_SLOT =
        bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);

    bytes32 private constant ADMIN_SLOT =
        bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);

    /// @notice 读取 implementation 地址
    function _getImplementation() internal view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly {
            impl := sload(slot)
        }
    }

    /// @notice 设置 implementation 地址
    function _setImplementation(address newImpl) internal {
        require(newImpl.code.length > 0, "Not a contract");
        bytes32 slot = IMPL_SLOT;
        assembly {
            sstore(slot, newImpl)
        }
    }

    /// @notice 读取 admin 地址
    function _getAdmin() internal view returns (address admin) {
        bytes32 slot = ADMIN_SLOT;
        assembly {
            admin := sload(slot)
        }
    }

    /// @notice 设置 admin 地址
    function _setAdmin(address newAdmin) internal {
        bytes32 slot = ADMIN_SLOT;
        assembly {
            sstore(slot, newAdmin)
        }
    }
}

3.4 存储槽可视化

Solidity 存储布局 (每个 slot = 32 bytes):

Proxy 合约存储:
┌──────────┬──────────────────────────────────┐
│ Slot 0   │ (空/留给 Implementation 用)       │
│ Slot 1   │ (空/留给 Implementation 用)       │
│ ...      │ (Implementation 的存储区域)       │
│ Slot N   │ (Implementation 的存储区域)       │
│ ...      │                                  │
│ EIP-1967 │                                  │
│ IMPL_SLOT│ Implementation 合约地址           │ ← 0x3608...
│ ADM_SLOT │ Admin 地址                        │ ← 0xb531...
└──────────┴──────────────────────────────────┘

因为 EIP-1967 的 slot 值极大 (接近 2^256),
和正常的 slot 0, 1, 2... 不会冲突。

代码实战

完整 Timelock 合约

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

/**
 * @title SimpleTimelock
 * @notice DeFi 治理时间锁合约
 * @dev 所有敏感操作必须先排队,等待 delay 后才能执行
 */
contract SimpleTimelock {
    // ============ 常量 ============

    uint256 public constant MINIMUM_DELAY = 1 hours;
    uint256 public constant MAXIMUM_DELAY = 30 days;
    uint256 public constant GRACE_PERIOD = 14 days;

    // ============ 状态变量 ============

    address public admin;
    address public pendingAdmin;
    uint256 public delay;

    // txId => 是否在队列中
    mapping(bytes32 => bool) public queuedTransactions;

    // ============ 事件 ============

    event NewAdmin(address indexed newAdmin);
    event NewDelay(uint256 indexed newDelay);
    event QueueTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint256 value,
        string signature,
        bytes data,
        uint256 eta  // Estimated Time of Arrival
    );
    event ExecuteTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint256 value,
        string signature,
        bytes data,
        uint256 eta
    );
    event CancelTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint256 value,
        string signature,
        bytes data,
        uint256 eta
    );

    // ============ 修饰符 ============

    modifier onlyAdmin() {
        require(msg.sender == admin, "Timelock: caller is not admin");
        _;
    }

    modifier onlyTimelock() {
        require(msg.sender == address(this), "Timelock: call must come from Timelock");
        _;
    }

    // ============ 构造函数 ============

    constructor(address admin_, uint256 delay_) {
        require(delay_ >= MINIMUM_DELAY, "Delay too short");
        require(delay_ <= MAXIMUM_DELAY, "Delay too long");

        admin = admin_;
        delay = delay_;
    }

    // ============ 管理函数 (只能通过 Timelock 自己调用) ============

    /// @notice 修改 delay — 必须通过排队执行
    function setDelay(uint256 newDelay) external onlyTimelock {
        require(newDelay >= MINIMUM_DELAY && newDelay <= MAXIMUM_DELAY, "Invalid delay");
        delay = newDelay;
        emit NewDelay(newDelay);
    }

    /// @notice 接受新 admin
    function acceptAdmin() external {
        require(msg.sender == pendingAdmin, "Not pending admin");
        admin = msg.sender;
        pendingAdmin = address(0);
        emit NewAdmin(msg.sender);
    }

    /// @notice 设置 pending admin — 必须通过排队执行
    function setPendingAdmin(address newPendingAdmin) external onlyTimelock {
        pendingAdmin = newPendingAdmin;
    }

    // ============ 核心函数 ============

    /**
     * @notice 将交易加入队列
     * @param target 目标合约地址
     * @param value 发送的 ETH 数量
     * @param signature 函数签名 (如 "transfer(address,uint256)")
     * @param data 编码后的参数
     * @param eta 预计执行时间 (必须 >= block.timestamp + delay)
     */
    function queueTransaction(
        address target,
        uint256 value,
        string calldata signature,
        bytes calldata data,
        uint256 eta
    ) external onlyAdmin returns (bytes32 txHash) {
        require(eta >= block.timestamp + delay, "ETA too soon");

        txHash = _getTxHash(target, value, signature, data, eta);
        require(!queuedTransactions[txHash], "Already queued");

        queuedTransactions[txHash] = true;
        emit QueueTransaction(txHash, target, value, signature, data, eta);
    }

    /**
     * @notice 执行已到期的队列交易
     */
    function executeTransaction(
        address target,
        uint256 value,
        string calldata signature,
        bytes calldata data,
        uint256 eta
    ) external payable onlyAdmin returns (bytes memory) {
        bytes32 txHash = _getTxHash(target, value, signature, data, eta);

        require(queuedTransactions[txHash], "Not queued");
        require(block.timestamp >= eta, "Not yet ready");
        require(block.timestamp <= eta + GRACE_PERIOD, "Transaction expired");

        queuedTransactions[txHash] = false;

        // 构造 calldata
        bytes memory callData;
        if (bytes(signature).length == 0) {
            callData = data;
        } else {
            callData = abi.encodePacked(
                bytes4(keccak256(bytes(signature))),
                data
            );
        }

        // 执行调用
        (bool success, bytes memory returnData) = target.call{value: value}(callData);
        require(success, "Transaction execution reverted");

        emit ExecuteTransaction(txHash, target, value, signature, data, eta);
        return returnData;
    }

    /**
     * @notice 取消队列中的交易
     */
    function cancelTransaction(
        address target,
        uint256 value,
        string calldata signature,
        bytes calldata data,
        uint256 eta
    ) external onlyAdmin {
        bytes32 txHash = _getTxHash(target, value, signature, data, eta);
        require(queuedTransactions[txHash], "Not queued");

        queuedTransactions[txHash] = false;
        emit CancelTransaction(txHash, target, value, signature, data, eta);
    }

    // ============ 内部函数 ============

    function _getTxHash(
        address target,
        uint256 value,
        string calldata signature,
        bytes calldata data,
        uint256 eta
    ) internal pure returns (bytes32) {
        return keccak256(abi.encode(target, value, signature, data, eta));
    }

    // ============ 接收 ETH ============

    receive() external payable {}
}

简化多签逻辑

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

/**
 * @title SimpleMultiSig
 * @notice M-of-N 多签钱包核心逻辑
 */
contract SimpleMultiSig {
    // ============ 状态 ============

    struct Transaction {
        address to;
        uint256 value;
        bytes data;
        bool executed;
        uint256 numConfirmations;
    }

    address[] public owners;
    mapping(address => bool) public isOwner;
    uint256 public required; // 最小确认数

    Transaction[] public transactions;
    // txIndex => owner => confirmed
    mapping(uint256 => mapping(address => bool)) public isConfirmed;

    // ============ 事件 ============

    event SubmitTransaction(uint256 indexed txIndex, address indexed owner, address to, uint256 value);
    event ConfirmTransaction(uint256 indexed txIndex, address indexed owner);
    event RevokeConfirmation(uint256 indexed txIndex, address indexed owner);
    event ExecuteTransaction(uint256 indexed txIndex, address indexed owner);

    // ============ 修饰符 ============

    modifier onlyOwner() {
        require(isOwner[msg.sender], "Not owner");
        _;
    }

    modifier txExists(uint256 txIndex) {
        require(txIndex < transactions.length, "TX does not exist");
        _;
    }

    modifier notExecuted(uint256 txIndex) {
        require(!transactions[txIndex].executed, "Already executed");
        _;
    }

    modifier notConfirmed(uint256 txIndex) {
        require(!isConfirmed[txIndex][msg.sender], "Already confirmed");
        _;
    }

    // ============ 构造 ============

    constructor(address[] memory _owners, uint256 _required) {
        require(_owners.length > 0, "Need owners");
        require(_required > 0 && _required <= _owners.length, "Invalid required");

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(owner != address(0), "Zero address");
            require(!isOwner[owner], "Duplicate owner");
            isOwner[owner] = true;
            owners.push(owner);
        }
        required = _required;
    }

    // ============ 核心函数 ============

    /// @notice 提交交易提案
    function submitTransaction(address to, uint256 value, bytes calldata data)
        external onlyOwner returns (uint256 txIndex)
    {
        txIndex = transactions.length;
        transactions.push(Transaction({
            to: to,
            value: value,
            data: data,
            executed: false,
            numConfirmations: 0
        }));
        emit SubmitTransaction(txIndex, msg.sender, to, value);
    }

    /// @notice 确认交易
    function confirmTransaction(uint256 txIndex)
        external onlyOwner txExists(txIndex) notExecuted(txIndex) notConfirmed(txIndex)
    {
        Transaction storage tx_ = transactions[txIndex];
        tx_.numConfirmations += 1;
        isConfirmed[txIndex][msg.sender] = true;
        emit ConfirmTransaction(txIndex, msg.sender);
    }

    /// @notice 执行交易 (确认数 >= required)
    function executeTransaction(uint256 txIndex)
        external onlyOwner txExists(txIndex) notExecuted(txIndex)
    {
        Transaction storage tx_ = transactions[txIndex];
        require(tx_.numConfirmations >= required, "Not enough confirmations");

        tx_.executed = true;
        (bool success, ) = tx_.to.call{value: tx_.value}(tx_.data);
        require(success, "TX failed");

        emit ExecuteTransaction(txIndex, msg.sender);
    }

    /// @notice 撤回确认
    function revokeConfirmation(uint256 txIndex)
        external onlyOwner txExists(txIndex) notExecuted(txIndex)
    {
        require(isConfirmed[txIndex][msg.sender], "Not confirmed");
        Transaction storage tx_ = transactions[txIndex];
        tx_.numConfirmations -= 1;
        isConfirmed[txIndex][msg.sender] = false;
        emit RevokeConfirmation(txIndex, msg.sender);
    }

    // ============ 查询 ============

    function getTransactionCount() external view returns (uint256) {
        return transactions.length;
    }

    receive() external payable {}
}

EIP-1967 代理合约骨架

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

/**
 * @title EIP1967Proxy
 * @notice 最小化代理合约,展示存储槽原理
 */
contract EIP1967Proxy {
    // EIP-1967 存储槽
    bytes32 private constant IMPLEMENTATION_SLOT =
        bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);

    bytes32 private constant ADMIN_SLOT =
        bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);

    constructor(address implementation_, address admin_) {
        _setImplementation(implementation_);
        _setAdmin(admin_);
    }

    // ============ 存储槽操作 (汇编) ============

    function _getImplementation() internal view returns (address impl) {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            impl := sload(slot)
        }
    }

    function _setImplementation(address impl) internal {
        require(impl.code.length > 0, "Not a contract");
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            sstore(slot, impl)
        }
    }

    function _getAdmin() internal view returns (address adm) {
        bytes32 slot = ADMIN_SLOT;
        assembly {
            adm := sload(slot)
        }
    }

    function _setAdmin(address adm) internal {
        bytes32 slot = ADMIN_SLOT;
        assembly {
            sstore(slot, adm)
        }
    }

    // ============ 升级函数 ============

    function upgradeTo(address newImplementation) external {
        require(msg.sender == _getAdmin(), "Not admin");
        _setImplementation(newImplementation);
    }

    // ============ Fallback: 所有未知调用转发给 Implementation ============

    fallback() external payable {
        address impl = _getImplementation();
        assembly {
            // 复制 calldata
            calldatacopy(0, 0, calldatasize())
            // delegatecall 到 implementation
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            // 复制返回数据
            returndatacopy(0, 0, returndatasize())
            // 根据结果返回或回滚
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    receive() external payable {}
}

关键要点总结

Timelock 设计要点

要素说明
最小 delay通常 24-48 小时,给社区审查时间
Grace period执行窗口期,过期作废
Admin 权限通常由治理合约(Governor)担任
Cancel 权限紧急情况下的安全阀
tx hash用参数哈希作为唯一标识

多签 vs Timelock 对比

维度多签Timelock
保护方式需要多人同意需要等待时间
适用场景资金管理、紧急操作治理操作、参数修改
组合使用Gnosis Safe (多签)Compound Timelock
最佳实践多签 + Timelock 组合Governor -> Timelock -> Protocol

EIP-1967 存储槽

IMPLEMENTATION_SLOT = keccak256("eip1967.proxy.implementation") - 1
ADMIN_SLOT          = keccak256("eip1967.proxy.admin") - 1
BEACON_SLOT         = keccak256("eip1967.proxy.beacon") - 1

常见误区

  1. Timelock 的 delay 设得太短: 失去安全意义。主流协议至少 24 小时
  2. 多签的 required 等于 owners 数: 任何一个 owner 丢失密钥就无法执行任何操作
  3. 代理合约的 constructor 初始化: 代理合约不能在 constructor 初始化 implementation 的状态,必须用 initialize() 函数
  4. 忘记 initializer 修饰符: implementation 的 initialize() 可能被多次调用
  5. 存储槽冲突: 不遵循 EIP-1967 标准,自定义 slot 位置和 implementation 冲突

面试关联

面试题本课关联
"为什么 DeFi 协议需要 Timelock?"安全审查窗口 + 去中心化治理
"多签钱包如何工作?"M-of-N 确认 + 交易提案/确认/执行
"代理模式如何避免存储冲突?"EIP-1967 标准存储槽
"UUPS 和 Transparent Proxy 的区别?"升级逻辑位置不同
"Compound 的治理流程是怎样的?"Propose -> Vote -> Queue(Timelock) -> Execute
"如何设计紧急暂停机制?"多签 Guardian + 短 delay Timelock

参考资源