深度解析 ERC-7579:如何构建一个支持“意图交易”的模块化执行器

6 阅读7分钟

前言

随着账户抽象(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 定义的 isModuleTypeonInstall 接口,该合约可以无缝安装到任何兼容标准的钱包(如 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 = 2000callData 为 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 的自动化程度靠拢。