本文是对 GSN 代码的解析。
1 ERC-20 token 支付手续费流程
流程:
1)client 向 relay service 发送签名后的请求,不需要用 ETH 支付手续费。
2)relay service 将请求放到 tx 中,为用户用 ETH 垫付手续费,将 tx 发送到区块链网络中,目标是 RelayHub 合约。
3-5)RelayHub 合约请求 TokenPaymaster 合约将最大可能的手续费换算成 ERC-20 token,并从用户账户扣除。
6-8)RelayHub 合约请求 Forwarder 调用目标合约。Forwarder 会先验证签名和 nonce,再将请求转发给目标合约。
9-11)RelayHub 合约请求 TokenPaymaster 合约将多向用户收取的 ERC-20 token 退还给用户。
12-13)RelayHub 合约收到 TokenPaymaster 合约将 ERC-20 token 换成的 ETH 后,为 relay service 报销其为用户垫付的 ETH 手续费。
GSN 组件:
- provider:供 client 使用,会为 client 拼接 relay request,并发送到 relayer。当发送完 relay request 后,还会发送审计请求,作为后续惩罚 relayer 的依据。
- relay service:支付 gas 的第三方服务。将用户的 tx 发送到区块链,并垫付手续费。是去中心化的,志愿者可以自己启动一个 relay service。relay service 需要在 RelayHub 合约上质押一定数量的 token,如果被发现作恶,则会被惩罚。
- 合约:
- RelayHub:是 GSN 中的主要合约,连接了 client、relayer 和 paymaster。会包含如下合约的地址:
- RelayRegistrar:管理 relayer 的注册信息。
- StakeManager:管理 relayer 的质押信息。
- Penalizer:惩罚 relayer 的规则。
- Paymaster:决定是否接受 tx,并退还 relayer 垫付的手续费。需要开发者自己实现,不过提供了一些示例。
- Forwarder:验证签名,将用户的地址添加到 call data 中,将请求转发给 target 合约。
- ERC2771Recipient:target 合约需要继承该合约,兼容 EIP-2771 标准,以从 call data 中获取用户地址。
2 Provider
代码 使用示例 demo
@opengsn/provider
是 web3 provider 的封装,供 client 调用,为 client 拼接 relay request,并发送给 relayer。
发送 tx 过程:
3 Relay Service
代码 使用示例
是支付 gas 的第三方 relayer。
meta tx 不直接发送到区块链,而是发送元交易到第三方 relayer,该第三方支付 gas。
- 志愿者可以自己启动一个 relayer,但须提前在 RelayHub 注册并抵押 ETH。正确完成 relay tx 会收到来自 RelayHub 的奖励,若恶意攻击则会被没收抵押。
- relay manager 需确保有足够 ETH 来代替 DApp 使用者支付 gas,若 ETH 不足则会变成非活跃状态,不会被 Client 选择。
需要在 RelayHub 注册 relayer,目的是:
- 为 relayer 质押 ETH 或 ERC-20 token,以免 relayer 提交无效 tx。
- 为 relayer 提供初始资金以便发送 tx,默认是 2 ETH。
- 将 relayer 添加到链上 relayers 列表,供 client 使用。
可自行启动 relayer 后,在该页面进行注册:https://relays.opengsn.org/
3.1 config
定义 默认值
| | | | |
---|
| | | 0xbF06d99FDE1dc4e4C24F4191Fad82F8f5524Ce62 | |
| | relayer 用于质押的 ERC-20 token 地址 | 0x7e282733bbca1994fa0d63848b40a1df2ebb5623 | |
| | owner 账户,被 relayer-register 使用 | 0x8C1FD2DE219c98f5F88620422e36a8A32f83324E | |
| | | http://host.docker.internal:8545 | |
| | | | |
| | | | |
| | | | |
| | | | |
| | 是否运行 Paymaster Reputations | | |
| | | | |
| | relay worker 账户的最小 eth 数量 | | |
| | relay worker 账户的目标 eth 数量 | | |
| | relay manager 账户的最小 eth 数量 | | |
| | relay manager 账户的目标 eth 数量 | | |
| | | | |
| | | | |
3.2 relay service 初始化
- 解析配置文件。
- 根据配置文件中的 RelayHub 地址创建 RelayHub。从 RelayHub 获得 StakeManager,Penalizer 和 RelayRegistrar 的地址并进行创建。
- 根据配置文件决定是否创建 paymaster reputation manager。
- 根据配置文件决定是否初始化 PenalizerService。PenalizerService 会周期性地查看内存中是否新增非法 tx 信息(例如,tx 中调用的不是 IRelayHub.relayCall() 函数,tx 的 nonce 重复),如果有则调用 penalizer 合约中的惩罚函数,取消 relayer 的注册,并将其质押奖励给提交非法 tx 者。为了防止股权绑架(stake kidnapping),relayer 的质押一半被 burn,一半用于奖励。
- 初始化 relayer,会初始化 transaction manager 和 registration manager。
- transaction manager 会监听 TransactionBroadcast 事件并发送交易。
- registration manager 会检查并更新 relay manager 的质押信息,如下图所示。
3.3 relay service 启动
启动 http server,会启动 relayer。relayer 会周期性地执行如下步骤:
3.4 relayer 监听请求
- /getaddr,返回合约地址信息。如果运行 paymaster reputation,还会返回 paymaster reputation 相关信息。
- /stats,返回状态信息,供该页面展示:https://relays.opengsn.org/
- /relay,relay worker 会发送 tx 并垫付手续费
- 进行一系列检查
- 如果运行 PaymasterReputation,还会检查 request 中 paymaster 的名誉
- 请求消耗的 gas 不能超过 paymaster.getGasAndDataLimits()
- 请求中 paymaster 的 RelayHub 的余额不能过小
- 检查 relay worker 余额是否够发送 tx
- …
- 发送交易,调用 relayHub.RelayCall(),并将 tx 保存到本地。
- 发送交易后,检查 relay worker 的余额,必要时进行补充:如果 relay manager 的余额小于配置,则从 RelayHub 中撤回一部分;如果 relay worker 的余额小于配置,则将 relay manager 的余额转给 relay worker 一部分。
- /audit,如果配置文件中运行 penalizer,则监听该方法。如果发现 nonce 重复或没有调用 IRelayHub.relayCall() 函数的 tx,则将非法 tx 信息更新到内存,并发送交易,调用 penalizer.commit() 声明惩罚。
3.5 惩罚 relayer
@opengsn/provider 向 relayer 发送 tx 后,还会请求 /audit 审计 tx。penalizer service 会周期性地根据审计结果对 relayer 实施惩罚:
根据配置决定是否运行 Penalizer Service。
3.6 计算 paymaster 名誉
- 根据配置决定是否运行 Paymaster Reputation Manager。
- 周期性检查,如果发现 RelayHub 中发生了被 paymaster 拒绝或接受的 event,则更新 paymaster 的名誉。拒绝会导致 paymaster 名誉分值减一,接受会加一。
- 如果 relayer 发现请求中的 paymaster 名誉低于配置中的阈值,则拒绝 relay tx。
4 合约
4.1 RelayHub
合约代码
是 GSN 中的主要合约。连接了 client、relayer 和 paymaster,这样它们就可以互相不需要了解或信任对方。
Dapp 开发人员不需要了解或信任 RelayHub,就可以集成 GSN 。
RelayHub 可以自行部署,但自行部署的 RelayHub 无法共享已存在的 relayer。
RelayHub 中会保存合约 StakeManager、Penalizer 和 RelayRegistrar 的地址。
支付手续费流程相关的方法:
-
relayCall
:relay 一个 tx。- 验证 msg.sender 是否为注册过的 relayer。
- 验证 relay manager 的质押 token 数量是否够:通过 relayer 查找到 relay manager,调用 stakeManager.getStakeInfo() 获得 relay manager 的质押信息,检查其否质押了最小额度的 token。
- 验证 paymaster 存放在 RelayHub 的 ETH 数量是否够支付最大可能的手续费,msg.data 是否过长。
- 验证编码后的请求是否被正确打包,没有任何额外字节。
- 原子调用 paymaster.preRelayedCall(),forwarder.execute() 和 paymaster.postRelayedCall()。
- 更新 paymaster 和 relay manager 的 ETH 余额:从 paymaster 中扣除实际消耗的 gas 和 fee;为 relay manager 报销垫付的 gas 和 fee。
-
depositFor
:Paymaster 会调用该方法存 ETH 到 RelayHub,以便 RelayHub 为 tx 支付手续费。
function depositFor(address target) external payable;
Relayer 相关的方法:
- function addRelayWorkers(address[] calldata newRelayWorkers) external:被 relay manager 调用,添加被其控制的 worker。
- function onRelayServerRegistered(address relayManager) external:被 RelayRegistrar 回调,通知 RelayHub 该 relay manager 已经更新了注册信息。
- function setMinimumStakes(IERC20[] memory token, uint256[] memory minimumStake) external:设置或更改给定 token 的最新额度,是 relay manager 质押到 RelayHub 的最小额度。零表示该 token 不允许被质押。
Penalizer 相关的方法:
- function penalize(address relayWorker, address payable beneficiary) external:如果 Penalizer 发现 relay worker 违反了 Penalizer 合约中的某些规定,如 relay worker 所 relay 的 tx 不是调用 RelayHub 的 relayCall() 方法,会调用此方法进行惩罚。RelayHub 会根据给定的 relay worker 查找到 relay manager,并调用 StakeManager 合约进行惩罚。
4.2 Token Paymaster
合约代码
用来为 tx 支付手续费。Paymaster 会保存合约 RelayHub 和 Forwarder 的地址。
没有经过审计,只是一个示例。
Dapp 开发者需要实现 Paymaster 合约,只要继承 BasePaymaster 即可。该合约需要在 RelayHub 存一定数量的 ETH,并在 tx 执行后被 RelayHub 收费。
Dapp 开发者需要实现两个被 RelayHub 回调的函数:preRelayedCall
和postRelayedCall
。
在 BasePaymaster 中:
-
preRelayedCall
会做一系列检查,继承 BasePaymaster 的合约需要实现 _preRelayedCall 即可。 -
postRelayedCall
同理。
在给出的 TokenPaymaster(继承 BasePaymaster )示例中:
- 构造函数中会添加支持的 uniswap 和 token,并执行 token.approve(),以允许 uniswap 转移 token:
-
_preRelayedCall
为 tx 预付最大可能的 token 费用: - 验证 token 是否支持 uniswap,即 caller 是否可以用 ERC20 token 进行支付。
- 计算最大可能的 token 费用。会调用 relayHub.calculateCharge() 计算出 ETH 费用后,再换算成 token。
- 调用 token.transferFrom() 向用户收取 token 费用。
-
_postRelayedCall
向用 caller 退还未使用的 gas。- 调用 relayHub.calculateCharge() 计算出实际需要支付的 ETH 费用后,换算成 token。
- 预付的 token 减去实际需要支付的 token,得到需要退还的多余 token,并调用 token.transfer() 退还给 payer。
- 将收取的 token 通过 uniswap 换成 ETH 后,调用 relayHub.depositFor() 存到 RelayHub,以便 RelayHub 为 tx 支付手续费。
4.3 Forwarder
合约代码
- 接收 ForwardRequest 并验证是否合法,验证 sender 的签名和 nonce。用户合约仅依赖 forwarder 保证安全性。
- 将 20 字节的 sender 地址添加到 ForwardRequest 的 data 字段,并调用 ForwardRequest 中 to 字段所代表的目标合约。
4.4 Target Contract
开发者编写的合约,获取原始 sender 并执行原始 tx。
需要兼容 ERC-2771 标准
5 CLI
代码 使用示例
CLI 用来部署 GSN 合约,启动 relayer 等。
gsn start
: 在本地测试环境中运行 GSN。
1. 部署 GSN 合约:
2. 启动一个 relay service。
参考
Ethereum Gas Station Network (GSN)
Paying for your user's meta-transaction
Creating a Paymaster