在前一篇文章账户抽象之路#1-从零设计,我们完全复制了 EOA 的功能,并通过允许用户选择自己的自定义验证逻辑对智能钱包进行了改进。但是就目前而言,钱包仍然需要支付gas费,这意味着钱包所有者需要找到一种方式来获得一些ETH,然后才能在链上执行操作。
如果我们希望钱包所有者以外的人都可以代替支付gas,要怎么去实现呢?
这么做的一些原因是:
如果钱包所有者是区块链新手,那么在执行链上操作之前需要获取 ETH 是一个巨大的门槛;
去中心化应用(dapp)可能愿意为其功能支付 gas 费用,这样 gas 费用就不会吓跑潜在用户;
发起人可能允许钱包使用 ETH 以外的其他代币支付 gas,例如 USDC;
出于隐私考虑,用户可能希望将资产从混币器提取到新地址,并将gas费记入与他们无关的帐户;
假设我是一个想要为其他人付gas的dapp应用,大概率我不想为每个人都付gas费,所以我需要在链上定义一套逻辑,用来检查用户的op,以此来决定是否为他支付gas费。
将自定义逻辑放到链上的方法是部署一个合约,我们称之为代理者(paymaster)。
它有一个方法可以查看用户的操作,并决定是否愿意为改操作支付gas费:
contract Paymaster {
function validatePaymasterOp(UserOperation op);
}
然后当智能钱包提交用户操作op时,他们需要指定他们希望哪个代理者(如果有的话)来为他支付gas费。
我们将为UserOperation
添加一个新的字段来标识它。
我们还将向用户操作Op添加一个字段,智能钱包可以使用该字段将任意数据传递给代付者,以帮助它验证,并让代付者支付其费用。
例如,这个数据可以是代付者链下签名的内容。
struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}
接下来我们将改变入口点合约的handleOps
方法,让它使用代付者支付gas费。
他的行为如下:
对于每一个op:
在op的发送者指定的钱包上调用validateOp
如果op有paymaster地址,则调用该paymaster上的validatePaymasterOp
任何验证失败的操作都将被丢弃
对于每个op,在op的发送者钱包上调用executeOp
,跟踪使用了多少gas,然后将ETH转移给执行者以支付该 gas。如果op有paymaster
字段,那么这个ETH来自代付者paymaster
。否则,它像以前一样来自钱包。
就像智能钱包一样,paymasters通过入口点的存款方式存入他们的 ETH,然后才能用于支付操作费用。
这很简单,对不对?
我们只需要打包者更新他们链下的模拟和...
在上一篇关于钱包退款给打包者的文章中,打包者使用模拟来尝试避免执行验证失败的操作,因为这意味着钱包不会付款,因此打包者将承担 gas 费用。
同样的问题出现在这里:
打包者希望避免提交未通过代付者验证的操作,因为代付者不会付款,打包者将再次陷入困境。
起初,我们看似可以对validatePaymasterOp
设置相同的限制,就像我们在validateOp
上所做的那样(即它只能访问钱包及其自己的关联存储,不能使用被禁止的操作码),然后打包者可以简单地为用户操作模拟validatePaymasterOp
,同时模拟钱包的validateOp
方法。
但这里有一个陷阱。
由于存储限制,即智能钱包的valiateOp
只能访问该钱包的关联存储,我们知道,只要它们来自不同的钱包,捆绑的包中多个操作的验证就不能相互干扰,因为它们访问的共同存储空间很少。
但一个代付者的存储是被这个包中所有使用该代付者的用户操作所共享的。
这意味着一个validatePaymasterOp
的操作可能会导致捆绑包中使用同一代付者的许多其他操作的验证失败。
恶意的代付者paymaster可以使用这个来DDoS攻击整个系统。
为了防止这种情况,我们引入了声誉系统。
我们将让打包者跟踪每个代付者最近失败验证的频率,并通过限制或禁止使用该代付者的操作来惩罚经常失败的代付者。
如果恶意的代付者可以自己创建许多实例(Sybil攻击/女巫攻击),则此声誉系统将不起作用,因此我们要求代付者质押ETH,这样它就不会从拥有多个帐户中受益。
让我们在入口点添加处理质押的新方法:
contract EntryPoint {
// ...
function addStake() payable;
function unlockStake();
function withdrawStake(address payable destination);
}
一旦押金被放入,并调用lockingStake后,经过一些延迟之后,它才能被提取。
这些新方法与之前讨论的 deposit
和 withdrawTo
不同,智能钱包和代付者使用它们来添加 ETH,这些 ETH 将用于支付 gas费, 并可以随时提现。
质押规则有一个例外:
如果代付者只访问只能钱包相关的存储,而不是代付者自己的存储,那么它就不需要质押,因为在这种情况下,捆绑中的多个操作所访问的存储不会相互重叠,原因与智能钱包的validateOp
调用相同。
实际上,我认为了解信誉系统的详细规则并不那么重要。你可以在这里读到它们,但只要你知道打包者将有一个机制来避免从一个不好的代付者那里选择操作Op,这就足够了。
另外,每个打包者都会在本地跟踪信誉,所以如果一个打包者认为自己可以做得更好,并且不会给其他打包者带来麻烦,那么他可以自由地实现自己的信誉逻辑。
与许多质押机制不同,这里的质押从未被削减,它们只是作为一种方式存在,要求潜在的攻击者质押一个非常大的资本,以防止进行大规模的攻击。
我们可以做一个小小的改进,让代付者可以做的更多。现在,代付者只在验证步骤中被调用,这在操作实际运行之前。
但是,一个代付者也可能需要根据操作的结果做一些不同的事情。
例如,一个允许用户用USDC支付gas费,那它就需要知道用户操作实际使用了多少gas费,这样它才知道该收取多少USDC。
因此,我们将为代付者添加一个新的方法,postOp
,在用户操作完成后,入口点将调用这个方法,并传递给它使用了多少gas费。
我们还希望代付者能够 "向自己传递信息",将验证过程中的结果数据放到postOp
中计算,因此我们将允许验证返回任意的 "上下文 "数据,这些数据将在以后被传递给postOp
。
我们对 postOp 的第一次修改如下:
contract Paymaster {
function validatePaymasterOp(UserOperation op) returns (bytes context);
function postOp(bytes context, uint256 actualGasCost);
}
但是对于想要在最后以 USDC 收费的代付者来说,有一些棘手的事情。
理论上,代付者在批准执行之前(在validatePaymasterOp
中)检查用户是否有足够的USDC来支付该操作,但是有可能该操作在执行过程中转移了钱包中所有的USDC,这将意味着代付者无法在最后获得付款。
代付者能否通过在开始时收取最大金额的USDC,然后在结束时退还未使用的部分来避免这种情况?这将是可行的,但它更麻烦的:它需要做两次transfer调用,而不是一次,这增加了gas费成本,并发出了两个不同的transfer事件。我们将看看我们是否能做得更好。
我们需要一种方法,即使操作失败了,也能够支付gas费,因为无论发生什么,它已经在validatePaymasterOp
时同意支付gas费了。
方法是让入口点合约调用postOp
两次。
入口点合约首先调用postOp
,作为它刚刚运行钱包executeOp
的同一个执行的一部分,因此,如果postOp
失败被回滚,它会导致executeOp
的所有操作也回滚。
译注:如果第二次运行,则说明执行失败,同时在这里收取手续费
如果发生这种情况,那么入口点会再次调用postOp
,但现在我们处于executeOp
发生之前的情况,因为在这种情况下,我们刚刚检查了validatePaymasterOp
,代付者应该能够提取它应得的。
为了给postOp
提供更多的上下文信息,我们将给它多一个参数:一个标志,以表明我们是否在它的 "第二次运行 "中,因为它已经被回滚了一次:
contract Paymaster {
function validatePaymasterOp(UserOperation op) returns (bytes context);
function postOp(bool hasAlreadyReverted, bytes context, uint256 actualGasCost);
}
为了让钱包拥有者以外的人支付gas费,我们引入了一个新的参与者,即代付者paymaster,他部署了一个具有以下接口的智能合约:
contract Paymaster {
function validatePaymasterOp(UserOperation op) returns (bytes context);
function postOp(bool hasAlreadyReverted, bytes context, uint256 actualGasCost);
}
我们在用户操作中增加了新的字段,允许钱包指定他们想要的代付者:
struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}
代付者将ETH存入入口点合约,就像钱包支付自己的gas费一样。
入口点合约更新了handleOps
方法,以便对每个操作op,除了通过钱包的validateOp
进行钱包的验证外,还通过代付者的validatePaymasterOp
对代付者(如果有)进行验证,然后执行操作,最后调用代付者的postOp
。
为了解决模拟付款人验证中的一些异常问题,我们需要引入一个代付者质押系统,用于锁定代付者的ETH。
新的入口点合约方法如下:
contract EntryPoint {
// ...
function addStake() payable;
function unlockStake();
function withdrawStake(address payable destination);
}
随着 代付者paymasters 的加入,我们已经实现了大多数人在看到帐户抽象时,所能想到的所有功能!
我们也已经非常接近 ERC-4337,但我们还需要更多功能来实现最终功能。
来到这里感觉真好!
原文链接