EIP-7702引入了一种新的交易类型0x4,使外部账户(EOA)能够执行临时的智能合约功能,支持批量交易、赞助Gas支付等功能。文章详细介绍了EIP-7702的技术细节、使用场景,并通过Foundry工具展示了如何测试和部署该功能。
EIP-7702 在 以太坊的 Pectra 升级 中引入了一种新的交易类型 0x4,使外部拥有账户(EOAs)能够执行临时的智能合约功能。这一账户抽象的进步弥合了 EOAs 和智能合约之间的差距,实现了批量交易、赞助 gas 支付和受控访问委托等关键功能。
更新:当前 EIP-7702 已经在主网和Sepolia 测试网上线,但可以通过本地 Foundry 环境 fork 的主网环境测试。在本指南中,我们将设置一个全新的本地网络。有关如何分叉主网的更多信息,请查看我们的 如何使用 Foundry 分叉以太坊区块链 指南。
本指南将带你了解 EIP-7702 的技术细节、用例以及如何使用 Foundry 和 Foundry 的 cheatcodes 进行测试。通过本指南,你将清楚地了解如何在项目中利用 EIP-7702,通过部署一个实现合约来查看 EIP-7702 的实际应用,并测试 EIP-7702 的功能。
0x4 交易0x4 交易,查看 EIP‑7702 的实际应用虽然典型的以太坊交易要么是转账,要么是与智能合约交互,但新的 0x4 交易类型允许 EOA 直接执行代码。这为外部拥有账户(EOAs)解锁了新的可能性,使它们能够更像智能合约一样运作。
通过这一新标准,EOAs 可以直接从自己的地址执行智能合约逻辑,从而实现以下功能:
以太坊账户类型
通过 本指南 了解更多关于以太坊账户类型的信息。
用户(EOA)签署一个授权消息,该消息包括链 ID、nonce、委托地址和签名部分(y_parity、r、s)。这生成了一个签名授权,确保只有批准的实现合约可以执行交易,并防止重放攻击。
对于每个授权的委托地址,用户(EOA)存储一个委托指示器(delegation designator),指向 EOA 将委托给的实现合约。当用户(或赞助者)执行 EIP-7702 交易时,它会从该指示器指示的地址加载并运行代码。
在典型的以太坊交易中,如果你想调用智能合约上的函数,你将 to 字段设置为该合约的地址,并包含适当的 data 以调用其函数。在 EIP-7702 中,你将 to 字段设置为 EOA 的地址,并包含 data 以调用实现合约的函数,同时附带签名授权消息。
以下代码片段展示了如何使用 Viem 的钱包客户端为 EIP‑7702 智能账户构建批量交易。
to 字段设置为智能账户自己的地址。data 字段通过编码对 execute 函数的调用创建,该函数包含两个调用对象的数组。execute 函数应在实现合约中定义,并处理批量交易逻辑。authorizationList 中包含签名授权,允许智能账户将执行委托给实现合约。如果另一个钱包(赞助者)想要执行此交易(赞助交易),它可以使用相同的授权签名将执行委托给实现合约。
注意: 实现合约应设计为处理由 EIP-7702 启用的批量交易和其他功能。此外,它们应包括 nonce 和重放保护机制,以防止未经授权的交易。
import { createWalletClient, http, parseEther } from 'viem'
import { anvil } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { eip7702Actions } from 'viem/experimental'
import { abi, contractAddress } from './contract' // 假设你已经部署了合约并在单独的文件中导出了 ABI 和合约地址
const account = privateKeyToAccount('0x...')
const walletClient = createWalletClient({
  account,
  chain: anvil,
  transport: http(),
}).extend(eip7702Actions())
const authorization = await walletClient.signAuthorization({
  contractAddress,
})
const hash = await walletClient.sendTransaction({
  authorizationList: [authorization],
  data: encodeFunctionData({
    abi,
    functionName: 'execute',
    args: [
      [
        {
          data: '0x',
          to: '0xcb98643b8786950F0461f3B0edf99D88F274574D',
          value: parseEther('0.001'),
        },
        {
          data: '0x',
          to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
          value: parseEther('0.002'),
        },
      ],
    ]
  }),
  to: walletClient.account.address,
})
如果 EIP-7702 感觉抽象或复杂,不用担心!我们将通过一个动手演示来分解它——部署一个实现合约并测试其功能。
备注:EIP-7702 在设置 Delegation indicator Code (0xef0100 + 委托地址 )后,不会还原为EOA , 只有在委托到零地址时,才会将 EOA 委托指示代码清空。 但是另外一个角度:EOA 在执行交易时,加载的委托代码,是仅在当前交易周期内生效
在本节中,你将学习如何使用 Foundry 实现 EIP-7702 功能,Foundry 是一个强大的智能合约开发工具。
Foundry Cheatcodes Foundry 提供了一组 cheatcodes——特殊的命令,用于修改以太坊虚拟机(EVM)的行为以简化测试。在本指南中,我们将使用
signDelegation、attachDelegation和signAndAttachDelegationcheatcodes 来测试 EIP‑7702 功能。有关更多信息,请查看 Foundry Cheatcodes 文档。
在本项目中,我们部署了一个名为 BatchCallAndSponsor 的实现合约,该合约支持以下功能:
项目包括以下文件:
BatchCallAndSponsor.sol – 包含批量交易和赞助交易的核心逻辑。BatchCallAndSponsor.t.sol – 包括直接执行和赞助执行的单元测试。BatchCallAndSponsor.s.sol – 用于部署合约并执行网络交易的脚本。MockERC20.sol – 用于测试代币转账的模拟 ERC-20 代币合约。警告
本项目中的所有材料和代码仅用于教育目的。它们不适用于生产环境。
如果尚未安装 Foundry,请使用以下命令进行安装:
curl -L https://foundry.paradigm.xyz | bash然后,重新启动终端并运行:
foundryup这将确保你安装了最新版本。
如果你想从头开始设置项目,请初始化一个新的 Foundry 项目:
forge init eip-7702-project
cd eip-7702-project或者,你可以克隆 QuickNode 示例项目:
git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/ethereum/eip-7702安装所需的库:
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-contractsforge-std:提供测试的实用函数。openzeppelin-contracts:包括 ERC-20 实现和加密实用工具。为了简化导入路径,请运行以下命令。它将在项目根目录中创建一个 remappings.txt 文件,包括所需的重映射。
forge remappings > remappings.txt这将确保像以下这样的合约导入能够正确工作,而无需长相对路径:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";修改 foundry.toml 以确保与 EIP‑7702 兼容,设置 Prague 硬分叉。在 [profile.default] 部分添加以下行:
evm_version = "prague"这是必要的,因为 EIP‑7702 仅在 Prague 升级之后可用。
如果你使用示例项目,可以跳过此部分及以下步骤。
在 src 文件夹中,创建一个名为 BatchCallAndSponsor.sol 的新文件,并从 EIP-7702 示例项目 中添加合约逻辑。这是一个支持批量交易和赞助 gas 的基本实现合约。
不要忘记从 src 文件夹中删除任何未使用的文件(例如 Counter.sol)。
实现合约的详细信息在 实现合约详解 部分提供。
在 test 文件夹中,创建一个名为 BatchCallAndSponsor.t.sol 的文件来测试合约。从 EIP-7702 示例项目 中添加测试用例。
不要忘记从 test 文件夹中删除任何未使用的文件(例如 Counter.t.sol)。
测试用例的详细信息在 测试用例详解 部分提供。
在 script 文件夹中创建一个名为 BatchCallAndSponsor.s.sol 的文件,用于部署合约并执行交易。从 EIP-7702 示例项目 中添加部署脚本。
不要忘记从 script 文件夹中删除任何未使用的文件(例如 Counter.s.sol)。
部署脚本的详细信息在 部署脚本详解 部分提供。
项目结构如下:
├── README.md
├── foundry.toml                    # Foundry 配置文件
├── lib                             # 已安装的包
│   ├── forge-std
│   └── openzeppelin-contracts
├── remappings.txt                  # 重映射文件
├── script
│   └── BatchCallAndSponsor.s.sol   # 部署脚本
├── src
│   └── BatchCallAndSponsor.sol     # 实现合约
└── test
    └── BatchCallAndSponsor.t.sol   # 测试用例如果你想跳过详解并直接进入操作,请转到 [运行和测试 EIP-7702 项目](#运行和测试 EIP-7702 项目) 部分。
BatchCallAndSponsor 合约是一个支持批量交易和赞助 gas 的简单实现合约。
请查看项目中的 BatchCallAndSponsor.sol 文件以获取完整实现。以下部分提供了合约功能的简要概述;但请注意,未包含完整的实现代码。
合约中的 execute 函数接受一个 Call 结构体数组,每个结构体代表一个不同的调用,并指定目标地址、值(以 Ether 为单位)和调用数据。
struct Call {
    address to;
    uint256 value;
    bytes data;
}
function execute(Call[] calldata calls) external payable {
    require(msg.sender == address(this), "Invalid authority");
    _executeBatch(calls);
}
function _executeBatch(Call[] calldata calls) internal {
    uint256 currentNonce = nonce;
    nonce++;
    for (uint256 i = 0; i < calls.length; i++) {
        _executeCall(calls[i]);
    }
    emit BatchExecuted(currentNonce, calls);
}
function _executeCall(Call calldata callItem) internal {
    (bool success,) = callItem.to.call{value: callItem.value}(callItem.data);
    require(success, "Call reverted");
    emit CallExecuted(msg.sender, callItem.to, callItem.value, callItem.data);
}合约使用 OpenZeppelin 的 ECDSA 库和 MessageHashUtils 验证签名。签名消息包括调用者地址、目标合约、调用和 nonce。
bytes32 digest = keccak256(abi.encodePacked(nonce, encodedCalls));
require(ECDSA.recover(digest, signature) == msg.sender, "Invalid signature");合约支持直接执行或赞助执行。
function execute(Call[] calldata calls) external payable {
    // 调用者直接执行调用
}
function execute(Call[] calldata calls, bytes calldata signature) external payable {
    // 赞助者代表调用者执行调用
}合约使用 nonce 防止重放攻击。每次成功执行后,nonce 都会递增。如果不实现 nonce,攻击者可以多次重放相同的交易。
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);
}BatchCallAndSponsor.t.sol 文件包含 BatchCallAndSponsor 合约的测试用例。测试用例涵盖了直接执行和赞助执行场景、重放保护和错误处理。
以下部分提供了一些测试代码的见解,但并未包含所有代码。完整的测试文件请参考 BatchCallAndSponsor.t.sol 文件。
testDirectExecution 函数测试调用者(即 Alice)直接执行调用。它验证 Alice 在单个交易中向 Bob 发送 1 ETH 和 100 个代币。
在此测试中,Alice 授权实现合约代表她执行交易。我们使用 signAndAttachDelegation cheatcode 签署授权消息并将其附加到交易中。
然后,Alice 自己在Alice 的 EOA 上调用 execute 函数,并传递调用数组,这在没有 EIP-7702 的情况下是不可能的。
testDirectExecution 函数的部分代码
function testDirectExecution() public {
    console2.log("Sending 1 ETH from Alice to Bob and transferring 100 tokens to Bob in a single transaction");
    BatchCallAndSponsor.Call[] memory calls = new BatchCallAndSponsor.Call[](2);
    // ETH transfer
    calls[0] = BatchCallAndSponsor.Call({to: BOB_ADDRESS, value: 1 ether, data: ""});
    // Token transfer
    calls[1] = BatchCallAndSponsor.Call({
        to: address(token),
        value: 0,
        data: abi.encodeCall(ERC20.transfer, (BOB_ADDRESS, 100e18))
    });
    vm.signAndAttachDelegation(address(implementation), ALICE_PK);
    vm.startPrank(ALICE_ADDRESS);
    BatchCallAndSponsor(ALICE_ADDRESS).execute(calls);
    vm.stopPrank();
    assertEq(BOB_ADDRESS.balance, 1 ether);
    assertEq(token.balanceOf(BOB_ADDRESS), 100e18);
}testSponsoredExecution 函数测试了由赞助者(即 Bob)发起的调用的赞助执行。它验证了第三方(Bob)可以代表 Alice 执行交易。我们验证了发送者是 Bob,而不是 Alice,并且接收者已收到资金。
在此测试中,Alice 签署了一份授权,允许 implementation 代表她执行交易。Bob 附加了 Alice 签署的授权并进行广播。
然后,Alice 签署交易,Bob 通过 Alice 临时分配的合约执行交易。
最后,execute 函数在 Alice 的 EOA 上由 Bob 调用,而不是 Alice。
testSponsoredExecution 函数的一部分
function testSponsoredExecution() public {
    // Arrange the call(s).
    calls[0] = BatchCallAndSponsor.Call({to: recipient, value: 1 ether, data: ""});
    // Alice signs a delegation allowing `implementation` to execute transactions on her behalf.
    Vm.SignedDelegation memory signedDelegation = vm.signDelegation(address(implementation), ALICE_PK);
    // Bob attaches the signed delegation from Alice and broadcasts it.
    vm.startBroadcast(BOB_PK);
    vm.attachDelegation(signedDelegation);
    // Prepare the signature for the transaction.
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(ALICE_PK, MessageHashUtils.toEthSignedMessageHash(digest));
    bytes memory signature = abi.encodePacked(r, s, v);
    // Expect the event. The first parameter should be BOB_ADDRESS.
    vm.expectEmit(true, true, true, true);
    emit BatchCallAndSponsor.CallExecuted(BOB_ADDRESS, calls[0].to, calls[0].value, calls[0].data);
    // As Bob, execute the transaction via Alice's temporarily assigned contract.
    BatchCallAndSponsor(ALICE_ADDRESS).execute(calls, signature);
    vm.stopBroadcast();
    assertEq(recipient.balance, 1 ether);
}testWrongSignature 函数测试了签名不正确的场景。它验证了如果签名无效,合约会回滚。
testWrongSignature 函数的一部分
function testWrongSignature() public {
        // Bob attaches the signed delegation from Alice and broadcasts it.
        vm.startBroadcast(BOB_PK);
        vm.attachDelegation(signedDelegation);
        // Sign with the wrong key (Bob's instead of Alice's).
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(BOB_PK, MessageHashUtils.toEthSignedMessageHash(digest));
        bytes memory signature = abi.encodePacked(r, s, v);
        vm.expectRevert("Invalid signature");
        BatchCallAndSponsor(ALICE_ADDRESS).execute(calls, signature);
        vm.stopBroadcast();
    }testReplayProtection 函数测试了重放保护机制。它验证了如果多次使用相同的签名,合约会回滚。
testReplayProtection 函数的一部分
function testReplayAttack() public {
    // Bob attaches the signed delegation from Alice and broadcasts it.
    vm.startBroadcast(BOB_PK);
    vm.attachDelegation(signedDelegation);
    uint256 nonceBefore = BatchCallAndSponsor(ALICE_ADDRESS).nonce();
    bytes32 digest = keccak256(abi.encodePacked(nonceBefore, encodedCalls));
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(ALICE_PK, MessageHashUtils.toEthSignedMessageHash(digest));
    bytes memory signature = abi.encodePacked(r, s, v);
    // First execution: should succeed.
    BatchCallAndSponsor(ALICE_ADDRESS).execute(calls, signature);
    vm.stopBroadcast();
    // Attempt a replay: reusing the same signature should revert because nonce has incremented.
    vm.expectRevert("Invalid signature");
    BatchCallAndSponsor(ALICE_ADDRESS).execute(calls, signature);
}请查看项目中的 BatchCallAndSponsor.s.sol 文件以获取完整的部署脚本。以下部分提供了简要概述,但并未包含所有代码。
BatchCallAndSponsor 脚本文件的一部分
function run() external {
    // Start broadcasting transactions with Alice's private key.
    vm.startBroadcast(ALICE_PK);
    // Deploy the delegation contract (Alice will delegate calls to this contract).
    implementation = new BatchCallAndSponsor();
    // Deploy an ERC-20 token contract where Alice is the minter.
    token = new MockERC20();
    // // Fund accounts
    token.mint(ALICE_ADDRESS, 1000e18);
    vm.stopBroadcast();
    // Perform direct execution
    performDirectExecution();
    // Perform sponsored execution
    performSponsoredExecution();
}在终端中运行以下命令以启动一个本地网络,使用 Prague 硬分叉。
anvil --hardfork prague在另一个终端中,运行以下命令以安装依赖项。
forge install然后,运行以下命令以构建合约。
forge build构建合约后,运行以下命令以运行测试用例。如果你想为所有测试显示堆栈跟踪,请使用 -vvvv 标志,而不是 -vvv。
forge test -vvv输出应如下所示:
Ran 4 tests for test/BatchCallAndSponsor.t.sol:BatchCallAndSponsorTest
[PASS] testDirectExecution() (gas: 128386)
Logs:
  Sending 1 ETH from Alice to Bob and transferring 100 tokens to Bob in a single transaction
[PASS] testReplayAttack() (gas: 114337)
Logs:
  Test replay attack: Reusing the same signature should revert.
[PASS] testSponsoredExecution() (gas: 110461)
Logs:
  Sending 1 ETH from Alice to a random address while the transaction is sponsored by Bob
[PASS] testWrongSignature() (gas: 37077)
Logs:
  Test wrong signature: Execution should revert with 'Invalid signature'.
Suite result: ok. 4 passed; 0 failed; 0 skipped;现在你已经设置好项目,是时候运行部署脚本了。该脚本部署合约,铸造代币,并测试批量执行和赞助执行功能。
我们使用以下命令:
--broadcast: 将交易广播到你的本地网络。--rpc-url 127.0.0.1:8545: 连接到你的本地网络。--tc BatchCallAndSponsorScript: 指定脚本的目标合约。forge script ./script/BatchCallAndSponsor.s.sol --tc BatchCallAndSponsorScript --broadcast --rpc-url 127.0.0.1:8545可能会出现类似以下的警告消息:
Warning: Script contains a transaction to 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 which does not contain any code.
Do you wish to continue?此警告是预期的,因为在 EIP‑7702 生效之前,EOA 不具有任何链上代码。只需输入 y 以继续。
脚本完成后,你将看到日志消息,指示链上执行成功。交易保存在 broadcast 文件夹中,任何敏感值保存在 cache 文件夹中。
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: /project-path/broadcast/...
Sensitive values saved to: /project-path/cache/...你可以检查保存的交易以了解 EIP-7702 交易的执行方式。
你可以查看保存的交易以了解 EIP‑7702 交易的执行方式。例如,在最新的赞助执行交易中,Bob 是发送者,而合约地址是 Alice 的。此外,该交易包括一个包含签名授权的 authorizationList。
恭喜你!你已成功使用 Foundry 部署和测试 EIP-7702 功能。
EIP-7702 的开发和探索得益于以太坊生态系统中许多个人和项目的贡献。我们要感谢以下人员:
EIP-7702 为批量交易、委托执行和赞助交易等创新用例打开了大门。这是一个非常新颖的功能,我们期待看到它如何改变我们与智能合约的交互方式。
在本指南中,我们介绍了 EIP-7702 的基础知识、用例以及如何使用 Foundry 实现它。我们还通过部署实现合约并在本地网络上测试其功能来测试 EIP-7702 功能。我们希望本指南能为你提供对 EIP-7702 及其潜在应用的深入理解。
[让我们知道](https://airtable.com/shrKKKP7O1Uw3ZcUB?prefill_Guide+Name=EIP-7702 Implementation Guide%3A Build and Test Smart Accounts) 如果你有任何反馈或对新主题的请求。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!