EIP-7702 技术详解

授权机制与实现原理

EIP-7702 技术概述

EIP-7702(以太坊改进提案7702)引入了一种新的机制,允许外部拥有账户(EOA)临时获得智能合约的功能,而无需将其转换为合约账户。这种机制被称为"账户委托"(Account Delegation)。

EIP-7702 的核心思想是:允许用户签署一个特殊的授权消息,指定一个合约地址,然后在交易执行时,用户的 EOA 临时获得该合约的代码,使其能够执行智能合约功能。

授权消息格式

EIP-7702 定义了一种特殊的授权消息格式,用户需要使用其私钥签署这个消息:

// EIP-7702 授权消息结构
struct Authorization {
  address contractAddress; // 被授权的合约地址
  uint256 validUntil; // 授权有效期(可选)
  uint256 validAfter; // 授权生效时间(可选)
  bytes32 nonce; // 防止重放攻击的随机数
}

在 viem 库中,签署授权的代码如下:

// 使用 viem 库签署 EIP-7702 授权
const authorization = await wallet.signAuthorization({
  contractAddress: BATCH_CALL_DELEGATION,
  // 可选参数
  // validUntil: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1小时后过期
  // validAfter: BigInt(Math.floor(Date.now() / 1000)), // 立即生效
});

技术实现原理

EIP-7702 的实现涉及以太坊协议层的修改,主要包括以下几个方面:

  1. 授权验证:以太坊节点验证用户提交的授权签名是否有效
  2. 代码注入:临时将授权合约的代码注入到用户的 EOA 地址
  3. 上下文保持:在执行过程中保持 msg.sender 为用户的 EOA 地址
  4. 状态恢复:交易执行完成后恢复 EOA 的原始状态

关键技术点:当 EOA 被委托给合约后,它会临时获得该合约的代码,但在执行过程中,msg.sender 仍然是 EOA 的地址,而不是合约地址。这使得 EOA 可以直接调用其他合约,就好像是 EOA 自己在调用一样。

批处理合约示例

以下是一个简单的批处理合约示例,它允许用户在一次交易中执行多个操作:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract BatchCallDelegation {
  struct Call {
    bytes data;
    address to;
    uint256 value;
  }

  function execute(Call[] calldata calls) external payable {
    for (uint256 i = 0; i < calls.length; i++) {
      Call memory call = calls[i];
      (bool success,) = call.to.call{ value: call.value }(call.data);
      require(success, "call reverted");
    }
  }
}

这个合约非常简单,它只有一个 execute 函数,接受一个 Call 结构体数组,然后依次执行每个调用。

注意: 这个合约没有任何访问控制,任何人都可以调用它的 execute 函数。在实际应用中,您可能需要添加适当的访问控制机制。

为什么不需要传统的代币授权

在传统的 ERC20 代币交互中,如果一个合约要代表用户转移代币,需要以下步骤:

  1. 用户调用代币合约的 approve() 函数,允许第三方合约使用一定数量的代币
  2. 第三方合约调用 transferFrom() 函数来实际转移这些代币

这个过程需要两个交易,用户体验不佳。而使用 EIP-7702,情况完全不同:

技术原理解析

当使用 EIP-7702 授权执行交易时:

  1. 用户的 EOA 临时获得批处理合约的代码
  2. 批处理合约调用代币合约的 transfer() 函数
  3. 代币合约看到的 msg.sender 是用户的 EOA 地址,而不是批处理合约的地址
  4. 因此,代币合约直接从用户的余额中扣除代币,而不需要事先批准

这就是为什么使用 EIP-7702 时,批处理合约可以直接调用代币的 transfer() 函数而不需要事先获得授权的原因。

// 传统方式:需要两个交易
// 交易1:授权
await token.approve(batchContract.address, amount);

// 交易2:转账
await batchContract.execute([{ to: token.address, data: transferFromData }]);

// EIP-7702方式:只需一个交易
await wallet.writeContract({
  abi: batchAbi,
  address: account.address,
  functionName: "execute",
  args: [[{ to: token.address, data: transferData }]],
  authorizationList: [authorization],
});

EIP-7702 与 ERC-4337 的比较

EIP-7702 和 ERC-4337(账户抽象)都旨在改善以太坊的用户体验,但它们的方法和目标有所不同:

特性 EIP-7702 ERC-4337
账户类型 保持 EOA 不变,临时赋予合约功能 创建新的智能合约账户
实现方式 协议层修改 应用层实现
兼容性 需要网络升级 在现有网络上可用
用户迁移 无需迁移 需要创建新账户
Gas 支付 用户支付 可由第三方支付

EIP-7702 可以被视为向完全账户抽象过渡的一个中间步骤,它为现有的 EOA 用户提供了智能合约功能,而无需迁移到新的账户系统。

安全考虑

使用 EIP-7702 时,需要考虑以下安全问题:

警告: 授权一个合约意味着该合约可以代表您执行操作,包括转移您的资产。只授权您信任的合约,并且仔细检查合约的功能和安全性。

代码示例:批量转账 ETH 和代币

以下是一个完整的代码示例,展示如何使用 EIP-7702 在一次交易中同时转账 ETH 和代币:

import { createWalletClient, http, parseEther, parseAbi, createPublicClient, encodeFunctionData } from "viem";
import { anvil } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { eip7702Actions } from "viem/experimental";

const ALICE_PK = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const BOB = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
const BATCH_CALL_DELEGATION = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const SIMPLE_TOKEN = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";

const main = async () => {
  const account = privateKeyToAccount(ALICE_PK);

  const clients = {
    wallet: createWalletClient({ chain: anvil, transport: http(), account }).extend(eip7702Actions()),
    public: createPublicClient({ chain: anvil, transport: http() }),
  };

  // 签署授权
  const authorization = await clients.wallet.signAuthorization({
    contractAddress: BATCH_CALL_DELEGATION,
  });

  // 准备代币转账数据
  const tokenAbi = parseAbi([
    "function transfer(address,uint256) returns (bool)",
  ]);

  const tokenTransferData = encodeFunctionData({
    abi: tokenAbi,
    functionName: "transfer",
    args: [BOB, 100n * 10n ** 18n],
  });

  // 批量执行ETH和代币转账
  const batchAbi = parseAbi(["function execute((bytes data,address to,uint256 value)[])"]);

  await clients.wallet.writeContract({
    abi: batchAbi,
    address: account.address,
    functionName: "execute",
    args: [
      [
        // ETH转账
        {
          data: "0x",
          to: BOB,
          value: parseEther("1"),
        },
        // 代币转账
        {
          data: tokenTransferData,
          to: SIMPLE_TOKEN,
          value: 0n,
        },
      ],
    ],
    authorizationList: [authorization],
  });

  console.log(">>> 批量转账成功完成");
};

main();