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 感觉抽象或复杂,不用担心!我们将通过一个动手演示来分解它——部署一个实现合约并测试其功能。
在本节中,你将学习如何使用 Foundry 实现 EIP-7702 功能,Foundry 是一个强大的智能合约开发工具。
Foundry Cheatcodes Foundry 提供了一组 cheatcodes——特殊的命令,用于修改以太坊虚拟机(EVM)的行为以简化测试。在本指南中,我们将使用
signDelegation
、attachDelegation
和signAndAttachDelegation
cheatcodes 来测试 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-contracts
forge-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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!