GSN 策略 - OpenZeppelin 文档

本文介绍了如何使用 OpenZeppelin Contracts 通过 Gas Station Network (GSN) 接受中继调用,详细解释了 GSN 策略的概念,并展示了两种常见策略 GSNRecipientSignatureGSNRecipientERC20Fee 的用法,最后介绍了如何创建自定义策略以满足不同的业务需求,通过继承 GSNRecipient 并实现相应函数即可。

你当前阅读的不是此文档的最新版本。5.x 是当前版本。

GSN 策略

本指南将向你展示使用 OpenZeppelin Contracts 通过 Gas Station Network (GSN) 接受中继调用的不同策略。

首先,我们将介绍什么是“GSN 策略”,然后展示如何使用两种最常见的策略。 最后,我们将介绍如何创建自己的自定义策略。

如果你仍在学习 Gas Station Network 的基础知识,你应该首先前往 GSN 指南

GSN 策略详解

GSN 策略 决定批准哪个中继调用,拒绝哪个中继调用。策略是 GSN 中的一个关键概念。Dapps 需要一种策略来防止恶意用户花费 dapp 的资金来支付中继调用费用。

正如我们在 GSN 指南 中所看到的,为了启用 GSN,你的合约需要从 GSNRecipient 扩展。

GSN 接收者合约需要以下内容才能工作:

  1. 它需要在其 RelayHub 上存入资金。

  2. 它需要以不同的方式处理 msg.sendermsg.data

  3. 它需要决定如何批准和拒绝中继调用。

可以通过 GSN Dapp 工具 或使用 OpenZeppelin GSN Helpers 以编程方式为 GSN 接收者合约存入资金。

实际用户的 msg.sendermsg.data 可以通过 GSNRecipient_msgSender()_msgData() 安全地获得。

决定如何批准和拒绝中继调用有点复杂。你可能想要选择哪些用户可以通过 GSN 使用你的合约,并可能向他们收取费用,就像夜总会的保镖一样。我们称这些为 GSN 策略

基础的 GSNRecipient 合约不包含策略,因此你必须使用预构建的策略之一或编写自己的策略。我们将首先介绍如何使用包含的策略:GSNRecipientSignatureGSNRecipientERC20Fee

GSNRecipientSignature

GSNRecipientSignature 允许用户通过 GSN 将调用中继到你的接收者合约(向你收费),如果他们可以证明你信任的帐户批准他们这样做。他们通过 签名 来实现这一点。

中继调用必须包含已添加到合约作为受信任签名者的同一帐户对中继调用参数的签名。如果不是同一个,GSNRecipientSignature 将不接受中继调用。

这意味着你需要设置一个系统,让你的受信任帐户签署中继调用参数,然后将其包含在中继调用中,只要它们是有效用户(根据你的业务逻辑)。

有效用户的定义取决于你的系统,但一个例子是通过某种 OAuth 和验证完成注册的用户,例如,通过验证码或验证了他们的电子邮件地址。 你可以进一步限制它,让新用户发送特定数量的中继调用(例如,限制通过 GSN 的 5 个中继调用,此时用户需要创建一个钱包)。 或者,你可以离线向用户收取费用(例如,通过信用卡)以获取你系统上的信用额度,并让他们创建中继调用,直到他们的信用额度用完。

这种设置的优点是,如果你想更改业务规则,你的合约不需要更改。你所做的只是更改后端逻辑条件,在此条件下用户可以免费使用你的合约。 另一方面,你需要一个后端服务器、微服务或 lambda 函数才能完成此操作。

GSNRecipientSignature 如何工作?

GSNRecipientSignature 根据包含的签名决定是否接受中继调用。

acceptRelayedCall 的实现从 approvalData 中中继调用参数的签名中恢复地址,并将其与受信任的签名者进行比较。 如果包含的签名与受信任的签名者匹配,则批准中继调用。 另一方面,当包含的签名与受信任的签名者不匹配时,中继调用将被拒绝,错误代码为 INVALID_SIGNER

如何使用 GSNRecipientSignature

你需要创建一个链下服务(例如,后端服务器、微服务或 lambda 函数),你的 dapp 调用该服务以使用你的受信任签名者帐户签名(或不签名,基于你的业务逻辑)中继调用参数。然后将签名作为中继调用中的 approvalData 包含。

你的 GSN 接收者合约将继承自 GSNRecipientSignature,而不是直接使用 GSNRecipient,并且按照以下示例代码在 GSNRecipientSignature 的构造函数中设置受信任的签名者:

import "@openzeppelin/contracts/GSN/GSNRecipientSignature.sol";

contract MyContract is GSNRecipientSignature {
    constructor(address trustedSigner) public GSNRecipientSignature(trustedSigner) {
    }
}
我们编写了一个深入的指南,介绍如何设置与 GSNRecipientSignature 配合使用的签名服务器,查看一下!

GSNRecipientERC20Fee

GSNRecipientERC20Fee 有点复杂(但别担心,它已经为你编写好了!)。与 GSNRecipientSignature 不同,GSNRecipientERC20Fee 不需要任何链下服务。 你将向你的用户提供特殊用途的 ERC20 代币,而不是在链下批准每个中继调用。然后,这些代币将用作中继调用到你的接收者合约的付款。 任何拥有足够的代币来支付的用户都会自动批准其转发的调用,并且接收者合约将承担其交易成本!

每个接收者合约都有自己的特殊用途代币。代币到以太币的汇率为 1:1,因为这些代币用于支付你的合约以支付使用 GSN 时的 gas 费用。

GSNRecipientERC20Fee 有一个内部的 _mint 函数。首先,你需要设置一种调用它的方法(例如,添加一个具有某种形式的 访问控制(例如 onlyMinter)的公共函数)。 然后,根据你的业务逻辑向用户发行代币。例如,你可以向新用户铸造有限数量的代币,在用户离线购买代币时铸造代币,根据用户的订阅提供代币等等。

用户不需要调用 approve 他们的代币,你的接收者合约才能使用它们。它们是一种修改后的 ERC20 变体,允许接收者合约检索它们。

GSNRecipientERC20Fee 如何工作?

GSNRecipientERC20Fee 根据用户代币的余额决定批准或拒绝中继调用。

acceptRelayedCall 函数实现检查用户的代币余额。 如果用户没有足够的代币,则中继调用将被拒绝,并显示 INSUFFICIENT_BALANCE 错误。 如果有足够的代币,则会批准中继调用,并返回最终用户的地址、maxPossibleChargetransactionFeegasPrice 数据,以便可以在 _preRelayedCall_postRelayedCall 中使用它们。

_preRelayedCall 函数中,maxPossibleCharge 金额的代币被转移到接收者合约。 假设中继调用将使用所有可用的 gas,则转移所需的最大代币数量。 然后,在 _postRelayedCall 函数中,计算实际金额,包括接收者合约实现和 ERC20 代币转账,并退还差额。

_preRelayedCall 中转移所需的最大代币数量是为了保护合约免受攻击(这与以太币在以太坊交易中被锁定的方式非常相似)。

gas 成本估算不是 100% 准确的,我们可能会在以后进行调整。
始终使用 _preRelayedCall_postRelayedCall 函数。内部 _preRelayedCall_postRelayedCall 函数代替公共 preRelayedCallpostRelayedCall 函数使用,因为公共函数被阻止被非 RelayHub 合约调用。

如何使用 GSNRecipientERC20Fee

你的 GSN 接收者合约需要继承自 GSNRecipientERC20Fee 以及适当的 访问控制(用于代币铸造),在 GSNRecipientERC20Fee 的构造函数中设置代币详细信息,并创建一个受你选择的访问控制适当保护的公共 mint 函数,如以下示例代码所示(该代码使用 AccessControl):

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/GSN/GSNRecipientERC20Fee.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract MyContract is GSNRecipientERC20Fee, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() public GSNRecipientERC20Fee("FeeToken", "FEE") {
        _setupRole(MINTER_ROLE, _msgSender());
    }

    function _msgSender() internal view override(Context, GSNRecipient) returns (address payable) {
        return GSNRecipient._msgSender();
    }

    function _msgData() internal view override(Context, GSNRecipient) returns (bytes memory) {
        return GSNRecipient._msgData();
    }

    function mint(address account, uint256 amount) public {
        require(hasRole(MINTER_ROLE, _msgSender()), "Caller is not a minter");
        _mint(account, amount);
    }
}

自定义策略

如果包含的策略不太符合你的业务需求,你也可以编写自己的策略!例如,你的自定义策略可以使用指定的代币来支付中继调用,并使用自定义的以太币兑换率。或者,你可以向订阅你的 dapp 的用户发行 ERC721 代币,并且持有订阅代币的帐户可以免费使用你的合约作为订阅的一部分。有很多潜在的选择!

要编写自定义策略,只需从 GSNRecipient 继承并实现 acceptRelayedCall_preRelayedCall_postRelayedCall 函数。

你的 acceptRelayedCall 实现决定是否接受中继调用:返回 _approveRelayedCall 以接受,并返回 _rejectRelayedCall 以及错误代码以拒绝。

并非所有 GSN 策略都使用 _preRelayedCall_postRelayedCall(尽管它们仍然必须实现,例如 GSNRecipientSignature 使它们为空),但当你的策略涉及向最终用户收费时,它们很有用。

_preRelayedCall 应该收取最大可能的费用,_postRelayedCall 在完成中继调用后退还与实际费用之间的任何差额。 当返回 _approveRelayedCall 以批准中继调用时,也可以返回最终用户的地址、maxPossibleChargetransactionFeegasPrice 数据,以便可以在 _preRelayedCall_postRelayedCall 中使用这些数据。 请参阅 GSNRecipientERC20Fee 的代码 作为示例实现。

一旦你的策略准备就绪,你的 GSN 接收者需要做的就是继承它:

contract MyContract is MyCustomGSNStrategy {
    constructor() public MyCustomGSNStrategy() {
    }
}

← Gas Station Network

实用工具 →

  • 原文链接: docs.openzeppelin.com/co...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。