链上 Agent 跨合约调用编排:基于 Multicall3 实现无状态原子聚合执行

封面信息图

在设计去中心化多步骤自主交易 Agent 时,很多工程师最容易踩中的一个深坑,就是将复杂的链上动作拆分成多个独立的 RPC 交互:先发一笔交易授权代币,等待出块;再发一笔交易查询链上价格,等待出块;最后发一笔交易执行兑换。

这种“串行化执行”在瞬息万变的链上世界无异于自杀。在这几个区块的等待间隙里,外部流动性池的价格可能早就发生了剧烈滑点,甚至原本有利可图的套利空间早已被潜伏在内存池中的抢跑机器人(Front-runners)掠夺殆尽。更糟糕的是,如果第二笔交易成功而第三笔交易因网络拥堵 revert,账户的资金就会陷入进退两难的未决中间态。

要实现真正工业级的链上自动化执行,必须利用 Multicall3 协议将多个只读查询与写操作聚合为单笔原子交易。

通过在单次以太坊状态转换(State Transition)内完成“无状态数据抓取 -> 动态计算 -> 条件执行 -> 状态校验”,Agent 才能真正具备抗滑点漂移和防抢跑的确定性执行力。


一、为什么多调用聚合是自主 Agent 的生命线

以太坊交易具备天然的“原子性(Atomicity)”:要么整笔交易的所有操作全部执行成功,要么任何一步失败则全盘回滚(Revert),恢复到最初的状态。

Multicall3 是全网部署在相同固定地址(0xcA11bde05977b3631167028862bE2a173976CA11)的标准基础设施合约。它为 Agent 带来了三大核心武器:

  1. 零状态时间旅行与强一致性快照:
    在单笔 Multicall 中批量读取 20 个不同代币对的价格、储备量和区块时间戳。所有数据全部取自同一个区块高度(Block Number),彻底消灭了因跨块查询引发的时序脏读。
  2. 容错与可选执行(AllowFailure):
    Multicall3 的 aggregate3 接口允许为每一个子调用指定 allowFailure: true。当 Agent 尝试在 5 个不同的流动性池中探测流动性时,即使某一个池子暂停或回滚,也不会导致整个任务崩溃,未失败的子调用依然能够平稳执行并返回具体数据。
  3. 批量写操作的 Gas 极限压缩:
    每一笔独立的以太坊交易都需要固定支付 21,000 点的基础固有 Gas(Intrinsic Gas)并消耗独立的签名校验。将 5 个动作合并进同一个 Multicall,直接省去了 4 次 21,000 点的基础开销,为高频策略节省下宝贵的摩擦成本。

二、架构设计:Agent 编排与调用栈拓扑

[Agent 链下决策引擎]
          │
          ▼ 构造批量 Call3[] 数组
┌───────────────────────────────────────────────────────────┐
│ Call 1: 预言机查询最新价格 (allowFailure: false)          │
│ Call 2: 目标代币授权 approve (allowFailure: false)        │
│ Call 3: DEX 执行 swapExactTokens (allowFailure: false)    │
│ Call 4: 校验最终钱包余额增量是否达标 (allowFailure: false) │
└─────────────────────────────┬─────────────────────────────┘
                              │
                              ▼ 单笔交易广播
             [Multicall3 合约: aggregate3Value]
                              │
               ┌──────────────┼──────────────┐
               ▼              ▼              ▼
         [Oracle 合约]   [ERC20 合约]   [Uniswap 合约]

如果在最后一项校验中发现兑换后的收益未达到预期阈值,整笔 Multicall 瞬间全量回滚,除了极少量的 Gas 之外,Agent 的本金毫发无损。


三、代码实操:基于 Python 与 Web3 的原子调用流水线

下面的 Python 核心代码演示了 Agent 如何使用 Multicall3 在一笔交易内并发聚合查询与执行:

from web3 import Web3
from eth_abi import encode, decode

# Multicall3 全网唯一定址
MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"

MULTICALL3_ABI = [
    {
        "inputs": [
            {
                "components": [
                    {"name": "target", "type": "address"},
                    {"name": "allowFailure", "type": "bool"},
                    {"name": "callData", "type": "bytes"}
                ],
                "name": "calls",
                "type": "tuple[]"
            }
        ],
        "name": "aggregate3",
        "outputs": [
            {
                "components": [
                    {"name": "success", "type": "bool"},
                    {"name": "returnData", "type": "bytes"}
                ],
                "name": "returnData",
                "type": "tuple[]"
            }
        ],
        "stateMutability": "payable",
        "type": "function"
    }
]

class MulticallAgentOrchestrator:
    def __init__(self, rpc_url: str, private_key: str):
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        self.account = self.w3.eth.account.from_key(private_key)
        self.multicall = self.w3.eth.contract(
            address=MULTICALL3_ADDRESS,
            abi=MULTICALL3_ABI
        )

    def execute_atomic_batch(self, targets: list, calldatas: list, allow_failures: list) -> list:
        """
        组装 Call3 结构体并提交原子打包执行
        """
        calls = []
        for target, data, allow_fail in zip(targets, calldatas, allow_failures):
            calls.append((
                Web3.to_checksum_address(target),
                allow_fail,
                bytes.fromhex(data.replace("0x", ""))
            ))

        # 估算 Gas 并构造交易
        tx = self.multicall.functions.aggregate3(calls).build_transaction({
            'from': self.account.address,
            'nonce': self.w3.eth.get_transaction_count(self.account.address),
            'gas': 800000,
            'maxFeePerGas': self.w3.to_wei(35, 'gwei'),
            'maxPriorityFeePerGas': self.w3.to_wei(2, 'gwei')
        })

        signed_tx = self.w3.eth.account.sign_transaction(tx, self.account.key)
        tx_hash = self.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
        
        receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
        print(f"原子编排调用成功落盘,Tx: {receipt.transactionHash.hex()}")
        return receipt

四、生产避坑与安全边界

在为自主 Agent 编排 Multicall 链路时,必须谨记以下两条铁律:

  1. 防范 msg.value 多重消费漏洞(Multicall with Value Trap):如果调用的目标方法包含原生 ETH 转移(如 payable 函数),绝对禁止在循环调用中使用原始的 msg.value!如果外部向 Multicall 传入了 1 ETH,而内部有两个子调用都去读取 msg.value,这会导致同一个 ETH 被计算了两次,直接引发资金被套取的严重漏洞。对于涉及原生资产的操作,应通过 aggregate3Value 精准指定每个子调用携带的精确 value 额度。
  2. 正确处理失败返回数据(Revert Reason Decoding):当某个子调用设置了 allowFailure: false 并发生回滚时,Multicall3 会把底层合约抛出的错误选择器封装在 returnData 中向外冒泡。Agent 客户端必须集成错误解码器,准确识别是“流动性不足”还是“滑点超标”,从而为下一轮模型重试提供精确的上下文反馈。

用原子编排终结串行调用的碎片化风险,这是 AI Agent 在高速链上博弈中立于不败之地的标准战术。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐