
假设你要执行一笔复杂的一体化原子交易,需要兑换代币、添加流动性、质押奖励。可问题是,你的普通 Ethereum 钱包 EOA 做不了批量交易。所以,你得签三笔独立的交易,付三次 gas,这可万万不行,然后还得祈祷 MEV 机器人别在步骤之间把你夹成三明治。
这是最近 EVM 生态里讨论最激烈的话题之一,我们到底要怎样才能赋予普通钱包超能力,同时又不把所有东西一下子全搞砸?
这个挑战催生了两个 EIPs,先是 EIP-3074,随后是 EIP-7702。两者都想弥合简单的 EOA 与 smart contract accounts 之间的差距,但它们在委托权限上采取了不同的方式。一个提供持久的权力,事情可能会变得非常糟糕,你的钱包会被掏空。另一个既能给你持久委托,也能给你临时委托,后者可以在你的交易上链执行的那一刻就消失。
我们来聊聊为什么这个区别很重要,以及 Vitalik 的提案是如何可能让 Ethereum 躲过一场潜在的安全噩梦的。
在进入委托这个话题之前,有一块重要的基础设施让这一切成为可能,那就是 EIP-2718。可以把它看作 Ethereum 为交易打造的集装箱系统。
// before eip-2718 all transactions looked the same
const legacyTx = rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s]);
// after eip-2718 transactions can now be typed
const typedTx = TransactionType || TransactionPayload;EIP-2718 引入了 Typed Transaction Envelopes,其中 TransactionType || TransactionPayload 就是一笔有效的交易。TransactionType 标识交易的格式,而 Payload 包含交易的内容。这套包装系统意味着,我们可以在不破坏现有功能的情况下引入全新的交易行为。
引入信封交易类型之后,我们只需要保证与现有交易的向后兼容,从那以后,只需解决一个简单得多的问题:确保各个 TransactionTypes 之间没有编号冲突。所以当 EIP-7702 需要引入它的临时委托机制时,它只需定义一个新的交易类型,例如 0x04,而不需要对 EVM 本身做任何改动。
EIP-3074 在委托上采取了大胆的做法,通过两个新的 EVM opcodes,也就是 AUTH 和 AUTHCALL,让 EOAs 能够把权限永久委托给智能合约。
机制很直接
// user signs an auth message
bytes32 commit = keccak256(abi.encode(invokerAddress, batchActions));
bytes memory signature = signMessage(userPrivateKey, commit);
// invoker contract calls AUTH opcode
AUTH(commit, signature.v, signature.r, signature.s);
// This sets the 'authorized' context variable to user's address
// now invoker can make calls as the user
AUTHCALL(gasLimit, targetContract, 0, 0, calldata, calldataLength, 0, 0);AUTH 接收四个参数,commit 以及签名的 yParity、r 和 s,如果签名有效,并且签名者地址等于 authority,上下文变量 authorized 就会被设置为该 authority。随后,AUTHCALL 允许被授权的 invoker 合约发送交易,此时 CALLER 被设置为被授权的地址,而不是 invoker。
现在,想象一个能处理批量交易的 invoker 合约
contract BatchInvoker {
struct BatchCall {
address target;
bytes data;
uint256 value;
}
function executeBatch(
BatchCall[] calldata calls,
bytes32 commit,
uint8 v, bytes32 r, bytes32 s
) external {
// authorize the user
AUTH(commit, v, r, s);
// Execute all calls as the user
for (uint i = 0; i < calls.length; i++) {
AUTHCALL(
gasleft(),
calls[i].target,
calls[i].value,
0, // valueExt (must be 0 for now)
// ... calldata parameters
);
}
}
}尽管这很令人兴奋,但这两个 opcodes 在 EVM 社区引发了一系列安全担忧。上下文变量 authorized 在合约的单个执行帧内一直存在,但不会传递给任何调用,包括 DELEGATECALL。这基本上意味着授权在整个交易范围内都是有效的。
真正的问题不在于它在单笔交易内的临时性,而在于当 invoker 合约被广泛使用时会发生什么。如果最终每个人都会用上智能账户,那么 AUTH 和 AUTHCALL 这两个 opcodes 可能会成为巨大的技术负担,并增加 EVM 本身的复杂度。
如果像上面那样一个热门的 invoker 合约出了 bug,会怎么样?突然之间,每一个曾经授权过这个合约的用户都可能面临风险。一次恶意更新,一个被遗漏的边界情况,一次整数溢出,就这样,大规模的资产掏空开始了。
EVM 生态意识到,这造成了一种奇怪的局面。EIP-3074 的安全模型严重依赖 invoker 合约。由于多个用户可能依赖同一套 invoker 逻辑,一个 invoker 中的漏洞或攻击可能一下子危及许多用户的资产或权限,这就是 EIP-3074 被撤回的原因。
现在来看 EIP-7702,它由 Vitalik 在 2024 年 5 月提出,于 2025 年 5 月通过 pectra upgrade 上了 mainnet,它引入了一种新的精简方案,让 EOAs 能在单笔交易期间临时像智能合约一样运作,这可是件大事!
EIP-7702 没有引入新的 opcodes,而是利用 EIP-2718's 交易信封系统引入了一种新的交易类型
interface EIP7702Transaction {
chainId: number;
nonce: number;
maxPriorityFeePerGas: bigint;
maxFeePerGas: bigint;
gasLimit: bigint;
to: string;
value: bigint;
data: Uint8Array;
accessList: AccessList;
authorizationList: Authorization[];
}
interface Authorization {
chainId: number;
address: string; // the address of the contract code to delegate to
nonce: number;
yParity: 0 | 1;
r: Uint8Array;
s: Uint8Array;
}对于每个授权对象,都会有一个委托标识 (0xef0100 || address) 被写入授权账户的代码中。所有执行代码的操作都必须加载并执行该委托所指向的代码。
这正是很多人搞不清楚的地方。EIP-7702 自最初的草案以来一直在演进。起初,这个提案的设计是让 EOAs 只在一笔交易内临时变得“智能”。但最终版本让你可以控制委托持续多久。
function processEIP7702Transaction(tx) {
// verify all auth signatures
for (const auth of tx.authorizationList) {
const signer = recoverSigner(auth);
if (signer !== getAddressFromAuth(auth)) {
throw new Error("Invalid authorization");
}
}
// set delegation pointers (these persist after transaction)
for (const auth of tx.authorizationList) {
const eoaAddress = getAddressFromAuth(auth);
// set delegation pointer - 0xef0100 || contract_address
const delegationPointer = concat(['0xef0100', auth.address]);
setCode(eoaAddress, delegationPointer);
// nowm this delegation PERSISTS unless explicitly cleared
}
// execute the transaction normally
executeTransaction(tx);
// no auto cleanup, delegation stays active
}注意:如果一笔 7702 交易执行失败,已经处理的委托标识不会回滚,而是会一直保留,除非在后续的一笔 7702 交易中把委托地址设置为 0x0,显式地清除它。
这意味着你有三种委托模式:
Batching Contract,委托本身会一直存在,直到用户把它清除。EIP-7702 的好处在于它让你自己选。想为一次性的复杂交易做临时委托?可以。想把你的 EOA 持久升级成像智能合约钱包一样工作?也可以。
临时委托的例子如下
// one off delegation
const tempDelegationTx = {
type: 0x04,
authorizationList: [{
address: batchExecutorContract,
// ... auth details
}],
to: userAddress,
data: encodeBatchCall([
// execute your complex operation
swapTokens(),
addLiquidity(),
stakeRewards(),
])
};而持久委托的例子如下
// upgrade EOA to smart wallet
const persistentDelegationTx = {
type: 0x04,
authorizationList: [{
address: smartWalletImplementation,
// ... auth details
}],
to: userAddress,
data: encodeCall("initializeSmartFeatures", [])
// this delegation persists for future txns
};
// later txns can now use smart wallet features
const futureTransaction = {
to: userAddress, // your EOA will now execute smart wallet code
data: encodeCall("batchExecute", [multipleOperations])
// no new auth needed
};EIP-3074 和 EIP-7702 的关键区别不只是临时与持久,而在于风险的分布与控制。EIP-3074 制造了一个共享的故障点,一个热门合约的问题可能波及成千上万的用户。EIP-7702 隔离了每一笔交易,所以 bug 只会影响那一笔具体的交易。EIP-7702 与现有的 ERC-4337 完全兼容,不需要新的 opcodes。这基本上意味着,钱包可以逐步采用它,而不会破坏任何现有系统。
关键的洞见在于,管理委托的是 wallets,而不是 dApps。所以普遍的共识是只允许钱包管理委托,这意味着:
如果你在构建一个将要支持 EIP-7702 的应用,有几个技术点需要注意
// old: simple EOA transaction
const tx = {
to: contractAddress,
data: encodedCall,
// ... standard fields
};
// new way with eip-7702 delegation
const tx = {
type: 0x04,
to: userAddress, // calling user's own address
data: encodeBatchCall([
contract.interface.encodeFunctionData("swap", [tokenA, tokenB, amount]),
contract.interface.encodeFunctionData("addLiquidity", [lpTokens]),
contract.interface.encodeFunctionData("stake", [lpAmount])
]),
authorizationList: [{
address: batchExecutorImplementation,
// ... authorization signature
}],
// ... standard fields
};7702 交易,把委托重新设置为 0x0。这意味着你的 gas 估算需要考虑到EIP-7702 为 EOAs 启用了 EIP-1271 签名验证。这意味着你升级后的 EOA 可以使用 passkeys 这类替代方式来签名消息,同时依然保持向后兼容。// your EOA can now implement smart signature verification
function isValidSignature(bytes32 hash, bytes signature) external view returns (bytes4) {
// can verify passkey signatures, multisig, etc.
if (verifyPasskeySignature(hash, signature)) {
return 0x1626ba7e; // EIP-1271 magic value
}
return 0xffffffff;
}将最新文章直接发送到你的收件箱。