这篇文章上次修改于 660 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
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
@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
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 relay service 监听请求
/getaddr,返回合约地址信息。如果运行 paymaster reputation,还会返回 paymaster reputation 相关信息。
例如:
{ "relayWorkerAddress": "0x249c573c2cf62b798db4e6b563fbc636247f098b", "relayManagerAddress": "0x7e282733bbca1994fa0d63848b40a1df2ebb5623", "relayHubAddress": "0xbF06d99FDE1dc4e4C24F4191Fad82F8f5524Ce62", "ownerAddress": "0x8C1FD2DE219c98f5F88620422e36a8A32f83324E", "minMaxPriorityFeePerGas": "1000000", "maxMaxFeePerGas": "500000000000", "minMaxFeePerGas": "1000000", "maxAcceptanceBudget": "285252", "chainId": "10", "networkId": "10", "ready": true, "version": "3.0.0-beta.3" }
- /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。function relayCall( string calldata domainSeparatorName, uint256 maxAcceptanceBudget, GsnTypes.RelayRequest calldata relayRequest, bytes calldata signature, bytes calldata approvalData ) external returns ( bool paymasterAccepted, uint256 charge, IRelayHub.RelayCallStatus status, bytes memory returnValue );
- 验证 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 相关的方法:
addRelayWorkers:被 relay manager 调用,添加被其控制的 worker。
function addRelayWorkers(address[] calldata newRelayWorkers) external;
onRelayServerRegistered:被 RelayRegistrar 回调,通知 RelayHub 该 relay manager 已经更新了注册信息。
function onRelayServerRegistered(address relayManager) external;
setMinimumStakes:设置或更改给定 token 的最新额度,是 relay manager 质押到 RelayHub 的最小额度。零表示该 token 不允许被质押。
function setMinimumStakes(IERC20[] memory token, uint256[] memory minimumStake) external;
Penalizer 相关的方法:
penalize:如果 Penalizer 发现 relay worker 违反了 Penalizer 合约中的某些规定,如 relay worker 所 relay 的 tx 不是调用 RelayHub 的 relayCall() 方法,会调用此方法进行惩罚。RelayHub 会根据给定的 relay worker 查找到 relay manager,并调用 StakeManager 合约进行惩罚。
function penalize(address relayWorker, address payable beneficiary) external;
4.2 Token Paymaster
用来为 tx 支付手续费。Paymaster 会保存合约 RelayHub 和 Forwarder 的地址。
没有经过审计,只是一个示例。
Dapp 开发者需要实现 Paymaster 合约,只要继承 [BasePaymaster](https://github.com/opengsn/gsn/blob/master/packages/contracts/src/BasePaymaster.sol)
即可。该合约需要在 RelayHub 存一定数量的 ETH,并在 tx 执行后被 RelayHub 收费。
Dapp 开发者需要实现两个被 RelayHub 回调的函数:preRelayedCall
and postRelayedCall
。
在 BasePaymaster 中:
preRelayedCall
会做一系列检查,继承 BasePaymaster 的合约需要实现 _preRelayedCall 即可:function preRelayedCall( GsnTypes.RelayRequest calldata relayRequest, bytes calldata signature, bytes calldata approvalData, uint256 maxPossibleGas ) external override returns (bytes memory, bool) { _verifyRelayHubOnly(); _verifyForwarder(relayRequest); _verifyValue(relayRequest); _verifyPaymasterData(relayRequest); _verifyApprovalData(approvalData); return _preRelayedCall(relayRequest, signature, approvalData, maxPossibleGas); }
postRelayedCall
同理:function postRelayedCall( bytes calldata context, bool success, uint256 gasUseWithoutPost, GsnTypes.RelayData calldata relayData ) external override { _verifyRelayHubOnly(); _postRelayedCall(context, success, gasUseWithoutPost, relayData); }
在给出的 TokenPaymaster(继承 BasePaymaster )示例中:
构造函数中会添加支持的 uniswap 和 token,并执行 token.approve(),以允许 uniswap 转移 token:
constructor(IUniswapV3[] memory _uniswaps) { uniswaps = _uniswaps; for (uint256 i = 0; i < _uniswaps.length; i++){ supportedUniswaps[_uniswaps[i]] = true; tokens.push(IERC20(_uniswaps[i].tokenAddress())); tokens[i].approve(address(_uniswaps[i]), type(uint256).max); } }
_preRelayedCall
为 tx 预付最大可能的 token 费用:function _preRelayedCall( GsnTypes.RelayRequest calldata relayRequest, bytes calldata signature, bytes calldata approvalData, uint256 maxPossibleGas ) internal override virtual returns (bytes memory context, bool revertOnRecipientRevert) { (signature, approvalData); (IERC20 token, IUniswapV3 uniswap) = _getToken(relayRequest.relayData.paymasterData); (address payer, uint256 tokenPrecharge) = _calculatePreCharge(token, uniswap, relayRequest, maxPossibleGas); token.transferFrom(payer, address(this), tokenPrecharge); return (abi.encode(payer, tokenPrecharge, token, uniswap), false); }
- 验证 token 是否支持 uniswap,即 caller 是否可以用 ERC20 token 进行支付。
- 计算最大可能的 token 费用。会调用 relayHub.calculateCharge() 计算出 ETH 费用后,再换算成 token。
- 调用 token.transferFrom() 向用户收取 token 费用。
_postRelayedCall
向用 caller 退还未使用的 gas。function _postRelayedCall( bytes calldata context, bool, uint256 gasUseWithoutPost, GsnTypes.RelayData calldata relayData ) internal override virtual { (address payer, uint256 tokenPrecharge, IERC20 token, IUniswapV3 uniswap) = abi.decode(context, (address, uint256, IERC20, IUniswapV3)); _postRelayedCallInternal(payer, tokenPrecharge, 0, gasUseWithoutPost, relayData, token, uniswap); } function _postRelayedCallInternal( address payer, uint256 tokenPrecharge, uint256 valueRequested, uint256 gasUseWithoutPost, GsnTypes.RelayData calldata relayData, IERC20 token, IUniswapV3 uniswap ) internal { uint256 ethActualCharge = relayHub.calculateCharge(gasUseWithoutPost + gasUsedByPost, relayData); uint256 tokenActualCharge = uniswap.getTokenToEthOutputPrice(valueRequested + ethActualCharge); uint256 tokenRefund = tokenPrecharge - tokenActualCharge; _refundPayer(payer, token, tokenRefund); _depositProceedsToHub(ethActualCharge, uniswap); emit TokensCharged(gasUseWithoutPost, gasUsedByPost, ethActualCharge, tokenActualCharge); }
- 调用 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 保证安全性。
struct ForwardRequest { address from; address to; uint256 value; uint256 gas; uint256 nonce; bytes data; uint256 validUntilTime; }. function verify( ForwardRequest calldata req, bytes32 domainSeparator, bytes32 requestTypeHash, bytes calldata suffixData, bytes calldata sig) external override view { _verifyNonce(req); _verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); }
将 20 字节的 sender 地址添加到 ForwardRequest 的 data 字段,并调用 ForwardRequest 中 to 字段所代表的目标合约。
function execute( ForwardRequest calldata req, bytes32 domainSeparator, bytes32 requestTypeHash, bytes calldata suffixData, bytes calldata sig ) external payable override returns (bool success, bytes memory ret) { ... bytes memory callData = abi.encodePacked(req.data, req.from); ... (success,ret) = req.to.call{gas : req.gas, value : req.value}(callData); ... return (success,ret); }
4.4 Target Contract
开发者编写的合约,获取原始 sender 并执行原始 tx。
需要兼容 ERC-2771 标准
- 继承
[ERC2771Recipient.sol](https://github.com/opengsn/gsn/blob/master/packages/contracts/src/ERC2771Recipient.sol)
[将
msg.sender
替换为_msgSende](https://docs.opengsn.org/contracts/#receiving-a-relayed-call)r
,以获取原始 sender。因为 target contract 只接收到 Forwarder 的请求,所以 msg.sender 是 Forwarder 的地址,而非用户的地址。function _msgSender() internal override virtual view returns (address ret) { ... assembly { ret := shr(96, calldataload(sub(calldatasize(), 20))) } ... }
5 CLI
CLI 用来部署 GSN 合约,启动 relayer 等。
gsn start
: 在本地测试环境中运行 GSN。
部署 GSN 合约:
- 启动一个 relayer。
参考
Ethereum Gas Station Network (GSN)
没有评论