资金管理与时间锁

资金管理与时间锁

资金管理与时间锁

概述

这四章围绕一个核心主题:如何安全、公平地管理链上资金和权限

章节合约解决的问题
12PaymentSplit多人按比例分钱
13TokenVesting代币线性释放(防一次性抛售)
14TokenLocker代币锁定到期才能取
15TimeLock管理员操作必须延时执行(防作恶)

第 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:

受益人份额理论收益已提取可提取
A505 ETH05 ETH
B303 ETH03 ETH
C202 ETH02 ETH

A 提取 3 ETH 后,合约又收到 6 ETH(总流入=16 ETH):

受益人理论收益已提取可提取
A8 ETH3 ETH5 ETH
B4.8 ETH04.8 ETH
C3.2 ETH03.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;
        }
    }
}

使用流程

  1. 部署 TokenVesting 合约,指定受益人和归属期(如 365 天)
  2. 项目方将代币转入 TokenVesting 合约
  3. 受益人可以随时调用 release() 领取已释放的部分
  4. 归属期结束后,可一次性领完剩余代币

第 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);
    }
}

使用流程

  1. 部署 TokenLocker,指定代币地址、受益人、锁定时长(如 180 天)
  2. 将代币转入 TokenLocker 合约
  3. 锁定期内任何人调用 release() 都会失败
  4. 锁定期结束后,调用 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 被更新

总结对比

特性PaymentSplitTokenVestingTokenLockerTimeLock
目的按比例分 ETH线性释放代币到期解锁代币延迟执行操作
资金类型ETHERC20ERC20任意调用
释放方式按份额随时可提线性逐步释放全有或全无不涉及释放
受益人多个单个单个N/A
核心安全机制份额不可篡改时间决定释放量时间锁定社区审查窗口
典型应用项目收入分成投资者/团队锁仓LP锁/团队锁DAO治理