Solidity定时任务:让你的合约按点做事稳如泰山
Solidity定时任务!在区块链上,智能合约要想自动干活,比如每天分红、定期锁仓释放,或者按时更新数据,咋整?以太坊可没内置定时器!定时任务得靠外部触发或预言机来搞定。这篇干货从基础的时间检查到Chainlink Keeper、外部调用触发,再到防重入和权限控制,配合OpenZeppelin和Hardhat测试,带你一步步打造准时又安全的定时机制。
定时任务的核心概念
先搞清楚几个关键点:
- 定时任务:合约在特定时间或周期执行特定逻辑,如分红、数据更新。
- 以太坊限制:EVM无内置定时器,需外部触发(如用户、预言机)。
- 实现方式:
- 时间戳检查:用
block.timestamp判断时间,简单但依赖外部调用。 - Chainlink Keeper:去中心化自动化网络,定时触发任务。
- 外部脚本:用Hardhat或脚本手动触发。
- 时间戳检查:用
- 安全风险:
- 时间戳操控:矿工可能微调
block.timestamp。 - 权限控制:谁能触发任务?没限制可能被滥用。
- 重入攻击:外部调用可能引发重入。
- Gas限制:复杂任务可能耗尽Gas。
- 时间戳操控:矿工可能微调
- 工具:
- Solidity 0.8.x:安全的时间戳和数学操作。
- Chainlink Keeper:可靠的去中心化触发。
- OpenZeppelin:权限和安全库。
- Hardhat:测试和调试任务。
- 事件:记录任务执行,便于链上追踪。
咱们用Solidity 0.8.20,结合Chainlink Keeper、OpenZeppelin和Hardhat,从基础到复杂,逐一实现定时任务。
环境准备
用Hardhat搭建开发环境,集成Chainlink。
mkdir timed-task-demo
cd timed-task-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @chainlink/contracts
npm install ethers
初始化Hardhat:
npx hardhat init
选择TypeScript项目,安装依赖:
npm install --save-dev ts-node typescript @types/node @types/mocha
目录结构:
timed-task-demo/
├── contracts/
│ ├── BasicTimedTask.sol
│ ├── KeeperTimedTask.sol
│ ├── MultiSigTimedTask.sol
│ ├── ConditionalTimedTask.sol
│ ├── RewardTimedTask.sol
├── scripts/
│ ├── deploy.ts
├── test/
│ ├── TimedTask.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./"
},
"include": ["hardhat.config.ts", "scripts", "test"]
}
hardhat.config.ts:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 1337,
},
},
};
export default config;
跑本地节点:
npx hardhat node
基础时间戳检查
用block.timestamp实现简单的定时任务。
合约代码
contracts/BasicTimedTask.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract BasicTimedTask is Ownable {
uint256 public lastExecution;
uint256 public interval = 1 days;
uint256 public reward = 1 ether;
mapping(address => uint256) public userRewards;
event TaskExecuted(address indexed caller, uint256 timestamp);
constructor() Ownable() {
lastExecution = block.timestamp;
}
function executeTask() public {
require(block.timestamp >= lastExecution + interval, "Too soon");
lastExecution = block.timestamp;
userRewards[msg.sender] += reward;
emit TaskExecuted(msg.sender, block.timestamp);
}
function withdrawReward() public {
uint256 amount = userRewards[msg.sender];
require(amount > 0, "No rewards");
userRewards[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function deposit() public payable onlyOwner {}
}
解析
- 逻辑:
interval:任务间隔(1天)。lastExecution:记录上次执行时间。executeTask:检查block.timestamp,执行任务,奖励调用者。withdrawReward:提取奖励。deposit:为合约充值ETH。
- 安全特性:
block.timestamp检查防止过早执行。onlyOwner限制充值。- 事件记录执行。
- 问题:
- 依赖外部调用,需手动触发。
block.timestamp可被矿工微调(±15秒)。- 无权限控制,任何人可触发。
- Gas:
executeTask~30k Gas。withdrawReward~20k Gas。
测试
test/TimedTask.test.ts:
import { ethers } from "hardhat";
import { expect } from "chai";
import { BasicTimedTask } from "../typechain-types";
describe("BasicTimedTask", function () {
let task: BasicTimedTask;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TaskFactory = await ethers.getContractFactory("BasicTimedTask");
task = await TaskFactory.deploy();
await task.deployed();
await task.deposit({ value: ethers.utils.parseEther("10") });
});
it("should execute task after interval", async function () {
await ethers.provider.send("evm_increaseTime", [86400]); // 1 day
await ethers.provider.send("evm_mine", []);
await expect(task.connect(user1).executeTask())
.to.emit(task, "TaskExecuted")
.withArgs(user1.address, await ethers.provider.getBlock("latest").then(b => b.timestamp));
expect(await task.userRewards(user1.address)).to.equal(ethers.utils.parseEther("1"));
});
it("should revert if too soon", async function () {
await expect(task.connect(user1).executeTask()).to.be.revertedWith("Too soon");
});
it("should withdraw rewards", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await task.connect(user1).executeTask();
const initialBalance = await ethers.provider.getBalance(user1.address);
await task.connect(user1).withdrawReward();
expect(await ethers.provider.getBalance(user1.address)).to.be.gt(initialBalance);
});
});
跑测试:
npx hardhat test
- 解析:
- 1天后执行任务,奖励1 ETH。
- 过早执行失败。
- 奖励可提取,余额更新。
- 问题:需外部触发,安全性依赖
block.timestamp。
Chainlink Keeper触发
用Chainlink Keeper实现去中心化定时任务。
合约代码
contracts/KeeperTimedTask.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol";
contract KeeperTimedTask is Ownable, KeeperCompatibleInterface {
uint256 public lastExecution;
uint256 public interval = 1 days;
uint256 public reward = 1 ether;
mapping(address => uint256) public userRewards;
event TaskExecuted(address indexed caller, uint256 timestamp);
constructor() Ownable() {
lastExecution = block.timestamp;
}
function checkUpkeep(bytes calldata) external view override returns (bool upkeepNeeded, bytes memory) {
upkeepNeeded = block.timestamp >= lastExecution + interval;
return (upkeepNeeded, bytes(""));
}
function performUpkeep(bytes calldata) external override {
require(block.timestamp >= lastExecution + interval, "Too soon");
lastExecution = block.timestamp;
userRewards[msg.sender] += reward;
emit TaskExecuted(msg.sender, block.timestamp);
}
function withdrawReward() public {
uint256 amount = userRewards[msg.sender];
require(amount > 0, "No rewards");
userRewards[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function deposit() public payable onlyOwner {}
}
解析
- 逻辑:
- 继承
KeeperCompatibleInterface。 checkUpkeep:检查是否可执行(间隔1天)。performUpkeep:执行任务,奖励调用者。withdrawReward/deposit:同上。
- 继承
- 安全特性:
- Chainlink Keeper去中心化触发,减少依赖。
- 事件记录执行。
- 问题:
- 需注册Keeper(需LINK代币)。
block.timestamp仍可微调。
- Gas:
performUpkeep~35k Gas。- Keeper调用需支付LINK。
测试
test/TimedTask.test.ts(add):
import { KeeperTimedTask } from "../typechain-types";
describe("KeeperTimedTask", function () {
let task: KeeperTimedTask;
let owner: any, keeper: any;
beforeEach(async function () {
[owner, keeper] = await ethers.getSigners();
const TaskFactory = await ethers.getContractFactory("KeeperTimedTask");
task = await TaskFactory.deploy();
await task.deployed();
await task.deposit({ value: ethers.utils.parseEther("10") });
});
it("should check upkeep correctly", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
const { upkeepNeeded } = await task.checkUpkeep("0x");
expect(upkeepNeeded).to.be.true;
});
it("should perform upkeep", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await expect(task.connect(keeper).performUpkeep("0x"))
.to.emit(task, "TaskExecuted")
.withArgs(keeper.address, await ethers.provider.getBlock("latest").then(b => b.timestamp));
expect(await task.userRewards(keeper.address)).to.equal(ethers.utils.parseEther("1"));
});
it("should revert if too soon", async function () {
await expect(task.connect(keeper).performUpkeep("0x")).to.be.revertedWith("Too soon");
});
});
- 解析:
checkUpkeep确认任务可执行。performUpkeep成功执行,奖励1 ETH。- 过早执行失败。
- 优势:Chainlink Keeper自动化,适合生产环境。
多签控制定时任务
用多签机制限制任务触发。
合约代码
contracts/MultiSigTimedTask.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract MultiSigTimedTask is Ownable {
address[] public executors;
uint256 public required;
uint256 public transactionCount;
mapping(uint256 => Transaction) public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
uint256 public lastExecution;
uint256 public interval = 1 days;
uint256 public reward = 1 ether;
mapping(address => uint256) public userRewards;
struct Transaction {
bool executed;
uint256 confirmationCount;
}
event SubmitTask(uint256 indexed txId);
event ConfirmTask(uint256 indexed txId, address indexed executor);
event ExecuteTask(uint256 indexed txId, address indexed executor, uint256 timestamp);
event RevokeConfirmation(uint256 indexed txId, address indexed executor);
modifier onlyExecutor() {
bool isExecutor = false;
for (uint256 i = 0; i < executors.length; i++) {
if (executors[i] == msg.sender) {
isExecutor = true;
break;
}
}
require(isExecutor, "Not executor");
_;
}
constructor(address[] memory _executors, uint256 _required) Ownable() {
require(_executors.length > 0, "Executors required");
require(_required > 0 && _required <= _executors.length, "Invalid required");
executors = _executors;
required = _required;
lastExecution = block.timestamp;
}
function submitTask() public onlyExecutor {
uint256 txId = transactionCount++;
transactions[txId] = Transaction({
executed: false,
confirmationCount: 0
});
emit SubmitTask(txId);
}
function confirmTask(uint256 txId) public onlyExecutor {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(!confirmations[txId][msg.sender], "Already confirmed");
confirmations[txId][msg.sender] = true;
transaction.confirmationCount++;
emit ConfirmTask(txId, msg.sender);
if (transaction.confirmationCount >= required) {
executeTask(txId);
}
}
function executeTask(uint256 txId) internal {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(block.timestamp >= lastExecution + interval, "Too soon");
transaction.executed = true;
lastExecution = block.timestamp;
userRewards[msg.sender] += reward;
emit ExecuteTask(txId, msg.sender, block.timestamp);
}
function revokeConfirmation(uint256 txId) public onlyExecutor {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(confirmations[txId][msg.sender], "Not confirmed");
confirmations[txId][msg.sender] = false;
transaction.confirmationCount--;
emit RevokeConfirmation(txId, msg.sender);
}
function withdrawReward() public {
uint256 amount = userRewards[msg.sender];
require(amount > 0, "No rewards");
userRewards[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function deposit() public payable onlyOwner {}
}
解析
- 逻辑:
executors和required控制多签。submitTask:提交任务提案。confirmTask:确认提案,达标后执行。executeTask:检查时间,执行任务,奖励调用者。revokeConfirmation:撤销确认。
- 安全特性:
- 多签防止单人误触发。
- 时间检查确保间隔。
- 事件记录提案和执行。
- 问题:
- 需外部触发,依赖多签成员。
- 多签增加Gas成本。
- Gas:
- 提案
10k Gas,确认10k Gas,执行~30k Gas。
- 提案
测试
test/TimedTask.test.ts(add):
import { MultiSigTimedTask } from "../typechain-types";
describe("MultiSigTimedTask", function () {
let task: MultiSigTimedTask;
let owner: any, executor1: any, executor2: any, executor3: any;
beforeEach(async function () {
[owner, executor1, executor2, executor3] = await ethers.getSigners();
const TaskFactory = await ethers.getContractFactory("MultiSigTimedTask");
task = await TaskFactory.deploy([executor1.address, executor2.address, executor3.address], 2);
await task.deployed();
await task.deposit({ value: ethers.utils.parseEther("10") });
});
it("should execute task with multi-sig", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await task.connect(executor1).submitTask();
await task.connect(executor2).confirmTask(0);
await expect(task.connect(executor3).confirmTask(0))
.to.emit(task, "ExecuteTask")
.withArgs(0, executor3.address, await ethers.provider.getBlock("latest").then(b => b.timestamp));
expect(await task.userRewards(executor3.address)).to.equal(ethers.utils.parseEther("1"));
});
it("should not execute without enough confirmations", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await task.connect(executor1).submitTask();
await task.connect(executor2).confirmTask(0);
expect(await task.userRewards(executor2.address)).to.equal(0);
});
it("should allow revoking confirmation", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await task.connect(executor1).submitTask();
await task.connect(executor2).confirmTask(0);
await task.connect(executor2).revokeConfirmation(0);
await task.connect(executor3).confirmTask(0);
expect(await task.userRewards(executor3.address)).to.equal(0);
});
});
- 解析:
- 2/3确认后执行任务,奖励1 ETH。
- 单人确认不触发任务。
- 撤销确认阻止执行。
- 优势:多签增加安全性,适合团队管理。
条件触发定时任务
根据外部条件(如余额)触发任务。
合约代码
contracts/ConditionalTimedTask.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract ConditionalTimedTask is Ownable {
uint256 public lastExecution;
uint256 public interval = 1 days;
uint256 public reward = 1 ether;
uint256 public minBalance = 10 ether;
mapping(address => uint256) public userRewards;
event TaskExecuted(address indexed caller, uint256 timestamp, string reason);
constructor() Ownable() {
lastExecution = block.timestamp;
}
function executeTask() public {
require(block.timestamp >= lastExecution + interval, "Too soon");
require(address(this).balance >= minBalance, "Insufficient balance");
lastExecution = block.timestamp;
userRewards[msg.sender] += reward;
emit TaskExecuted(msg.sender, block.timestamp, "Balance condition met");
}
function withdrawReward() public {
uint256 amount = userRewards[msg.sender];
require(amount > 0, "No rewards");
userRewards[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function deposit() public payable onlyOwner {}
}
解析
- 逻辑:
minBalance:触发需合约余额≥10 ETH。executeTask:检查时间和余额,执行任务。withdrawReward/deposit:同上。
- 安全特性:
- 条件检查防止无效执行。
- 事件记录触发原因。
- 问题:
- 依赖外部调用。
- 余额条件需谨慎设置。
- Gas:
executeTask~35k Gas(含条件检查)。
Test
test/TimedTask.test.ts(add):
import { ConditionalTimedTask } from "../typechain-types";
describe("ConditionalTimedTask", function () {
let task: ConditionalTimedTask;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TaskFactory = await ethers.getContractFactory("ConditionalTimedTask");
task = await TaskFactory.deploy();
await task.deployed();
await task.deposit({ value: ethers.utils.parseEther("15") });
});
it("should execute task with sufficient balance", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await expect(task.connect(user1).executeTask())
.to.emit(task, "TaskExecuted")
.withArgs(user1.address, await ethers.provider.getBlock("latest").then(b => b.timestamp), "Balance condition met");
expect(await task.userRewards(user1.address)).to.equal(ethers.utils.parseEther("1"));
});
it("should revert if insufficient balance", async function () {
await task.deposit({ value: ethers.utils.parseEther("5") });
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await expect(task.connect(user1).executeTask()).to.be.revertedWith("Insufficient balance");
});
});
- 解析:
- 余额≥10 ETH时任务执行。
- 余额不足失败,验证条件。
- 优势:条件触发增加灵活性。
定时奖励分发
定时分发代币奖励。
合约代码
contracts/RewardTimedTask.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract RewardTimedTask is ERC20, Ownable, ReentrancyGuard {
uint256 public lastExecution;
uint256 public interval = 1 days;
uint256 public reward = 100 * 10**18;
mapping(address => uint256) public userRewards;
event TaskExecuted(address indexed caller, uint256 timestamp, uint256 reward);
constructor() ERC20("RewardToken", "RTK") Ownable() {
lastExecution = block.timestamp;
_mint(address(this), 1000000 * 10**decimals());
}
function executeTask() public nonReentrant {
require(block.timestamp >= lastExecution + interval, "Too soon");
lastExecution = block.timestamp;
userRewards[msg.sender] += reward;
_transfer(address(this), msg.sender, reward);
emit TaskExecuted(msg.sender, block.timestamp, reward);
}
function withdrawReward() public nonReentrant {
uint256 amount = userRewards[msg.sender];
require(amount > 0, "No rewards");
userRewards[msg.sender] = 0;
_transfer(address(this), msg.sender, amount);
}
}
解析
- 逻辑:
- 继承
ERC20、ReentrancyGuard。 executeTask:定时分发代币奖励,防重入。withdrawReward:提取奖励。
- 继承
- 安全特性:
nonReentrant防止重入攻击。- 事件记录分发。
- 问题:
- 依赖外部触发。
- 需确保合约有足够代币。
- Gas:
executeTask~40k Gas(含转账)。
Test
test/TimedTask.test.ts(add):
import { RewardTimedTask } from "../typechain-types";
describe("RewardTimedTask", function () {
let task: RewardTimedTask;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TaskFactory = await ethers.getContractFactory("RewardTimedTask");
task = await TaskFactory.deploy();
await task.deployed();
});
it("should execute task and reward tokens", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await expect(task.connect(user1).executeTask())
.to.emit(task, "TaskExecuted")
.withArgs(user1.address, await ethers.provider.getBlock("latest").then(b => b.timestamp), ethers.utils.parseEther("100"));
expect(await task.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("100"));
});
it("should withdraw rewards", async function () {
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);
await task.connect(user1).executeTask();
await task.connect(user1).withdrawReward();
expect(await task.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("200"));
});
});
- 解析:
- 任务执行分发100代币。
- 奖励可累积提取。
- 优势:代币奖励适合激励机制。
部署脚本
scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const [owner, executor1, executor2, executor3] = await ethers.getSigners();
const BasicTaskFactory = await ethers.getContractFactory("BasicTimedTask");
const basicTask = await BasicTaskFactory.deploy();
await basicTask.deployed();
console.log(`BasicTimedTask deployed to: ${basicTask.address}`);
const KeeperTaskFactory = await ethers.getContractFactory("KeeperTimedTask");
const keeperTask = await KeeperTaskFactory.deploy();
await keeperTask.deployed();
console.log(`KeeperTimedTask deployed to: ${keeperTask.address}`);
const MultiSigTaskFactory = await ethers.getContractFactory("MultiSigTimedTask");
const multiSigTask = await MultiSigTaskFactory.deploy([executor1.address, executor2.address, executor3.address], 2);
await multiSigTask.deployed();
console.log(`MultiSigTimedTask deployed to: ${multiSigTask.address}`);
const ConditionalTaskFactory = await ethers.getContractFactory("ConditionalTimedTask");
const conditionalTask = await ConditionalTaskFactory.deploy();
await conditionalTask.deployed();
console.log(`ConditionalTimedTask deployed to: ${conditionalTask.address}`);
const RewardTaskFactory = await ethers.getContractFactory("RewardTimedTask");
const rewardTask = await RewardTaskFactory.deploy();
await rewardTask.deployed();
console.log(`RewardTimedTask deployed to: ${rewardTask.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
跑部署:
npx hardhat run scripts/deploy.ts --network hardhat 版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
区块链技术网
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。