前言本文系统讲解代币锁(TokenLocker)合约的核心概念、应用场景,并完整实现其开发、测试、部署全流程,帮助开发者理解并落地代币锁仓机制。一、代币锁核心概念1.1定义代币锁仓是指通过智能合约将特定数量的代币在指定时间/条件下限制转移、交易的行为。其核心目标是绑定相关方与项目的长
本文系统讲解代币锁(Token Locker)合约的核心概念、应用场景,并完整实现其开发、测试、部署全流程,帮助开发者理解并落地代币锁仓机制。
代币锁仓是指通过智能合约将特定数量的代币在指定时间 / 条件下限制转移、交易的行为。其核心目标是绑定相关方与项目的长期利益,防止早期投资者、团队成员短期抛售代币套利离场,维护项目生态稳定。
| 功能类型 | 说明 | 典型应用场景 |
|---|---|---|
| 时间锁定 | 代币在设定时间段内完全不可转移 / 使用 | 团队代币解锁、早期投资者锁仓 |
| 数量锁定 | 固定数量代币在指定地址保持锁定状态 | 众筹 / 私募阶段的代币承诺 |
| 条件锁定 | 基于特定条件(时间、价格、事件)触发解锁 | 里程碑式解锁、流动性挖矿奖励 |
本次实现的代币锁合约核心逻辑:
// 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";
/**
特点:部署时确定锁仓参数,不可修改,保证规则的确定性 */ 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;
/**
@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_); }
/**
// 获取合约中当前代币余额 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); }
/**
/**
### 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`保证核心参数不可篡改,增加零地址 / 参数验证提升安全性,通过事件记录关键操作。
- **测试核心逻辑**:模拟区块链时间验证锁仓规则,覆盖 "锁仓期内不可释放"、"锁仓期结束可释放"、"重复释放失败" 等关键场景。
- **部署注意事项**:部署前确认代币地址、受益人地址、锁仓时间等核心参数,主网部署需等待足够区块确认。 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!