EIP-7702:委托执行和赞助交易

EIP-7702模糊了简单钱包和智能合约账户之间的界限,而无需更改用户地址或要求合约部署。本文深入探讨了EIP-7702在实际场景中的工作原理。

EIP-7702:委托执行和赞助交易
一键发币: Aptos | X Layer | SUI | SOL | BNB | ETH | BASE | ARB | OP | Polygon | Avalanche | 用AI学区块链开发

EIP-7702 引入了以太坊外部账户(EOA)操作方式的根本性变化。通过在EOA上启用代码执行,它模糊了简单钱包和智能合约账户之间的界限,而无需更改用户地址或要求合约部署。本文深入探讨了EIP-7702在实际场景中的工作原理。

1、快速上手

要使用EIP-7702将EOA(外部账户)升级为智能合约钱包,我们可以使用Viem.js来签署授权并委托执行到一个合约。

以下示例演示了如何:

  1. 使用EIP-7702签署授权。
  2. 指定智能合约(例如BatchCallAndSponsor)作为要执行的代码。
  3. 广播带有此授权的交易。
const test = privateKeyToAccount(PRIVATE_KEY);  

const walletClient = createWalletClient({  
  account: test,  
  chain: sepolia,  
  transport: http(),  
});  

// 步骤 1:使用 EIP-7702 签署授权  
const authorization = await walletClient.signAuthorization({  
  // 表示发送者(EOA)正在发起交易  
  executor: 'self',  
 // 要执行的合约地址  
  contractAddress: '0x...'  
});  

console.log('授权:', authorization);  

// 步骤 2:附加授权发送交易  
const txHash = await walletClient.sendTransaction({  
  to: test.address,  
  authorizationList: [authorization],  
});  

console.log(`交易: https://eth-sepolia.blockscout.com/tx/${txHash}`);

authorization 对象包含证明该 EOA 想要作为智能合约执行的签名数据:

授权: {  
  address: "0xBDBA7F921f8C8AFF014749Fc17324e832291bfB0",  
  chainId: 11155111,  
  nonce: 1,  
  r: "0x74f9b820c84aab9c2906d54b7b898fb668b1c14c241f5ebda33a54f0240322fe",  
  s: "0x044a1802ef3e817f77ec3535075095720b921c64b5e6da1d11a9cc73ce969f9a",  
  v: 27n,  
  yParity: 0,  
}

一旦交易被广播,你可以在像 Blockscout 这样的区块浏览器中检查它。在授权部分,你会看到与交易一起包含的授权数据。

此部分显示:

  • 权限 — 签署授权的 EOA(即我们的钱包地址)。
  • 合约 — 将处理执行的智能合约地址(在这个例子中是 BatchCallAndSponsor)。

EIP-7702 最有趣的地方在于,该账户现在既充当 EOA 又充当智能合约。

你也可以显式地设置 chainId: 0 使签名在所有 EVM 兼容链上有效。

2、智能合约:BatchCallAndSponsor

EIP-7702 示例的核心是 BatchCallAndSponsor 合约。这个智能 合约 支持批量执行多个调用,并支持自我执行和赞助执行:

contract BatchCallAndSponsor {  
    //....  

    uint256 public nonce;  

    function execute(Call[] calldata calls, bytes calldata signature) external payable {  
        bytes memory encodedCalls;  
        for (uint256 i = 0; i < calls.length; i++) {  
            encodedCalls = abi.encodePacked(encodedCalls, calls[i].to, calls[i].value, calls[i].data);  
        }  
        bytes32 digest = keccak256(abi.encodePacked(nonce, encodedCalls));  

        bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(digest);  

        // 从提供的签名中恢复签名者。  
        address recovered = ECDSA.recover(ethSignedMessageHash, signature);  
        require(recovered == address(this), "无效的签名");  

        _executeBatch(calls);  
    }  

    function execute(Call[] calldata calls) external payable {  
        require(msg.sender == address(this), "无效的权限");  
        _executeBatch(calls);  
    }  

    function _executeBatch(Call[] calldata calls) internal {  
        uint256 currentNonce = nonce;  
        nonce++; // 增量nonce以防止重放攻击  

        for (uint256 i = 0; i < calls.length; i++) {  
            _executeCall(calls[i]);  
        }  

        emit BatchExecuted(currentNonce, calls);  
    }  

    function _executeCall(Call calldata callItem) internal {  
        // 合约中的 address(this) 等于 EOA 地址而不是合约地址  
        (bool success,) = callItem.to.call{value: callItem.value}(callItem.data);  
        require(success, "调用失败");  
        emit CallExecuted(msg.sender, callItem.to, callItem.value, callItem.data);  
    }  
}

BatchCallAndSponsor 合约有两个版本的 execute 函数 —— 每个服务于不同的用例:

  1. execute(calls) — 自我执行
    当授权账户也是广播交易的账户时使用。不需要签名,因为调用来自账户本身。这是一种直接、无信任的执行——非常适合用户想要进行批量转账的情况。
  2. execute(calls, signature) — 赞助执行
    这种版本允许其他人(赞助商或中继器)代表用户提交交易。账户在离线状态下对请求的调用进行签名,合同在链上验证签名。这可以实现无gas流程或委托操作——非常适合用户没有ETH的情况。

两种路径的结果相同:在一个原子交易中执行一批操作。唯一的区别是 谁提交交易是否需要签名

3、EIP-7702 的现实场景

3.1 Alice 有足够的 ETH 用于 Gas

Alice 想要在一个交易中向多个接收者发送 USDT。她有 ETH 来支付交易费用,不想处理第三方合约、代币授权或许可流程。

使用 EIP-7702 自我执行:

  • Alice 签署单个 EIP-7702 授权。
  • 她自己发送交易。
  • 在她的地址上下文中运行逻辑并执行 USDT 转账批次。
const batchAbi = parseAbi([  
  "function execute((address to,uint256 value,bytes data)[] calls) payable",  
]);  

const authorization = await aliceWalletClient.signAuthorization({  
  executor: 'self',  
  contractAddress: '0xBDBA7F921f8C8AFF014749Fc17324e832291bfB0'  
});  

const erc20Abi = parseAbi([  
  'function transfer(address to, uint256 amount) returns (bool)'  
]);  

const recipients = [  
  { to: '0x111..', amount: '500'},  
  { to: '0x222..', amount: '1000'},  
  { to: '0x333..', amount: '1500'},  
];  

const batchCalls = recipients.map(({ to, amount }) => ({  
  to: USDTAddress,  
  value: 0n,  
  data: encodeFunctionData({  
    abi: erc20Abi,  
    functionName: 'transfer',  
    args: [to, parseUnits(amount, 6)],  
  }),  
})) as const;  

const encodedCallData = encodeFunctionData({  
  abi: batchAbi,  
  functionName: "execute",  
  args: [batchCalls],  
});  

const txHash = await aliceWalletClient.sendTransaction({  
  to: alice.address,  
  data: encodedCallData,  
  authorizationList: [authorization],  
});

你可以同时广播交易和 authorizationList,或者事先单独提交授权——两种方法都是有效的。请注意,包括 authorizationList 会增加交易的 gas 成本。

3.2 Alice 没有 ETH —— Bob 赞助交易

Alice 有 USDT 但没有 ETH 来支付 gas 费用。她要求 Bob,他确实有 ETH,代表她广播一个执行她转账的交易。

使用 EIP-7702 委托执行:

  • Alice 在离线状态下签署一批调用(例如,一个 transfer() 到 Kraken)。
  • Bob 将她的签名调用包装成一个交易。
  • 他将交易发送到 Alice 的地址。在链上,它表现得好像 Alice 执行了它一样。

这使得 Alice 在没有 ETH 的情况下具有智能合约级别的行为,而 Bob 覆盖了费用。

// ... transferToBinance, transferToCoinbase, transferEthToKraken ...  

// 1. 编码批处理调用以用于摘要  
const encodedCallBytes = encodePacked(  
  [  
    "address", "uint256", "bytes",  
    "address", "uint256", "bytes",  
    "address", "uint256", "bytes",  
  ],  
  [  
    transferToBinance.to, transferToBinance.value, transferToBinance.data,  
    transferToCoinbase.to, transferToCoinbase.value, transferToCoinbase.data,  
    transferEthToKraken.to, transferEthToKraken.value, transferEthToKraken.data,  
  ]  
);  

// 2. 从 Alice 的智能合约读取当前的 nonce  
const nonce = await publicClient.readContract({  
  address: alice.address,  
  abi: batchAbi,  
  functionName: "nonce",  
});  

// 3. 创建 nonce 和编码调用字节的摘要  
const digest = keccak256(  
  encodePacked(["uint256", "bytes"], [BigInt(nonce), encodedCallBytes])  
);  

// 4. Alice 使用她的私钥在离线状态下签名摘要  
const signature = await sign({  
  hash: toEthSignedMessageHash(digest),  
  privateKey: alice.privateKey,  
  to: "hex",  
});  

// 5. 编码对合约的 execute(calls, signature) 方法的调用  
const encodedCallData = encodeFunctionData({  
  abi: batchAbi,  
  functionName: "execute",  
  args: [calls, signature],  
});  

const txHash = await bobWalletClient.sendTransaction({  
  to: alice.address,  
  data: encodedCallData  
});

在这种情况下,我们没有发送 authorizationList,因为它已经之前发送过了。

4、持久化代码状态和停用

在使用 SetCode 的交易之后,你的 EOA 会继续像智能账户一样运行。你附加的逻辑会一直存在——即使交易结束。

要回到常规的 EOA,你可以提交另一个授权,其中 SetCode 指向 [0x000...000](https://github.com/bluealloy/revm/blob/main/crates/handler/src/pre_execution.rs#L241)(零地址)。

使用任何其他合约地址将简单地用新逻辑替换现有逻辑。如果你想要移除或更改账户的智能行为,这一步是必不可少的。

5、授权 nonce

为了防止重放攻击,我们需要传递一个 nonce。授权 nonce 与授权账户的 nonce 相关,但有一个细微差别需要注意——它取决于谁广播交易。

如果其他人(中继器)广播它,authorization.nonce 应该与授权账户的当前 nonce 匹配。

但如果授权账户本身广播交易,事情就有点复杂了:以太坊在执行授权逻辑之前会增加账户的 nonce。这意味着当 authorization.nonce 检查运行时,账户的 nonce 已经增加了 1。

因此,在这种情况下,你需要设置 authorization.nonce = transaction nonce + 1

例如,如果交易是以 nonce 1 发送的,那么在授权逻辑运行时,账户的 nonce 已经是 2 ——所以 authorization.nonce 必须设置为 2 才能通过检查。

这就是为什么在第一个示例中我们使用 executor: 'self'

const authorization = await walletClient.signAuthorization({  
  executor: 'self', // 👈 这意味着“我是广播者”  
  contractAddress: batchCallAndSponsor  
});

通过设置 executor: 'self',我们表明授权账户也将是广播交易的账户。在后台,Viem 会自动处理 nonce 调整逻辑——确保根据谁发送交易正确设置 authorization.nonce

6、检测升级后的账户

如果你查询一个升级后的 EOA(使用 EIP-7702 的智能账户)的代码,你不会看到典型的合约字节码。相反,你会得到类似:

0xef0100bdba7f921f8c8aff014749fc17324e832291bfb0

这是一个特殊的标记,表示该账户已被委托给一个合约。

在后台,EVM 会将账户的代码设置为 0xef0100 || address

  • 0xef0100 是一个固定前缀。
  • 剩下的 20 字节表示该账户委托给的合约地址。

这种紧凑的格式作为“委托指定”起作用,这是 EIP 标记账户的执行已委托给智能合约的方式。

你可以使用以下方法检查此代码:

export async function getCode(address: Hex) {  
  const code = await publicClient.getCode({ address });  
  console.log(`Code at ${address}:`, code);  
  return code;  
}  

// 示例:  
getCode("0x31a5F38E13Dd08171A48a0748D1aF446D71fF692");

如果返回的代码以 0xef0100 开头,你正在查看的是一个委托的智能账户。

7、安全考虑事项

权力越大,责任越大。签署 EIP-7702 授权相当于将代码执行控制权交给你的账户。这意味着:

  • 仅对已知、可信的合约签署授权。 恶意合约可以包含危险的调用——如耗尽代币或发送 ETH——这些调用将以你的 EOA 的完全权限运行。
  • 仔细审查合约逻辑。 即使交易是中继的,逻辑也会在你的账户地址下执行。
  • 避免在未检查的情况下签署浏览器弹出窗口。 不要盲目签署。

原文链接:EIP-7702: Delegated Execution and Sponsored Transactions

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。