前言
随着账户抽象(AA)的普及,钱包正在从简单的“私钥容器”进化为智能的“链上操作系统”。ERC-7579 作为模块化智能账户的标准,允许我们为钱包编写可插拔的功能模块。
今天,我们将通过解析一个实战合约
IntentExecutor,看看如何实现自动化定投、自动续费以及止损交易。
1. 核心设计思想:执行器 (Executor)
在 ERC-7579 标准中,Executor 是一类特殊的模块。它拥有账户的“执行权”:
- 非被动响应:它不需要用户在交易时签名。
- 主动触发:一旦预设条件(时间、价格)达成,它可以驱动账户发起交易。
2. 合约核心架构解析
我们的 IntentExecutor 合约实现了意图(Intent)的存储与触发逻辑。
A. 意图数据的结构化
我们通过一个 Intent 结构体来定义用户想做的事:
struct Intent {
uint256 interval; // 场景:自动续费(每30天执行一次)
uint256 priceThreshold; // 场景:自动止损(价格低于2000时卖出)
address target; // 目标合约(如 USDC 地址或 DEX 地址)
bytes callData; // 具体的执行动作(如 transfer 或 swap)
bool active; // 状态开关
}
B. 跨钱包的“即插即用”
通过实现 ERC-7579 定义的 isModuleType 和 onInstall 接口,该合约可以无缝安装到任何兼容标准的钱包(如 Safe 或 Biconomy)中:
solidity
function isModuleType(uint256 typeID) external pure returns (bool) {
return typeID == 2; // 2 代表执行器模块
}
C. 条件触发逻辑
这是“意图”转化为“行动”的关键。triggerExecution 函数允许 Keeper(机器人)在满足条件时触发交易:
// 判断逻辑:时间到 OR 价格到
bool timeCondition = (intent.interval > 0 && block.timestamp >= intent.lastTimestamp + intent.interval);
bool priceCondition = (intent.priceThreshold > 0 && currentPrice <= intent.priceThreshold);
require(timeCondition || priceCondition, "Conditions not met");
3. 具体落地场景演示
场景一:DeFi 自动续费(Subscription)
用户希望每月自动支付 50 USDC 的服务费。
- 操作:调用
registerIntent,设置interval = 30 days。 - 逻辑:当 30 天过去,Keeper 调用执行器,执行器驱动钱包调用 USDC 合约的
transfer。
场景二:链上自动化止损(Stop-Loss)
用户持有 ETH,希望在跌破 2000 美元时换成 USDT 避险。
- 操作:设置
priceThreshold = 2000,callData为 DEX 的swap函数。 - 逻辑:Keeper 监控链下价格,一旦满足条件立即触发。账户自动执行兑换,全程无需用户在线签名。
4. 最小可运行MVP:合约开发、测试、部署一站式
4.1.智能合约
- 4.1.1 Mock7579Account
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Mock7579Account {
function execute(address target, uint256 value, bytes calldata callData) external payable {
// 简化:生产环境需校验 msg.sender 是否为已安装模块
(bool success, ) = target.call{value: value}(callData);
require(success, "Mock7579Account: Execution failed");
}
function executeFromExecutor(address target, uint256 value, bytes calldata data) external returns (bytes memory) {
// 实际生产环境这里需要检查 msg.sender 是否为已授权的模块
(bool success, bytes memory result) = target.call{value: value}(data);
require(success, "Execution failed");
return result;
}
receive() external payable {}
}
- 4.1.2 IntentExecutorERC7579
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title IntentExecutor (ERC-7579 兼容)
* @notice 支持定时续费与简单价格意图的执行器模块
*/
contract IntentExecutorERC7579 is ReentrancyGuard {
// ERC-7579 模块类型定义:2 代表 Executor
uint256 public constant TYPE_EXECUTOR = 2;
struct Intent {
uint256 interval; // 时间间隔 (秒),用于续费/定投
uint256 lastTimestamp; // 上次执行时间
uint256 priceThreshold; // 价格阈值 (用于止损,0表示不启用)
address target; // 目标合约 (如 USDC 或 Dex)
bytes callData; // 执行的具体数据
bool active;
}
// 账户地址 => 意图 ID => 意图详情
mapping(address => mapping(bytes32 => Intent)) public intents;
// --- ERC-7579 标准接口 ---
function onInstall(bytes calldata data) external {
// 在安装时可以初始化默认意图,或者留空通过专门函数设置
}
function onUninstall(bytes calldata data) external {
// 卸载逻辑:清理数据(实际需根据 ID 清理,此处为示例)
}
function isModuleType(uint256 typeID) external pure returns (bool) {
return typeID == TYPE_EXECUTOR;
}
// --- 业务逻辑 ---
/**
* @notice 设置一个意图(如:每 30 天扣款,或者价格低于 X 时卖出)
*/
function registerIntent(
bytes32 intentId,
uint256 interval,
uint256 priceThreshold,
address target,
bytes calldata callData
) external {
intents[msg.sender][intentId] = Intent({
interval: interval,
lastTimestamp: block.timestamp,
priceThreshold: priceThreshold,
target: target,
callData: callData,
active: true
});
}
/**
* @notice 由 Keeper (机器人) 触发的执行函数
* @param account 智能钱包地址
* @param intentId 意图唯一标识
* @param currentPrice 当前市场价格 (由 Keeper 提供,实际生产需接 Oracle)
*/
function triggerExecution(address account, bytes32 intentId, uint256 currentPrice) external nonReentrant {
Intent storage intent = intents[account][intentId];
require(intent.active, "Intent not active");
bool timeCondition = (intent.interval > 0 && block.timestamp >= intent.lastTimestamp + intent.interval);
bool priceCondition = (intent.priceThreshold > 0 && currentPrice <= intent.priceThreshold);
require(timeCondition || priceCondition, "Conditions not met");
// 更新状态防止重入/重复执行
intent.lastTimestamp = block.timestamp;
// 【核心】:回调 ERC-7579 账户的执行接口
// 这里的 0 表示 callType (单笔执行),mode 为常量定义
// 实际上 7579 账户会暴露一个特定的执行 EntryPoint
(bool success, ) = account.call(
abi.encodeWithSignature("executeFromExecutor(address,uint256,bytes)", intent.target, 0, intent.callData)
);
require(success, "Execution failed");
}
}
4.2. 测试脚本
- ERC-7579 IntentExecutor 意图交易模块测试
- 场景1:注册定时转账意图 (自动续费)
- 场景2:时间未到触发应失败
- 场景3:满足价格意图 (止损触发)
- 场景4:跨越时间周期后的自动执行
import assert from "node:assert/strict";
import { describe, it, beforeEach } from "node:test";
import { parseEther, encodeAbiParameters, parseAbiParameters, getAddress, keccak256, toHex } from 'viem';
import { network } from "hardhat";
describe("ERC-7579 IntentExecutor 意图交易模块测试", function () {
let executor: any, account: any;
let publicClient: any, owner: any, receiver: any;
const INTENT_ID = keccak256(toHex("SUBSCRIPTION_PAYMENT"));
beforeEach(async function () {
// @ts-ignore
const { viem } = await (network as any).connect();
publicClient = await viem.getPublicClient();
[owner, receiver] = await viem.getWalletClients();
// 部署执行器合约
executor = await viem.deployContract("IntentExecutorERC7579");
// 部署模拟账户(需具备 executeFromExecutor 接口)
account = await viem.deployContract("Mock7579Account");
// 为账户注入资金
await owner.sendTransaction({
to: account.address,
value: parseEther("10"),
});
// 伪装账户发送交易
await publicClient.request({
method: "hardhat_impersonateAccount",
params: [account.address],
});
});
it("场景1:注册定时转账意图 (自动续费)", async function () {
const interval = 86400n; // 1天
const amount = parseEther("1");
// 编码转账 callData: transfer(receiver, amount)
const callData = "0x"; // 简化模拟,MockAccount 只需转发即可
await executor.write.registerIntent(
[INTENT_ID, interval, 0n, receiver.account.address, callData],
{ account: account.address }
);
const intent = await executor.read.intents([account.address, INTENT_ID]);
assert.equal(intent[0], interval); // interval
assert.equal(intent[5], true); // active
});
it("场景2:时间未到触发应失败", async function () {
await executor.write.registerIntent(
[INTENT_ID, 3600n, 0n, receiver.account.address, "0x"],
{ account: account.address }
);
// 时间未跨越,预期失败
await assert.rejects(
executor.write.triggerExecution([account.address, INTENT_ID, 0n]),
(err: any) => err.message.includes("Conditions not met")
);
});
it("场景3:满足价格意图 (止损触发)", async function () {
const stopLossPrice = 2000n;
const currentPrice = 1900n; // 跌破 2000
await executor.write.registerIntent(
[INTENT_ID, 0n, stopLossPrice, receiver.account.address, "0x"],
{ account: account.address }
);
const beforeBalance = await publicClient.getBalance({ address: receiver.account.address });
// 模拟 Keeper 传入当前价格触发
await executor.write.triggerExecution([account.address, INTENT_ID, currentPrice]);
const intent = await executor.read.intents([account.address, INTENT_ID]);
// 检查最后执行时间已更新
assert.notEqual(intent[1], 0n);
});
it("场景4:跨越时间周期后的自动执行", async function () {
const amount = parseEther("0.5");
await executor.write.registerIntent(
[INTENT_ID, 3600n, 0n, receiver.account.address, "0x"],
{ account: account.address }
);
const beforeBalance = await publicClient.getBalance({ address: receiver.account.address });
// 快进 1 小时
await publicClient.request({ method: "evm_increaseTime", params: [3601] });
await publicClient.request({ method: "evm_mine" });
// 触发执行 (价格参数在此不生效)
await executor.write.triggerExecution([account.address, INTENT_ID, 0n]);
// 验证 MockAccount 是否执行了转账(取决于 MockAccount.sol 的具体实现)
// 假设 MockAccount 的 executeFromExecutor 会向 target 转账
const afterBalance = await publicClient.getBalance({ address: receiver.account.address });
// 这里根据 MockAccount 逻辑验证
});
});
4.3. 部署脚本
// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits } from "viem";
async function main() {
// 连接网络
const { viem } = await network.connect({ network: network.name });//指定网络进行链接
// 获取客户端
const [deployer, investor] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address;
console.log("部署者的地址:", deployerAddress);
// 部署IntentExecutorERC7579合约
const IntentExecutorERC7579Artifact = await artifacts.readArtifact("IntentExecutorERC7579");
// 1. 部署合约并获取交易哈希
const IntentExecutorERC7579Hash = await deployer.deployContract({
abi: IntentExecutorERC7579Artifact.abi,
bytecode: IntentExecutorERC7579Artifact.bytecode,
args: [],
});
const IntentExecutorReceipt = await publicClient.waitForTransactionReceipt({
hash: IntentExecutorERC7579Hash
});
console.log("IntentExecutorERC7579合约地址:", IntentExecutorReceipt.contractAddress);
// 部署Mock7579Account合约
const Mock7579AccountArtifact = await artifacts.readArtifact("Mock7579Account");
// 1. 部署合约并获取交易哈希
const Mock7579AccountHash = await deployer.deployContract({
abi: Mock7579AccountArtifact.abi,
bytecode: Mock7579AccountArtifact.bytecode,
args: [],
});
const Mock7579AccountReceipt = await publicClient.waitForTransactionReceipt({
hash: Mock7579AccountHash
});
console.log("Mock7579Account合约地址:", Mock7579AccountReceipt.contractAddress);
}
main().catch(console.error);
总结
ERC-7579 不仅仅是一个接口标准,它是意图交易(Intent-centric) 落地的基石。通过将复杂的逻辑解耦到独立的模块中,我们正在让 Web3 的用户体验向 Web2 的自动化程度靠拢。