前言
本文系统讲解代币锁(Token Locker)合约的核心概念、应用场景,并完整实现其开发、测试、部署全流程,帮助开发者理解并落地代币锁仓机制。
一、代币锁核心概念
1.1 定义
代币锁仓是指通过智能合约将特定数量的代币在指定时间 / 条件下限制转移、交易的行为。其核心目标是绑定相关方与项目的长期利益,防止早期投资者、团队成员短期抛售代币套利离场,维护项目生态稳定。
1.2 核心功能
| 功能类型 | 说明 | 典型应用场景 |
|---|---|---|
| 时间锁定 | 代币在设定时间段内完全不可转移 / 使用 | 团队代币解锁、早期投资者锁仓 |
| 数量锁定 | 固定数量代币在指定地址保持锁定状态 | 众筹 / 私募阶段的代币承诺 |
| 条件锁定 | 基于特定条件(时间、价格、事件)触发解锁 | 里程碑式解锁、流动性挖矿奖励 |
1.3 常见解锁方式
- 线性解锁:代币按时间比例逐步解锁(如每月解锁 10%),平滑释放流通量
- 阶段性解锁:在特定时间点 / 项目里程碑(如产品上线、版本迭代)解锁部分代币
- 一次性解锁:所有锁定代币在指定时间点全部释放(适用于短期锁仓场景)
1.4 典型应用场景
- 团队与顾问:绑定核心开发人员、顾问的代币,确保其对项目的长期投入
- 早期投资者:限制 ICO / 私募投资者的代币流通,避免短期投机行为冲击市场
- 项目创始人:将创始人代币与项目长期发展绑定,体现核心团队的信心与承诺
- 流动性提供者:锁定 LP 代币,防止流动性突然撤出导致的交易滑点剧增
1.5 代币锁仓的核心优势
- 透明性:智能合约自动执行锁仓规则,所有操作上链可查,无人工干预空间
- 灵活性:可根据项目需求定制锁仓周期、解锁方式、触发条件
- 安全性:基于区块链技术,避免中心化机构操纵或人为篡改锁仓规则
- 长期价值:减少短期投机,增强市场信心,助力项目长期健康发展
二、代币锁合约开发
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保证核心参数不可篡改,增加零地址 / 参数验证提升安全性,通过事件记录关键操作。 -
测试核心逻辑:模拟区块链时间验证锁仓规则,覆盖 "锁仓期内不可释放"、"锁仓期结束可释放"、"重复释放失败" 等关键场景。
-
部署注意事项:部署前确认代币地址、受益人地址、锁仓时间等核心参数,主网部署需等待足够区块确认。