理解账户抽象 #2:使用Paymaster赞助交易

  • Tiny熊
  • 更新于 2023-02-22 11:43
  • 阅读 4148

理解账户抽象第二篇,看看如何使用第三方代替自己支付。

本系列的第一篇中,我们从头开始创建了就有EOA的功能的智能合约钱包,并通过允许用户选择自己的自定义验证逻辑进行改进。但是现在,钱包仍然需要支付Gas,这意味着钱包所有者需要找到一种方法来获得一些ETH,然后才能在链上执行任何操作。

如果我们想让钱包主人以外的其他人代替我们支付Gas呢?

有一些很好的理由支持我们加入这个功能:

  • 如果钱包所有者是一个区块链新手,那么在执行链上操作之前需要获得ETH是一个巨大的绊脚石。
  • 一个dapp可能愿意为其方法支付Gas费,这样Gas费就不会吓跑潜在用户了
  • 一些赞助者(或项目方)可能会允许钱包以ETH以外的其他代币支付Gas费用,例如以USDC支付。
  • 为了保护隐私,用户可能想从混合器中提取资产到一个新的地址,并将Gas费用记入一个与他们无关的账户中

引入Paymaster

假设我是一个想为其他人支付Gas费的dapp。我可能不想在任何地方为每个人的Gas付费,所以我需要把自定义逻辑放到链上,它可以查看用户的操作并决定是否要为该操作付费。

把自定义逻辑放到链上的方法是部署一个合约,我们称之为paymaster(支付者或付款人)

它将有一个方法来查看用户操作并决定是否愿意为该操作付费:

contract Paymaster {
  function validatePaymasterOp(UserOperation op);
}

然后,当一个钱包提交一个操作时,他们需要指出他们希望哪个支付方(如果有的话)来支付他们的Gas。

我们将在 "用户操作"中添加一个新的字段来指定支付者。

我们还将在用户操作中添加一个字段,钱包可以用它来向支付者传递任意的数据,以帮助它说服支付者支付其费用。

例如,这可能是由支付者的所有者在链外签署的内容。

struct UserOperation {
  // ...
  address paymaster;
  bytes paymasterData;
}

接下来,我们将改变入口点的handleOps,以利用新的paymaster。

现在它的行为将是:

对于每个操作(op):

  • 在操作者指定的钱包上调用validateOp。
  • 如果该操作有一个paymaster地址,那么对该paymaster调用validatePaymasterOp
  • 任何验证失败的操作都会被丢弃。
  • 对于每个操作,在操作的发送者钱包上调用executeOp,跟踪我们使用了多少Gas,然后将ETH转账到执行者那里以支付这些Gas。如果该操作有一个paymaster字段,那么这个ETH来自paymaster。否则,它就像以前一样来自钱包。

就像钱包一样,paymaster通过入口点(entrypoint)的存款方法存入他们的ETH,然后才能用于支付操作。

img

执行者同时调用支付者合约和用户的智能合约钱包,以确定用户的交易是否可以被赞助。

这其实是很简单的,对吗?

我们只要让捆绑器更新它的模拟操作和...

上一篇文章中,钱包向捆绑者退款,捆绑者会先模拟操作,试图避免执行验证失败的操作,因为这意味着钱包不会付款,所以捆绑者将承担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),这就足够了。

另外,每个捆绑者都会在本地跟踪信誉,所以如果一个捆绑者认为自己能做得更好,并且不会给其他捆绑者带来麻烦,那么它可以自由地实现自己的信誉逻辑。

与许多质押模式不同,这里的赌注从未被削减。它们只是作为一种方式存在,要求潜在的攻击者锁定大量的资金来进行大规模的攻击。

改进: paymaster 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);
}

回顾一下paymaster如何启用赞助交易

为了让钱包主人以外的人支付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,现在已经感觉挺好了,但仍有一些我们需要的功能(请期待),以实现相同的效果。

点赞 3
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。