资金管理与时间锁
概述
这四章围绕一个核心主题:如何安全、公平地管理链上资金和权限。
| 章节 | 合约 | 解决的问题 |
|---|---|---|
| 12 | PaymentSplit | 多人按比例分钱 |
| 13 | TokenVesting | 代币线性释放(防一次性抛售) |
| 14 | TokenLocker | 代币锁定到期才能取 |
| 15 | TimeLock | 管理员操作必须延时执行(防作恶) |
第 12 章:分账合约(PaymentSplit)
应用场景
一个 NFT 项目有 3 个合伙人,销售收入需要按约定比例分配:
- A 拿 50%
- B 拿 30%
- C 拿 20%
传统做法需要某人手动转账,容易赖账。分账合约实现自动化、透明化的收益分配。
核心设计思路
┌──────────────────────┐
用户/项目收入 ──▶│ PaymentSplit 合约 │
│ (暂存所有 ETH) │
└──────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
受益人A (50%) 受益人B (30%) 受益人C (20%)
主动调用 withdraw 主动调用 withdraw 主动调用 withdraw
关键点:采用”拉”模式(受益人主动提款),而非”推”模式(自动转账),避免恶意合约攻击。
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PaymentSplit {
event PaeeAdded(address indexed account, uint256 shares);
event PaymentRelease(address indexed to, uint256 amount);
event PaymentReceived(address indexed from, uint256 amount);
address public owner;
uint256 public totalShares; // 所有受益人的总份额
uint256 public totalReleased; // 合约已转出的总金额
address[] public payees; // 受益人列表
mapping(address => uint256) public sharesMap; // 地址 → 份额
mapping(address => uint256) public releaseMap; // 地址 → 已提取金额
constructor() {
owner = msg.sender;
}
/// @notice 添加受益人(只有 owner 可调用)
function addPayee(address payee, uint256 shares) public OnlyOwner {
require(payee != address(0), "Invalid Address");
require(shares > 0, "Invalid Shares");
// 检查是否重复添加
for (uint i = 0; i < payees.length; i++) {
require(payee != payees[i], "Payee exists");
}
payees.push(payee);
sharesMap[payee] = shares;
totalShares += shares;
emit PaeeAdded(payee, shares);
}
/// @notice 计算受益人的理论总收益
/// 公式: (合约总流入 × 个人份额) ÷ 总份额
function getAllBalance(address payee) public view returns (uint256) {
uint256 shares = sharesMap[payee];
// 合约总流入 = 当前余额 + 已经被提走的
return ((address(this).balance + totalReleased) * shares) / totalShares;
}
/// @notice 计算当前可提取金额
/// 公式: 理论总收益 - 已提取金额
function getAvaliableBalance(address payee) public view returns (uint256) {
return getAllBalance(payee) - releaseMap[payee];
}
/// @notice 受益人提款
function withdraw(uint256 amount) public {
require(sharesMap[msg.sender] > 0, "Not a payee");
uint256 available = getAvaliableBalance(msg.sender);
require(available >= amount, "Insufficient balance");
totalReleased += amount;
releaseMap[msg.sender] += amount;
payable(msg.sender).transfer(amount);
emit PaymentRelease(msg.sender, amount);
}
receive() external payable {
emit PaymentReceived(msg.sender, msg.value);
}
}
计算示例
假设 A 份额=50,B 份额=30,C 份额=20,总份额=100。合约收到 10 ETH:
| 受益人 | 份额 | 理论收益 | 已提取 | 可提取 |
|---|---|---|---|---|
| A | 50 | 5 ETH | 0 | 5 ETH |
| B | 30 | 3 ETH | 0 | 3 ETH |
| C | 20 | 2 ETH | 0 | 2 ETH |
A 提取 3 ETH 后,合约又收到 6 ETH(总流入=16 ETH):
| 受益人 | 理论收益 | 已提取 | 可提取 |
|---|---|---|---|
| A | 8 ETH | 3 ETH | 5 ETH |
| B | 4.8 ETH | 0 | 4.8 ETH |
| C | 3.2 ETH | 0 | 3.2 ETH |
第 13 章:代币归属(TokenVesting)
应用场景
项目方给早期投资者分配了 100 万代币,但不希望投资者一拿到就全部抛售砸盘。于是设定:
- 在 12 个月内线性释放
- 每过 1 个月,可领取总量的 1/12
线性释放公式
可释放总量 = 总代币量 × (已过时间 / 归属总时长)
当前可领取 = 可释放总量 - 已领取数量
时间线示意:
部署时(start) 6个月后 12个月后(start+duration)
| | |
|───── 50% 释放 ──▶|───── 100% 释放 ──▶|
| 可领 50 万 | 可领全部 100 万 |
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC20.sol";
contract TokenVesting {
event ERC20Released(address indexed token, uint256 amount);
mapping(address => uint256) erc20Released; // token地址 → 已释放数量
address public immutable beneficiary; // 受益人
uint256 public immutable start; // 开始时间
uint256 public immutable duration; // 归属期总时长
constructor(address _beneficiary, uint256 _duration) {
require(_beneficiary != address(0), "Invalid address");
beneficiary = _beneficiary;
start = block.timestamp;
duration = _duration;
}
/// @notice 受益人领取已释放的代币
function release(address token) public {
// 计算当前应释放的总量 - 已经领取的 = 本次可领取
uint256 releaseable = vestedAmount(token, block.timestamp)
- erc20Released[token];
erc20Released[token] += releaseable;
emit ERC20Released(token, releaseable);
IERC20(token).transfer(beneficiary, releaseable);
}
/// @notice 计算到 timestamp 时刻,应释放的代币总量
function vestedAmount(
address token,
uint256 timestamp
) public view returns (uint256) {
// 合约持有的代币 + 已释放的 = 代币总量
uint256 totalAllocation = IERC20(token).balanceOf(address(this))
+ erc20Released[token];
if (timestamp < start) {
return 0; // 还没开始
} else if (timestamp >= start + duration) {
return totalAllocation; // 已过归属期,全部释放
} else {
// 线性释放:总量 × (已过时间 / 总时长)
return (totalAllocation * (timestamp - start)) / duration;
}
}
}
使用流程
- 部署 TokenVesting 合约,指定受益人和归属期(如 365 天)
- 项目方将代币转入 TokenVesting 合约
- 受益人可以随时调用
release()领取已释放的部分 - 归属期结束后,可一次性领完剩余代币
第 14 章:代币锁(TokenLocker)
应用场景
与 TokenVesting 不同,代币锁是全有或全无:
- 锁定期内:一分钱都不能取
- 锁定期后:可以一次性全部取出
常见于:LP Token 锁定(证明项目方不会跑路)、团队代币锁定等。
时间线对比
TokenVesting(线性释放):
|████████████████████████████| 逐步释放
0% 100%
TokenLocker(锁定后一次释放):
|░░░░░░░░░░░░░░░░░░|████████| 锁定期内不可取,到期全部释放
锁定中 可取
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC20.sol";
contract TokenLocker {
event TokenLockStart(
address indexed beneficiary,
address indexed token,
uint256 startTime,
uint256 lockTime
);
event Release(
address indexed beneficiary,
address indexed token,
uint256 releaseTime,
uint256 releaseAmount
);
IERC20 public immutable token; // 锁定的代币合约
address public immutable beneficiary; // 受益人
uint256 public immutable lockTime; // 锁定时长(秒)
uint256 public immutable startTime; // 开始时间
constructor(IERC20 _token, address _beneficiary, uint256 _lockTime) {
require(_beneficiary != address(0), "Invalid address");
require(_lockTime > 0, "Invalid lock time");
token = _token;
beneficiary = _beneficiary;
lockTime = _lockTime;
startTime = block.timestamp;
emit TokenLockStart(_beneficiary, address(_token), block.timestamp, _lockTime);
}
/// @notice 锁定期结束后,受益人提取全部代币
function release() public {
// 核心:当前时间必须超过 startTime + lockTime
require(
block.timestamp >= startTime + lockTime,
"Token is currently locked!"
);
uint256 balance = token.balanceOf(address(this));
require(balance > 0, "Locker is empty");
token.transfer(beneficiary, balance);
emit Release(beneficiary, address(token), block.timestamp, balance);
}
}
使用流程
- 部署 TokenLocker,指定代币地址、受益人、锁定时长(如 180 天)
- 将代币转入 TokenLocker 合约
- 锁定期内任何人调用
release()都会失败 - 锁定期结束后,调用
release()将所有代币转给受益人
第 15 章:时间锁(TimeLock)
应用场景
DeFi 项目的管理员权力很大(可以修改参数、升级合约)。为了防止管理员作恶:
- 所有管理操作必须提前公开排队
- 等待一段时间(如 2 天)后才能执行
- 等待期间社区可以审查,管理员也可以取消
Compound、Uniswap 等知名项目都使用了时间锁。
核心流程(三步走)
时间线:
排队时刻(now) executeTime executeTime + 7天
| | |
|──── delay 锁定 ────▶|───── 可执行窗口 ──────────▶|── 过期作废
步骤:
1. queueTransaction() → 排队(公开交易内容)
2. 等待 delay 时间 → 社区审查窗口
3. executeTransaction() → 执行(或 cancelTransaction 取消)
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TimeLock {
event QueueTransaction(bytes32 indexed txHash, address indexed target,
uint256 value, string signature, bytes data, uint256 executeTime);
event ExecuteTransaction(bytes32 indexed txHash, address indexed target,
uint256 value, string signature, bytes data, uint256 executeTime);
event CancelTransaction(bytes32 indexed txHash, address indexed target,
uint256 value, string signature, bytes data, uint256 executeTime);
event ChangeAdmin(address indexed newAdmin);
address public admin;
uint256 public constant GRACE_PERIOD = 7 days; // 宽限期:7天
uint256 public delay; // 锁定延迟
mapping(bytes32 => bool) public queuedTransactions; // 交易队列
modifier onlyOwner() {
require(msg.sender == admin, "Not admin");
_;
}
modifier onlyTimeLock() {
require(msg.sender == address(this), "Not TimeLock");
_;
}
constructor(uint256 _delay) {
admin = msg.sender;
delay = _delay;
}
/// @notice 修改管理员(必须通过时间锁流程)
function changeAdmin(address newAdmin) public onlyTimeLock {
admin = newAdmin;
emit ChangeAdmin(newAdmin);
}
/// @notice 第1步:创建交易并排队
function queueTransaction(
address target,
uint256 value,
string memory signature,
bytes memory data,
uint256 executeTime
) public onlyOwner returns (bytes32) {
// 执行时间必须在 当前时间+delay 之后
require(executeTime >= block.timestamp + delay,
"Execute time too early");
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
queuedTransactions[txHash] = true;
emit QueueTransaction(txHash, target, value, signature, data, executeTime);
return txHash;
}
/// @notice 第2步(可选):取消已排队的交易
function cancelTransaction(
address target,
uint256 value,
string memory signature,
bytes memory data,
uint256 executeTime
) public onlyOwner {
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
require(queuedTransactions[txHash], "Not queued");
queuedTransactions[txHash] = false;
emit CancelTransaction(txHash, target, value, signature, data, executeTime);
}
/// @notice 第3步:执行交易(锁定期过后、宽限期内)
function executeTransaction(
address target,
uint256 value,
string memory signature,
bytes memory data,
uint256 executeTime
) public payable onlyOwner returns (bytes memory) {
bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
// 检查:必须在队列中
require(queuedTransactions[txHash], "Not queued");
// 检查:锁定期已过
require(block.timestamp >= executeTime, "Too early");
// 检查:未超过宽限期
require(block.timestamp <= executeTime + GRACE_PERIOD, "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, "Execution reverted");
emit ExecuteTransaction(txHash, target, value, signature, data, executeTime);
return returnData;
}
/// @notice 计算交易的唯一哈希
function getTxHash(
address target, uint256 value, string memory signature,
bytes memory data, uint256 executeTime
) public pure returns (bytes32) {
return keccak256(abi.encode(target, value, signature, data, executeTime));
}
}
实操示例:通过时间锁修改管理员
假设 delay = 2 days,当前时间 = 1000
1. 排队:
queueTransaction(
target: TimeLock合约地址,
value: 0,
signature: "changeAdmin(address)",
data: abi.encode(新管理员地址),
executeTime: 1000 + 2 days = 173800
)
2. 等待 2 天...(社区可以审查这笔交易)
3. 执行(在 173800 ~ 173800+7days 之间):
executeTransaction(相同参数)
→ TimeLock.call("changeAdmin(新地址)")
→ 因为 msg.sender == address(this),通过 onlyTimeLock 检查
→ admin 被更新
总结对比
| 特性 | PaymentSplit | TokenVesting | TokenLocker | TimeLock |
|---|---|---|---|---|
| 目的 | 按比例分 ETH | 线性释放代币 | 到期解锁代币 | 延迟执行操作 |
| 资金类型 | ETH | ERC20 | ERC20 | 任意调用 |
| 释放方式 | 按份额随时可提 | 线性逐步释放 | 全有或全无 | 不涉及释放 |
| 受益人 | 多个 | 单个 | 单个 | N/A |
| 核心安全机制 | 份额不可篡改 | 时间决定释放量 | 时间锁定 | 社区审查窗口 |
| 典型应用 | 项目收入分成 | 投资者/团队锁仓 | LP锁/团队锁 | DAO治理 |