Gas Estimation
首先,用户的 Account 是个合约,EVM 在执行交易时遇到合约会有一笔加载合约的 Gas 消耗。另外用户的 UserOp 会被封装到交易里发到链上,具体由一个统一的 EntryPoint 合约执行。所以在 AA 中哪怕是最普通的转账,消耗的 Gas 也是普通 EOA 地址转账的好几倍。
理论上,你可以设置一个很大的 GasLimit 去规避很多复杂的情况,这很简单。但是这要求用户的 Account 能够有相当大的余额去提前扣除这笔费用,这并不现实。如果能够准确的预估 Gas 的消耗,可以让用户在合理的范围内去正常交易,这对于提高用户体验和降低交易门槛有很大的帮助。
根据 ERC4337 的官方文档,跟 Gas 估算有关的字段如下:
preVerificationGas
verificationGasLimit
callGasLimit
让我们来一一讲解这几个字段并提供一个预测方法。
preVerificationGas
首先我们需要明白,UserOperation 是一个结构,由 Bundler 中的 Signer 将其打包成交易,并发送到链上去执行,而在执行的过程中消耗的是 Signer 的 Gas,在执行结束后计算产生的 GasCost,并返还给 Signer。
在以太坊的模型中,执行一个交易前会预先扣除一定的 Gas,这里简单归纳为两点:
如果是创建合约会扣除 53000,调用合约则扣除 21000
根据合约长度以及合约代码的字节类型扣除一定的 Gas
👉 相关的代码实现
也就是说,执行交易前就会消耗一部分隐性的 Gas,是无法在执行的时候计算的,所以 UserOperation 需要指定 preVerificationGas,用来补贴 Signer。不过这部分隐性的 Gas 是可以通过链下计算的,官方的 SDK 中给出了相关的接口,我们只需要调用即可。
import { calcPreVerificationGas } from '@account-abstraction/sdk';
@param userOp filled userOp to calculate. The only possible missing fields can be the signature and preVerificationGas itself
@param overheads gas overheads to use, to override the default values
const preVerificationGas = calcPreVerificationGas(userOp, overheads);
verificationGasLimit
顾名思义,这是在验证阶段分配的 GasLimit,有三种情况会使用到这个 GasLimit:
如果 UserOp.sender 不存在,执行 UserOp.initCode 初始化 Account
执行 Account.validateUserOp 验证签名
如果存在 PaymasterAndData
验证阶段调用 Paymaster.validatePaymasterUserOp
结束阶段调用 Paymaster.postOp
senderCreator.createSender{gas : verificationGasLimit}(initCode);
IAccount(sender).validateUserOp{gas : verificationGasLimit}
uint256 gas = verificationGasLimit - gasUsedByValidateAccountPrepayment;
IPaymaster(paymaster).validatePaymasterUserOp{gas : gas}
IPaymaster(paymaster).postOp{gas : verificationGasLimit}
可以看到,verificationGasLimit 基本代表上述所有操作的 Gas 总限制,但不是严格限制也不一定准确,因为 createSender 和 validateUserOp 的调用是独立的,也就意味着最坏的情况,实际 Gas 消耗可能是 verificationGasLimit 的两倍。
所以为了确保 createSender 和 validateUserOp,validatePaymasterUserOp 的 gas 总消耗不会超过 verificationGasLimit,我们需要预测这三个操作的 Gas 消耗。
其中 createSender 是可以准确预测的,这里我们可以使用传统的 estimateGas 方法去预测。
// userOp.initCode = [factory, initCodeData]
const createSenderGas = await provider.estimateGas({
from: entryPoint,
to: factory,
data: initCodeData,
});
为什么 from 要设置为 entryPoint 地址呢,因为基本上大部分的 Account 在创建的时候会设置一个来源(即 entryPoint),调用 validateUserOp 会验证来源。
其他的类似 validateUserOp,validatePaymasterUserOp 目前不太好预测,但是由于方法本身的特性为验证 UserOp 的有效性(大概率是验证签名),所以本身 Gas 消耗并不会很高,在实际操作中我们给一个 100000 的 GasLimit 基本能涵盖这类方法的消耗。所以综上,我们可以将 verificationGasLimit 设置为:
verificationGasLimit = 100000 + createSenderGas;
callGasLimit
callGasLimit 代表 Account 实际执行 callData 的消耗,也是预测 Gas 中最重要的部分。那么我们该如何预测这部分的 Gas 消耗呢,用传统的 estimateGas 实现如下:
const callGasLimit = await provider.estimateGas({
from: entryPoint,
to: userOp.sender,
data: userOp.callData,
});
这里模拟从 entryPoint 调用 Sender Account 的方法,通过了 Account 的来源检查,也绕过了 validateUserOp 中验证签名步骤(因为在 eth_estimateUserOperationGas 接口中的 UserOp 是没有签名的)。
这里存在一个问题,就是这种预测成立的前提是 Sender Account 是存在的,如果是 Account 的第一笔交易 (Account 还没有被部署,需要先执行 initCode),这种预测会因为 Account 不存在而发生 revert。无法预估准确的 callGasLimit。