代币锁仓 = 项目生命线?一招杜绝早期抛售砸盘

43 阅读7分钟

前言

本文系统讲解代币锁(Token Locker)合约的核心概念、应用场景,并完整实现其开发、测试、部署全流程,帮助开发者理解并落地代币锁仓机制。

一、代币锁核心概念

1.1 定义

代币锁仓是指通过智能合约将特定数量的代币在指定时间 / 条件下限制转移、交易的行为。其核心目标是绑定相关方与项目的长期利益,防止早期投资者、团队成员短期抛售代币套利离场,维护项目生态稳定。

1.2 核心功能

功能类型说明典型应用场景
时间锁定代币在设定时间段内完全不可转移 / 使用团队代币解锁、早期投资者锁仓
数量锁定固定数量代币在指定地址保持锁定状态众筹 / 私募阶段的代币承诺
条件锁定基于特定条件(时间、价格、事件)触发解锁里程碑式解锁、流动性挖矿奖励

1.3 常见解锁方式

  1. 线性解锁:代币按时间比例逐步解锁(如每月解锁 10%),平滑释放流通量
  2. 阶段性解锁:在特定时间点 / 项目里程碑(如产品上线、版本迭代)解锁部分代币
  3. 一次性解锁:所有锁定代币在指定时间点全部释放(适用于短期锁仓场景)

1.4 典型应用场景

  1. 团队与顾问:绑定核心开发人员、顾问的代币,确保其对项目的长期投入
  2. 早期投资者:限制 ICO / 私募投资者的代币流通,避免短期投机行为冲击市场
  3. 项目创始人:将创始人代币与项目长期发展绑定,体现核心团队的信心与承诺
  4. 流动性提供者:锁定 LP 代币,防止流动性突然撤出导致的交易滑点剧增

1.5 代币锁仓的核心优势

  1. 透明性:智能合约自动执行锁仓规则,所有操作上链可查,无人工干预空间
  2. 灵活性:可根据项目需求定制锁仓周期、解锁方式、触发条件
  3. 安全性:基于区块链技术,避免中心化机构操纵或人为篡改锁仓规则
  4. 长期价值:减少短期投机,增强市场信心,助力项目长期健康发展

二、代币锁合约开发

2.1 合约设计思路

本次实现的代币锁合约核心逻辑:

  • 锁定指定 ERC20 代币,设置锁仓时长
  • 锁仓期结束前无法释放代币
  • 锁仓期结束后,受益人可调用释放函数提取全部代币
  • 通过事件记录锁仓和释放关键信息,便于链上追踪

2.2 代币智能合约

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract BoykaYuriToken is ERC20, ERC20Burnable, Ownable, ERC20Permit {
    constructor(address recipient, address initialOwner)
        ERC20("MyToken", "MTK")
        Ownable(initialOwner)
        ERC20Permit("MyToken")
    {
        _mint(recipient, 1000000 * 10 ** decimals());
    }
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

2.3 代币锁智能合约

// SPDX-License-Identifier: MIT
// 基于OpenZeppelin实现的ERC20代币时间锁合约
pragma solidity ^0.8.22;

// 导入OpenZeppelin的ERC20接口(行业标准实现)
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
 * @title TokenLocker
 * @dev ERC20代币时间锁合约
 * 核心功能:受益人需等待锁仓周期结束后,才能提取合约中的代币
 * 特点:部署时确定锁仓参数,不可修改,保证规则的确定性
 */
contract TokenLocker {
    // 事件定义 - 记录关键操作,便于前端/链下分析
    /// @notice 代币锁仓开始事件
    /// @param beneficiary 代币受益人地址
    /// @param token 被锁定的ERC20代币地址
    /// @param startTime 锁仓起始时间戳(秒)
    /// @param lockTime 锁仓时长(秒)
    event TokenLockStart(
        address indexed beneficiary,
        address indexed token,
        uint256 startTime,
        uint256 lockTime
    );

    /// @notice 代币释放事件
    /// @param beneficiary 代币受益人地址
    /// @param token 被释放的ERC20代币地址
    /// @param releaseTime 释放时间戳(秒)
    /// @param amount 释放的代币数量
    event Release(
        address indexed beneficiary,
        address indexed token,
        uint256 releaseTime,
        uint256 amount
    );

    // 状态变量 - 全部使用immutable确保不可篡改
    /// @dev 被锁仓的ERC20代币合约地址
    IERC20 public immutable token;
    /// @dev 代币受益人地址(最终接收代币的地址)
    address public immutable beneficiary;
    /// @dev 锁仓时长(单位:秒)
    uint256 public immutable lockTime;
    /// @dev 锁仓起始时间戳(单位:秒,部署合约时的区块时间)
    uint256 public immutable startTime;

    /**
     * @dev 构造函数:初始化代币锁仓合约
     * @param token_ 被锁仓的ERC20代币合约地址
     * @param beneficiary_ 代币受益人地址
     * @param lockTime_ 锁仓时长(秒),必须大于0
     */
    constructor(
        IERC20 token_,
        address beneficiary_,
        uint256 lockTime_
    ) {
        // 输入验证:锁仓时间必须大于0
        require(lockTime_ > 0, "TokenLocker: lock time must be > 0");
        // 禁止零地址
        require(address(token_) != address(0), "TokenLocker: token is zero address");
        require(beneficiary_ != address(0), "TokenLocker: beneficiary is zero address");

        token = token_;
        beneficiary = beneficiary_;
        lockTime = lockTime_;
        startTime = block.timestamp;

        // 触发锁仓开始事件
        emit TokenLockStart(beneficiary_, address(token_), block.timestamp, lockTime_);
    }

    /**
     * @dev 释放代币函数:锁仓期结束后,将合约中所有代币转移给受益人
     * 注意:
     * 1. 任何人都可调用此函数(不限于受益人),符合链上自动化操作逻辑
     * 2. 释放后合约中代币余额清零
     */
    function release() external {
        // 验证:当前时间需达到锁仓结束时间
        uint256 releaseTime = startTime + lockTime;
        require(block.timestamp >= releaseTime, "TokenLocker: lock period not ended");

        // 获取合约中当前代币余额
        uint256 amount = token.balanceOf(address(this));
        require(amount > 0, "TokenLocker: no tokens available for release");

        // 转移代币给受益人(使用safeTransfer更安全,兼容所有ERC20代币)
        bool success = token.transfer(beneficiary, amount);
        require(success, "TokenLocker: token transfer failed");

        // 触发释放事件
        emit Release(beneficiary, address(token), block.timestamp, amount);
    }

    /**
     * @dev 辅助函数:查询锁仓结束时间
     * @return 锁仓结束的时间戳(秒)
     */
    function getReleaseTime() external view returns (uint256) {
        return startTime + lockTime;
    }

    /**
     * @dev 辅助函数:查询合约中可释放的代币数量
     * @return 可释放的代币数量(原始精度)
     */
    function getLockedTokenAmount() external view returns (uint256) {
        return token.balanceOf(address(this));
    }
}

2.4 编译指令

npx hardhat compile

三、合约测试

3.1 测试思路

  • 部署测试用 ERC20 代币和代币锁合约
  • 模拟代币转入锁仓合约
  • 通过修改区块链时间模拟锁仓周期结束
  • 验证锁仓期内无法释放代币,锁仓期结束后可正常释放
  • 验证代币释放后重复调用release应失败

3.2 完整测试代码

import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import hre from "hardhat";

describe("TokenLocker", async function () {
    // 1. 在 describe 顶级通过异步连接获取 viem 和 helpers
    // 这是 Hardhat v3 的标准写法
    const { viem, networkHelpers } = await hre.network.connect();

    let publicClient: any;
    let testClient: any;
    let Token: any;
    let TokenLocker: any;
    let deployerAddress: `0x${string}`;
    const lockTime = 3600n;

    beforeEach(async function () {
        // 使用从 connect() 拿到的 viem 实例
        publicClient = await viem.getPublicClient();
        testClient = await viem.getTestClient();
        
        const [owner] = await viem.getWalletClients();
        deployerAddress = owner.account.address;

        // 部署合约
        Token = await viem.deployContract("BoykaYuriToken", [deployerAddress, deployerAddress]);
        TokenLocker = await viem.deployContract("TokenLocker", [Token.address, deployerAddress, lockTime]);
        console.log("Token地址:",Token.address);
        console.log("TokenLocker地址:",TokenLocker.address);
        
        // 给锁仓合约转账
        await Token.write.transfer([TokenLocker.address, 1000n * 10n**18n]);
    });
    // 测试用例1:初始化参数验证
    it("初始化参数验证", async function () {
    console.log(await TokenLocker.read.token());
    console.log(Token.address.toLowerCase());
    console.log(await TokenLocker.read.beneficiary())
    console.log(deployerAddress.toLowerCase());
    console.log(Number(await TokenLocker.read.lockTime())==Number(lockTime))
  });
  // 测试用例2:锁仓期内无法释放
  it("锁仓期内释放代币应失败", async function () {
    try {
      await TokenLocker.write.release()
    } catch (error) {
      console.log("TokenLocker: lock period not ended")
    }
  });
    it("锁仓期结束后可释放全部代币", async function () {
        const currentBlock = await publicClient.getBlock();  
        // 模拟时间
        await networkHelpers.time.increase(lockTime + 1n);
        await networkHelpers.mine(); // 确保产生新块

        const newBlock = await publicClient.getBlock({ blockTag: 'latest' });
        
        // 断言
        assert.ok(newBlock.timestamp > currentBlock.timestamp + lockTime);
        console.log(`模拟后区块时间: ${newBlock.timestamp}`);
        console.log(`目标时间应大于: ${currentBlock.timestamp + lockTime}`);
        // 释放
        const hash = await TokenLocker.write.release();
        await publicClient.waitForTransactionReceipt({ hash });

        const lockerBal = await Token.read.balanceOf([TokenLocker.address]);
        console.log("锁仓合约剩余余额:", lockerBal.toString());
    });
    // 测试用例4:重复释放失败
  it("代币释放后重复调用release应失败", async function () {
    await networkHelpers.time.increase(lockTime + 1n);
    await networkHelpers.mine(); // 确保产生新块

    // 第一次释放
    await TokenLocker.write.release();

    // 第二次释放
    try{
      await TokenLocker.write.release()
    } catch (error) {
      console.log("TokenLocker: no tokens available for release")
    }
  });
});

3.3 测试指令

npx hardhat test ./test/xxx.ts

四、合约部署

4.1 部署脚本

// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
  // 连接网络
  const { viem } = await network.connect({ network: network.name });//指定网络进行链接
  
  // 获取客户端
  const [deployer] = await viem.getWalletClients();
  const publicClient = await viem.getPublicClient();
 
  const deployerAddress = deployer.account.address;
   console.log("部署者的地址:", deployerAddress);
  // 加载合约
  const artifact = await artifacts.readArtifact("BoykaYuriToken");

  // 部署(构造函数参数:recipient, initialOwner)
  const hash = await deployer.deployContract({
    abi: artifact.abi,//获取abi
    bytecode: artifact.bytecode,//硬编码
    args: [deployerAddress,deployerAddress],//process.env.RECIPIENT, process.env.OWNER
  });

  // 等待确认并打印地址
  const BoykaYuriTokenReceipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log("合约地址:", BoykaYuriTokenReceipt.contractAddress);
  // 部署TokenLocker合约
  const lockTime = 60 * 60; 
  const lockerArtifact = await artifacts.readArtifact("TokenLocker");
  // 1. 部署合约并获取交易哈希
  const lockerHash = await deployer.deployContract({
    abi: lockerArtifact.abi,
    bytecode: lockerArtifact.bytecode,
    args: [BoykaYuriTokenReceipt.contractAddress, deployerAddress, lockTime],
  });
  const lockerReceipt = await publicClient.waitForTransactionReceipt({ 
     hash: lockerHash 
   });
   console.log("TokenLocker合约地址:", lockerReceipt.contractAddress);
}

main().catch(console.error);

4.2 部署指令

npx hardhat run ./scripts/xxx.ts

总结

  • 代币锁核心价值:通过智能合约实现代币的时间 / 条件锁定,绑定相关方长期利益,维护项目生态稳定。

  • 合约设计关键:使用immutable保证核心参数不可篡改,增加零地址 / 参数验证提升安全性,通过事件记录关键操作。

  • 测试核心逻辑:模拟区块链时间验证锁仓规则,覆盖 "锁仓期内不可释放"、"锁仓期结束可释放"、"重复释放失败" 等关键场景。

  • 部署注意事项:部署前确认代币地址、受益人地址、锁仓时间等核心参数,主网部署需等待足够区块确认。