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来签署授权并委托执行到一个合约。
以下示例演示了如何:
- 使用EIP-7702签署授权。
- 指定智能合约(例如
BatchCallAndSponsor)作为要执行的代码。 - 广播带有此授权的交易。
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 函数 —— 每个服务于不同的用例:
execute(calls)— 自我执行
当授权账户也是广播交易的账户时使用。不需要签名,因为调用来自账户本身。这是一种直接、无信任的执行——非常适合用户想要进行批量转账的情况。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翻译整理,转载请标明出处
免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。