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治理
今日目标
- 理解 Timelock 的设计原理和在 DeFi 治理中的作用
- 掌握多签钱包的核心逻辑
- 深入理解代理模式的存储槽原理(EIP-1967)
- 用内联汇编操作存储槽
- 编写完整的 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 | 管理权限 |
|---|---|---|
| Compound | 48 小时 | Governance (COMP holders) |
| Uniswap | 2 天 (最小) | Governance (UNI holders) |
| Aave | 24 小时 (短) / 10 天 (长) | Guardian + Governance |
| MakerDAO | 48 小时 | 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
常见误区
- Timelock 的 delay 设得太短: 失去安全意义。主流协议至少 24 小时
- 多签的 required 等于 owners 数: 任何一个 owner 丢失密钥就无法执行任何操作
- 代理合约的 constructor 初始化: 代理合约不能在 constructor 初始化 implementation 的状态,必须用
initialize()函数 - 忘记
initializer修饰符: implementation 的initialize()可能被多次调用 - 存储槽冲突: 不遵循 EIP-1967 标准,自定义 slot 位置和 implementation 冲突
面试关联
| 面试题 | 本课关联 |
|---|---|
| "为什么 DeFi 协议需要 Timelock?" | 安全审查窗口 + 去中心化治理 |
| "多签钱包如何工作?" | M-of-N 确认 + 交易提案/确认/执行 |
| "代理模式如何避免存储冲突?" | EIP-1967 标准存储槽 |
| "UUPS 和 Transparent Proxy 的区别?" | 升级逻辑位置不同 |
| "Compound 的治理流程是怎样的?" | Propose -> Vote -> Queue(Timelock) -> Execute |
| "如何设计紧急暂停机制?" | 多签 Guardian + 短 delay Timelock |