理解账户抽象第二篇,看看如何使用第三方代替自己支付。
在本系列的第一篇中,我们从头开始创建了就有EOA的功能的智能合约钱包,并通过允许用户选择自己的自定义验证逻辑进行改进。但是现在,钱包仍然需要支付Gas,这意味着钱包所有者需要找到一种方法来获得一些ETH,然后才能在链上执行任何操作。
如果我们想让钱包主人以外的其他人代替我们支付Gas呢?
有一些很好的理由支持我们加入这个功能:
假设我是一个想为其他人支付Gas费的dapp。我可能不想在任何地方为每个人的Gas付费,所以我需要把自定义逻辑放到链上,它可以查看用户的操作并决定是否要为该操作付费。
把自定义逻辑放到链上的方法是部署一个合约,我们称之为paymaster(支付者或付款人)。
它将有一个方法来查看用户操作并决定是否愿意为该操作付费:
contract Paymaster {
function validatePaymasterOp(UserOperation op);
}
然后,当一个钱包提交一个操作时,他们需要指出他们希望哪个支付方(如果有的话)来支付他们的Gas。
我们将在 "用户操作"中添加一个新的字段来指定支付者。
我们还将在用户操作中添加一个字段,钱包可以用它来向支付者传递任意的数据,以帮助它说服支付者支付其费用。
例如,这可能是由支付者的所有者在链外签署的内容。
struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}
接下来,我们将改变入口点的handleOps
,以利用新的paymaster。
现在它的行为将是:
对于每个操作(op):
validatePaymasterOp
。executeOp
,跟踪我们使用了多少Gas,然后将ETH转账到执行者那里以支付这些Gas。如果该操作有一个paymaster字段,那么这个ETH来自paymaster。否则,它就像以前一样来自钱包。就像钱包一样,paymaster通过入口点(entrypoint)的存款方法存入他们的ETH,然后才能用于支付操作。
执行者同时调用支付者合约和用户的智能合约钱包,以确定用户的交易是否可以被赞助。
这其实是很简单的,对吗?
我们只要让捆绑器更新它的模拟操作和...
在上一篇文章中,钱包向捆绑者退款,捆绑者会先模拟操作,试图避免执行验证失败的操作,因为这意味着钱包不会付款,所以捆绑者将承担Gas成本。
这里也出现了同样的问题:
捆绑者想避免提交验证失败的操作,因为paymaster不会付款,而捆绑者又要负担成本。
起初,我们似乎可以对validatePaymasterOp
施加和validateOp
同样的限制(即它只能访问钱包和它自己的相关存储,不能使用被禁止的操作代码),然后捆绑者可以在模拟钱包的validateOp
的同时,简单地为用户操作模拟validatePaymasterOp
。
但是,这里有一个问题。
因为存储限制说一个钱包的validateOp只能访问该钱包的相关存储,我们知道一个捆绑程序中的多个操作的验证不能相互干扰,只要它们来自不同的钱包,因为它们访问的共同存储非常少。
但是一个paymaster的存储在捆绑中所有使用该paymaster的操作中是共享的。
这意味着一个validatePaymasterOp
的行为有可能导致使用同一个paymaster的交易包中的许多其他操作的验证失败。
恶意的paymaster可以利用这一点来破坏系统。
为了防止这种情况,我们引入了一个信誉系统。
我们将让捆绑者跟踪每个paymaster最近验证失败的频率,并通过节流或禁止使用该paymaster的操作来惩罚那些经常失败的paymaster。
如果一个恶意的paymaster可以创建许多自己的实例( Sybil 攻击),这个信誉系统就不会起作用,所以我们要求paymaster用ETH做抵押。这样一来,它就不能从拥有多个账户中获益。
让我们在EntryPoint添加新的方法来处理抵押:
contract EntryPoint {
// ...
function addStake() payable;
function unlockStake();
function withdrawStake(address payable destination);
}
一旦投入了质押,在调用unlockStake后,要经过一定的延迟才能撤出。
这些新方法有别于之前讨论的deposit和withdrawTo,后者是由钱包和paymaster用来添加ETH,这些ETH将被用来支付Gas,并且可以在任何时候立即提取。
质押规则有一个例外:
如果paymaster只访问过钱包的相关存储,而不是paymaster自己的存储,那么它就不需要质押,因为在此案例中,捆绑的多个操作所访问的存储不会相互重叠,原因与钱包的validateOp调用相同。
实际上,我不认为信誉系统的详细规则有多么重要,如果想详细了解,你可以在这里读到它们,但只要你知道捆绑者将有一个机制来避免从一个“刚刚销毁自己的支付者”那里选择操作(OP),这就足够了。
另外,每个捆绑者都会在本地跟踪信誉,所以如果一个捆绑者认为自己能做得更好,并且不会给其他捆绑者带来麻烦,那么它可以自由地实现自己的信誉逻辑。
与许多质押模式不同,这里的赌注从未被削减。它们只是作为一种方式存在,要求潜在的攻击者锁定大量的资金来进行大规模的攻击。
postOp
我们可以做一个小小的改进,让paymaster做得更多。现在,只有在验证步骤中,在操作实际运行之前,才会调用paymaster。
但是,paymaster也可能需要根据操作的结果做一些不同的事情。
例如,一个允许用户用美元支付Gas费的支付机构需要知道该操作实际使用了多少Gas,这样它就知道该收取多少美元。
因此,我们将为paymaster添加一个新的方法postOp
,在操作完成后,入口点将调用这个方法,并传递给它使用了多少Gas。
我们还希望paymaster能够 "向自己传递信息",并在postOp
的步骤中使用这些在验证过程中计算的数据,因此我们将允许验证返回任意的 "上下文" 数据,这些数据将在以后被传递给postOp
。
对postOp的第一次尝试将是这样的:
contract Paymaster {
function validatePaymasterOp(UserOperation op) returns (bytes context);
function postOp(bytes context, uint256 actualGasCost);
}
但是,对于想在最后以USDC收费的paymaster来说,有一些棘手的事情。
paymaster在授权执行之前(在validatePaymasterOp
中)可以检查用户是否有足够的USDC来支付该操作。但是,在执行过程中,执行操作完全有可能将钱包中所有的USDC 转出,这将意味着paymaster无法在最后提取付款。
paymaster能否通过在开始时收取最大数额的USDC,然后在结束时退还未使用的部分来避免这种情况?这也是可行的,但它很混乱:它需要两次转账调用,而不是一次,这就增加了Gas成本,并且会发出两个不同的转账事件。我们将看看我们是否能做得更好。
我们需要一种方法,让paymaster在操作完成后导致操作失败,如果它这样做,它应该能够提取付款,因为无论发生什么,它在验证PaymasterOp时已经同意支付Gas。
设置这个的方法是让入口点有可能调用postOp
两次。
入口点首先调用postOp
,作为它刚刚运行钱包executeOp
的同一个执行的一部分,因此,如果postOp 回退(revert),它会导致executeOp
的所有执行也回退。
如果发生这种情况,那么入口点就会再次调用postOp
,但现在我们处于executeOp
发生之前的情况,并且在这种情况下,我们刚刚检查了validatePaymasterOp
,paymaster应该能够提取其应得的。
为了给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);
}
在用户操作中增加了新的字段,以允许钱包指定对应的paymaster。
struct UserOperation {
// ...
address paymaster;
bytes paymasterData;
}
paymaster 将ETH存入EntryPoint,与钱包支付自己的Gas的方式相同。
入口点合约更新其handleOps方法,以便对每个操作,除了通过钱包的validateOp
进行钱包验证外,还通过paymaster的validatePaymasterOp
对操作的paymaster(如果有)进行验证,然后执行操作,最后调用paymaster的postOp
。
为了处理模拟paymaster验证的一些作弊的问题,我们需要引入一个质押系统,paymaster会锁定ETH。
这是通过加入一些新的入口点方法来实现:
contract EntryPoint {
// ...
function addStake() payable;
function unlockStake();
function withdrawStake(address payable destination);
}
随着paymaster的加入,我们已经实现了大多数人在要求账户抽象时想到的所有功能!
我们也已经非常接近ERC-4337,现在已经感觉挺好了,但仍有一些我们需要的功能(请期待),以实现相同的效果。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!