Solidity定时任务!在区块链上,智能合约要想自动干活,比如每天分红、定期锁仓释放,或者按时更新数据,咋整?以太坊可没内置定时器!定时任务得靠外部触发或预言机来搞定。这篇干货从基础的时间检查到ChainlinkKeeper、外部调用触发,再到防重入和权限控制,配合OpenZeppelin和Ha
Solidity定时任务!在区块链上,智能合约要想自动干活,比如每天分红、定期锁仓释放,或者按时更新数据,咋整?以太坊可没内置定时器!定时任务得靠外部触发或预言机来搞定。这篇干货从基础的时间检查到Chainlink Keeper、外部调用触发,再到防重入和权限控制,配合OpenZeppelin和Hardhat测试,带你一步步打造准时又安全的定时机制。
先搞清楚几个关键点:
block.timestamp判断时间,简单但依赖外部调用。block.timestamp。咱们用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秒)。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
block.timestamp。用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:同上。block.timestamp仍可微调。performUpkeep~35k Gas。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。用多签机制限制任务触发。
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:撤销确认。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);
});
});
根据外部条件(如余额)触发任务。
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:同上。executeTask~35k Gas(含条件检查)。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");
});
});
定时分发代币奖励。
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防止重入攻击。executeTask~40k Gas(含转账)。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"));
});
});
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 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!