GSN renaming (#1963)

* Merge GSNBouncerBase into GSNRecipient

* Remove emtpy implementations for _pre and _post

* Rename bouncers to recipients

* Rename bouncers documentation to strategies

* Rewrite guides and docstrings to use the strategy naming scheme

* Address review comments

* Apply suggestions from code review

Co-Authored-By: Francisco Giordano <frangio.1@gmail.com>

* change wording of docs

(cherry picked from commit aae95db4e0)
release-v2.4.0
Nicolás Venturo 5 years ago committed by Francisco Giordano
parent 21d014d481
commit 6efbee609e
  1. 103
      contracts/GSN/GSNRecipient.sol
  2. 20
      contracts/GSN/GSNRecipientERC20Fee.sol
  3. 22
      contracts/GSN/GSNRecipientSignature.sol
  4. 13
      contracts/GSN/README.adoc
  5. 92
      contracts/GSN/bouncers/GSNBouncerBase.sol
  6. 6
      contracts/mocks/ERC721GSNRecipientMock.sol
  7. 6
      contracts/mocks/GSNRecipientERC20FeeMock.sol
  8. 4
      contracts/mocks/GSNRecipientMock.sol
  9. 6
      contracts/mocks/GSNRecipientSignatureMock.sol
  10. 2
      docs/modules/ROOT/nav.adoc
  11. 86
      docs/modules/ROOT/pages/gsn-strategies.adoc
  12. 6
      test/GSN/GSNRecipientERC20Fee.test.js
  13. 12
      test/GSN/GSNRecipientSignature.test.js

@ -3,18 +3,28 @@ pragma solidity ^0.5.0;
import "./IRelayRecipient.sol";
import "./IRelayHub.sol";
import "./Context.sol";
import "./bouncers/GSNBouncerBase.sol";
/**
* @dev Base GSN recipient contract: includes the {IRelayRecipient} interface and enables GSN support on all contracts
* in the inheritance tree.
* @dev Base GSN recipient contract: includes the {IRelayRecipient} interface
* and enables GSN support on all contracts in the inheritance tree.
*
* Not all interface methods are implemented (e.g. {acceptRelayedCall}, derived contracts must provide one themselves.
* TIP: This contract is abstract. The functions {acceptRelayedCall},
* {_preRelayedCall}, and {_postRelayedCall} are not implemented and must be
* provided by derived contracts. See the
* xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategies] for more
* information on how to use the pre-built {GSNRecipientSignature} and
* {GSNRecipientERC20Fee}, or how to write your own.
*/
contract GSNRecipient is IRelayRecipient, Context, GSNBouncerBase {
contract GSNRecipient is IRelayRecipient, Context {
// Default RelayHub address, deployed on mainnet and all testnets at the same address
address private _relayHub = 0xD216153c06E857cD7f72665E0aF1d7D82172F494;
uint256 constant private RELAYED_CALL_ACCEPTED = 0;
uint256 constant private RELAYED_CALL_REJECTED = 11;
// How much gas is forwarded to postRelayedCall
uint256 constant internal POST_RELAYED_CALL_MAX_GAS = 100000;
/**
* @dev Emitted when a contract changes its {IRelayHub} contract to a new one.
*/
@ -97,6 +107,89 @@ contract GSNRecipient is IRelayRecipient, Context, GSNBouncerBase {
}
}
// Base implementations for pre and post relayedCall: only RelayHub can invoke them, and data is forwarded to the
// internal hook.
/**
* @dev See `IRelayRecipient.preRelayedCall`.
*
* This function should not be overriden directly, use `_preRelayedCall` instead.
*
* * Requirements:
*
* - the caller must be the `RelayHub` contract.
*/
function preRelayedCall(bytes calldata context) external returns (bytes32) {
require(msg.sender == getHubAddr(), "GSNRecipient: caller is not RelayHub");
return _preRelayedCall(context);
}
/**
* @dev See `IRelayRecipient.preRelayedCall`.
*
* Called by `GSNRecipient.preRelayedCall`, which asserts the caller is the `RelayHub` contract. Derived contracts
* must implement this function with any relayed-call preprocessing they may wish to do.
*
*/
function _preRelayedCall(bytes memory context) internal returns (bytes32);
/**
* @dev See `IRelayRecipient.postRelayedCall`.
*
* This function should not be overriden directly, use `_postRelayedCall` instead.
*
* * Requirements:
*
* - the caller must be the `RelayHub` contract.
*/
function postRelayedCall(bytes calldata context, bool success, uint256 actualCharge, bytes32 preRetVal) external {
require(msg.sender == getHubAddr(), "GSNRecipient: caller is not RelayHub");
_postRelayedCall(context, success, actualCharge, preRetVal);
}
/**
* @dev See `IRelayRecipient.postRelayedCall`.
*
* Called by `GSNRecipient.postRelayedCall`, which asserts the caller is the `RelayHub` contract. Derived contracts
* must implement this function with any relayed-call postprocessing they may wish to do.
*
*/
function _postRelayedCall(bytes memory context, bool success, uint256 actualCharge, bytes32 preRetVal) internal;
/**
* @dev Return this in acceptRelayedCall to proceed with the execution of a relayed call. Note that this contract
* will be charged a fee by RelayHub
*/
function _approveRelayedCall() internal pure returns (uint256, bytes memory) {
return _approveRelayedCall("");
}
/**
* @dev See `GSNRecipient._approveRelayedCall`.
*
* This overload forwards `context` to _preRelayedCall and _postRelayedCall.
*/
function _approveRelayedCall(bytes memory context) internal pure returns (uint256, bytes memory) {
return (RELAYED_CALL_ACCEPTED, context);
}
/**
* @dev Return this in acceptRelayedCall to impede execution of a relayed call. No fees will be charged.
*/
function _rejectRelayedCall(uint256 errorCode) internal pure returns (uint256, bytes memory) {
return (RELAYED_CALL_REJECTED + errorCode, "");
}
/*
* @dev Calculates how much RelayHub will charge a recipient for using `gas` at a `gasPrice`, given a relayer's
* `serviceFee`.
*/
function _computeCharge(uint256 gas, uint256 gasPrice, uint256 serviceFee) internal pure returns (uint256) {
// The fee is expressed as a percentage. E.g. a value of 40 stands for a 40% fee, so the recipient will be
// charged for 1.4 times the spent amount.
return (gas * gasPrice * (100 + serviceFee)) / 100;
}
function _getRelayedCallSender() private pure returns (address payable result) {
// We need to read 20 bytes (an address) located at array index msg.data.length - 20. In memory, the array
// is prefixed with a 32-byte length value, so we first add 32 to get the memory read index. However, doing

@ -1,14 +1,14 @@
pragma solidity ^0.5.0;
import "./GSNBouncerBase.sol";
import "../../math/SafeMath.sol";
import "../../ownership/Secondary.sol";
import "../../token/ERC20/SafeERC20.sol";
import "../../token/ERC20/ERC20.sol";
import "../../token/ERC20/ERC20Detailed.sol";
import "./GSNRecipient.sol";
import "../math/SafeMath.sol";
import "../ownership/Secondary.sol";
import "../token/ERC20/SafeERC20.sol";
import "../token/ERC20/ERC20.sol";
import "../token/ERC20/ERC20Detailed.sol";
/**
* @dev A xref:ROOT:gsn-bouncers.adoc#gsn-bouncers[GSN Bouncer] that charges transaction fees in a special purpose ERC20
* @dev A xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategy] that charges transaction fees in a special purpose ERC20
* token, which we refer to as the gas payment token. The amount charged is exactly the amount of Ether charged to the
* recipient. This means that the token is essentially pegged to the value of Ether.
*
@ -16,11 +16,11 @@ import "../../token/ERC20/ERC20Detailed.sol";
* whose only minter is the recipient, so the strategy must be implemented in a derived contract, making use of the
* internal {_mint} function.
*/
contract GSNBouncerERC20Fee is GSNBouncerBase {
contract GSNRecipientERC20Fee is GSNRecipient {
using SafeERC20 for __unstable__ERC20PrimaryAdmin;
using SafeMath for uint256;
enum GSNBouncerERC20FeeErrorCodes {
enum GSNRecipientERC20FeeErrorCodes {
INSUFFICIENT_BALANCE
}
@ -66,7 +66,7 @@ contract GSNBouncerERC20Fee is GSNBouncerBase {
returns (uint256, bytes memory)
{
if (_token.balanceOf(from) < maxPossibleCharge) {
return _rejectRelayedCall(uint256(GSNBouncerERC20FeeErrorCodes.INSUFFICIENT_BALANCE));
return _rejectRelayedCall(uint256(GSNRecipientERC20FeeErrorCodes.INSUFFICIENT_BALANCE));
}
return _approveRelayedCall(abi.encode(from, maxPossibleCharge, transactionFee, gasPrice));

@ -1,20 +1,20 @@
pragma solidity ^0.5.0;
import "./GSNBouncerBase.sol";
import "../../cryptography/ECDSA.sol";
import "./GSNRecipient.sol";
import "../cryptography/ECDSA.sol";
/**
* @dev A xref:ROOT:gsn-bouncers.adoc#gsn-bouncers[GSN Bouncer] that allows relayed transactions through when they are
* @dev A xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategy] that allows relayed transactions through when they are
* accompanied by the signature of a trusted signer. The intent is for this signature to be generated by a server that
* performs validations off-chain. Note that nothing is charged to the user in this scheme. Thus, the server should make
* sure to account for this in their economic and threat model.
*/
contract GSNBouncerSignature is GSNBouncerBase {
contract GSNRecipientSignature is GSNRecipient {
using ECDSA for bytes32;
address private _trustedSigner;
enum GSNBouncerSignatureErrorCodes {
enum GSNRecipientSignatureErrorCodes {
INVALID_SIGNER
}
@ -22,7 +22,7 @@ contract GSNBouncerSignature is GSNBouncerBase {
* @dev Sets the trusted signer that is going to be producing signatures to approve relayed calls.
*/
constructor(address trustedSigner) public {
require(trustedSigner != address(0), "GSNBouncerSignature: trusted signer is the zero address");
require(trustedSigner != address(0), "GSNRecipientSignature: trusted signer is the zero address");
_trustedSigner = trustedSigner;
}
@ -58,7 +58,15 @@ contract GSNBouncerSignature is GSNBouncerBase {
if (keccak256(blob).toEthSignedMessageHash().recover(approvalData) == _trustedSigner) {
return _approveRelayedCall();
} else {
return _rejectRelayedCall(uint256(GSNBouncerSignatureErrorCodes.INVALID_SIGNER));
return _rejectRelayedCall(uint256(GSNRecipientSignatureErrorCodes.INVALID_SIGNER));
}
}
function _preRelayedCall(bytes memory) internal returns (bytes32) {
// solhint-disable-previous-line no-empty-blocks
}
function _postRelayedCall(bytes memory, bool, uint256, bytes32) internal {
// solhint-disable-previous-line no-empty-blocks
}
}

@ -6,10 +6,10 @@ TIP: If you're new to the GSN, head over to our xref:openzeppelin::gsn/what-is-t
The core contract a recipient must inherit from is {GSNRecipient}: it includes all necessary interfaces, as well as some helper methods to make interacting with the GSN easier.
Utilities to make writing xref:ROOT:gsn-bouncers.adoc[GSN Bouncers] easy are available in {GSNBouncerBase}, or you can simply use one of our pre-made bouncers:
Utilities to make writing xref:ROOT:gsn-strategies.adoc[GSN strategies] easy are available in {GSNRecipient}, or you can simply use one of our pre-made strategies:
* {GSNBouncerERC20Fee} charges the end user for gas costs in an application-specific xref:ROOT:tokens.adoc#ERC20[ERC20 token]
* {GSNBouncerSignature} accepts all relayed calls that have been signed by a trusted third party (e.g. a private key in a backend)
* {GSNRecipientERC20Fee} charges the end user for gas costs in an application-specific xref:ROOT:tokens.adoc#ERC20[ERC20 token]
* {GSNRecipientSignature} accepts all relayed calls that have been signed by a trusted third party (e.g. a private key in a backend)
You can also take a look at the two contract interfaces that make up the GSN protocol: {IRelayRecipient} and {IRelayHub}, but you won't need to use those directly.
@ -19,11 +19,10 @@ NOTE: This feature is being released in the next version of OpenZeppelin Contrac
{{GSNRecipient}}
== Bouncers
== Strategies
{{GSNBouncerBase}}
{{GSNBouncerERC20Fee}}
{{GSNBouncerSignature}}
{{GSNRecipientSignature}}
{{GSNRecipientERC20Fee}}
== Protocol

@ -1,92 +0,0 @@
pragma solidity ^0.5.0;
import "../IRelayRecipient.sol";
/**
* @dev Base contract used to implement GSNBouncers.
*
* > This contract does not perform all required tasks to implement a GSN
* recipient contract: end users should use `GSNRecipient` instead.
*/
contract GSNBouncerBase is IRelayRecipient {
uint256 constant private RELAYED_CALL_ACCEPTED = 0;
uint256 constant private RELAYED_CALL_REJECTED = 11;
// How much gas is forwarded to postRelayedCall
uint256 constant internal POST_RELAYED_CALL_MAX_GAS = 100000;
// Base implementations for pre and post relayedCall: only RelayHub can invoke them, and data is forwarded to the
// internal hook.
/**
* @dev See `IRelayRecipient.preRelayedCall`.
*
* This function should not be overriden directly, use `_preRelayedCall` instead.
*
* * Requirements:
*
* - the caller must be the `RelayHub` contract.
*/
function preRelayedCall(bytes calldata context) external returns (bytes32) {
require(msg.sender == getHubAddr(), "GSNBouncerBase: caller is not RelayHub");
return _preRelayedCall(context);
}
/**
* @dev See `IRelayRecipient.postRelayedCall`.
*
* This function should not be overriden directly, use `_postRelayedCall` instead.
*
* * Requirements:
*
* - the caller must be the `RelayHub` contract.
*/
function postRelayedCall(bytes calldata context, bool success, uint256 actualCharge, bytes32 preRetVal) external {
require(msg.sender == getHubAddr(), "GSNBouncerBase: caller is not RelayHub");
_postRelayedCall(context, success, actualCharge, preRetVal);
}
/**
* @dev Return this in acceptRelayedCall to proceed with the execution of a relayed call. Note that this contract
* will be charged a fee by RelayHub
*/
function _approveRelayedCall() internal pure returns (uint256, bytes memory) {
return _approveRelayedCall("");
}
/**
* @dev See `GSNBouncerBase._approveRelayedCall`.
*
* This overload forwards `context` to _preRelayedCall and _postRelayedCall.
*/
function _approveRelayedCall(bytes memory context) internal pure returns (uint256, bytes memory) {
return (RELAYED_CALL_ACCEPTED, context);
}
/**
* @dev Return this in acceptRelayedCall to impede execution of a relayed call. No fees will be charged.
*/
function _rejectRelayedCall(uint256 errorCode) internal pure returns (uint256, bytes memory) {
return (RELAYED_CALL_REJECTED + errorCode, "");
}
// Empty hooks for pre and post relayed call: users only have to define these if they actually use them.
function _preRelayedCall(bytes memory) internal returns (bytes32) {
// solhint-disable-previous-line no-empty-blocks
}
function _postRelayedCall(bytes memory, bool, uint256, bytes32) internal {
// solhint-disable-previous-line no-empty-blocks
}
/*
* @dev Calculates how much RelaHub will charge a recipient for using `gas` at a `gasPrice`, given a relayer's
* `serviceFee`.
*/
function _computeCharge(uint256 gas, uint256 gasPrice, uint256 serviceFee) internal pure returns (uint256) {
// The fee is expressed as a percentage. E.g. a value of 40 stands for a 40% fee, so the recipient will be
// charged for 1.4 times the spent amount.
return (gas * gasPrice * (100 + serviceFee)) / 100;
}
}

@ -2,14 +2,14 @@ pragma solidity ^0.5.0;
import "../token/ERC721/ERC721.sol";
import "../GSN/GSNRecipient.sol";
import "../GSN/bouncers/GSNBouncerSignature.sol";
import "../GSN/GSNRecipientSignature.sol";
/**
* @title ERC721GSNRecipientMock
* A simple ERC721 mock that has GSN support enabled
*/
contract ERC721GSNRecipientMock is ERC721, GSNRecipient, GSNBouncerSignature {
constructor(address trustedSigner) public GSNBouncerSignature(trustedSigner) { }
contract ERC721GSNRecipientMock is ERC721, GSNRecipient, GSNRecipientSignature {
constructor(address trustedSigner) public GSNRecipientSignature(trustedSigner) { }
// solhint-disable-previous-line no-empty-blocks
function mint(uint256 tokenId) public {

@ -1,10 +1,10 @@
pragma solidity ^0.5.0;
import "../GSN/GSNRecipient.sol";
import "../GSN/bouncers/GSNBouncerERC20Fee.sol";
import "../GSN/GSNRecipientERC20Fee.sol";
contract GSNBouncerERC20FeeMock is GSNRecipient, GSNBouncerERC20Fee {
constructor(string memory name, string memory symbol) public GSNBouncerERC20Fee(name, symbol) {
contract GSNRecipientERC20FeeMock is GSNRecipient, GSNRecipientERC20Fee {
constructor(string memory name, string memory symbol) public GSNRecipientERC20Fee(name, symbol) {
// solhint-disable-previous-line no-empty-blocks
}

@ -17,11 +17,11 @@ contract GSNRecipientMock is ContextMock, GSNRecipient {
return (0, "");
}
function preRelayedCall(bytes calldata) external returns (bytes32) {
function _preRelayedCall(bytes memory) internal returns (bytes32) {
// solhint-disable-previous-line no-empty-blocks
}
function postRelayedCall(bytes calldata, bool, uint256, bytes32) external {
function _postRelayedCall(bytes memory, bool, uint256, bytes32) internal {
// solhint-disable-previous-line no-empty-blocks
}

@ -1,10 +1,10 @@
pragma solidity ^0.5.0;
import "../GSN/GSNRecipient.sol";
import "../GSN/bouncers/GSNBouncerSignature.sol";
import "../GSN/GSNRecipientSignature.sol";
contract GSNBouncerSignatureMock is GSNRecipient, GSNBouncerSignature {
constructor(address trustedSigner) public GSNBouncerSignature(trustedSigner) {
contract GSNRecipientSignatureMock is GSNRecipient, GSNRecipientSignature {
constructor(address trustedSigner) public GSNRecipientSignature(trustedSigner) {
// solhint-disable-previous-line no-empty-blocks
}

@ -10,7 +10,7 @@
.In Depth
* xref:erc20-supply.adoc[ERC20 Supply]
* xref:gsn-bouncers.adoc[GSN Bouncers]
* xref:gsn-strategies.adoc[GSN Strategies]
.FAQ
* xref:api-stability.adoc[API Stability]

@ -1,38 +1,38 @@
= GSN Bouncers
= GSN Strategies
This guide shows you different strategies to accept relayed calls via the Gas Station Network (GSN) using GSN Bouncers.
This guide shows you different strategies to accept relayed calls via the Gas Station Network (GSN) using OpenZeppelin Contracts.
First, we will explain the Bouncer concept, and then we will showcase how to use the two most common strategies.
Finally, we will cover how to create your own Custom Bouncer.
First, we will go over what a 'GSN strategy' is, and then showcase how to use the two most common strategies.
Finally, we will cover how to create your own custom strategies.
If you're still learning about the basics of the Gas Station Network, you should first head over to the xref:gsn.adoc[GSN Guide].
[[gsn-bouncers]]
== GSN Bouncers explained
[[gsn-strategies]]
== GSN strategies explained
A *GSN Bouncer* decides which relayed call gets approved and which relayed call gets rejected. Bouncers are a key concept within the GSN. Dapps need Bouncers to prevent malicious users from spending the dapp's funds for relayed call fees.
A *GSN strategy* decides which relayed call gets approved and which relayed call gets rejected. Strategies are a key concept within the GSN. Dapps need a strategy to prevent malicious users from spending the dapp's funds for relayed call fees.
As we have seen in the xref:gsn.adoc[GSN Guide], in order to be GSN enabled, your contracts need to extend from xref:api:GSN.adoc#GSNRecipient[`GSNRecipient`].
A GSN recipient contract needs the following to work:
1. It needs to have funds deposited on its RelayHub.
2. It needs to handle `msg.sender` and `msg.data` differently
2. It needs to handle `msg.sender` and `msg.data` differently.
3. It needs to decide how to approve and reject relayed calls.
Depositing funds for the GSN recipient contract can be done via the https://gsn.openzeppelin.com/recipients[GSN Dapp tool] or programmatically with https://github.com/OpenZeppelin/openzeppelin-gsn-helpers#usage-from-code[OpenZeppelin GSN Helpers].
The actual user's `msg.sender` and `msg.data` can be obtained safely via xref:api:GSN.adoc#GSNRecipient-_msgSender--[`_msgSender()`] and xref:api:GSN.adoc#GSNRecipient-_msgData--[`_msgData()`] of xref:api:GSN.adoc#GSNRecipient[`GSNRecipient`].
Deciding how to approve and reject relayed calls is a bit more complex. The GSN recipient contract, with the simplest implementation, will accept and pay for all relayed calls. Chances are you probably want to choose which users can use your contracts via the GSN and potentially charge them for it, like a bouncer at a nightclub. We call these contracts _GSN Bouncers_.
Deciding how to approve and reject relayed calls is a bit more complex. Chances are you probably want to choose which users can use your contracts via the GSN and potentially charge them for it, like a bouncer at a nightclub. We call these _GSN strategies_.
In this guide we describe how to use the included bouncers xref:api:GSN.adoc#GSNBouncerSignature[`GSNBouncerSignature`] and xref:api:GSN.adoc#GSNBouncerERC20Fee[`GSNBouncerERC20Fee`], along with how to create your own Custom Bouncer.
The base xref:api:GSN.adoc#GSNRecipient[`GSNRecipient`] contract doesn't include a strategy, so you must either use one of the pre-built ones or write your own. We will first go over using the included strategies: xref:api:GSN.adoc#GSNRecipientSignature[`GSNRecipientSignature`] and xref:api:GSN.adoc#GSNRecipientERC20Fee[`GSNRecipientERC20Fee`].
== GSNBouncerSignature
== GSNRecipientSignature
xref:api:GSN.adoc#GSNBouncerSignature[`GSNBouncerSignature`] lets users relay calls via the GSN to your recipient contract (charging you for it) if they can prove that an account you trust approved them to do so. The way they do this is via a _signature_.
xref:api:GSN.adoc#GSNRecipientSignature[`GSNRecipientSignature`] lets users relay calls via the GSN to your recipient contract (charging you for it) if they can prove that an account you trust approved them to do so. The way they do this is via a _signature_.
The relayed call must include a signature of the relayed call parameters by the same account that was added to the contract as a trusted signer. If it is not the same, `GSNBouncerSignature` will not accept the relayed call.
The relayed call must include a signature of the relayed call parameters by the same account that was added to the contract as a trusted signer. If it is not the same, `GSNRecipientSignature` will not accept the relayed call.
This means that you need to set up a system where your trusted account signs the relayed call parameters to then include in the relayed call, as long as they are valid users (according to your business logic).
@ -43,44 +43,48 @@ Alternatively, you could charge the user off-chain (e.g. via credit card) for cr
The great thing about this setup is that *your contract doesn't need to change* if you want to change the business rules. All you are doing is changing the backend logic conditions under which users use your contract for free.
On the other hand, you need to have a backend server, microservice, or lambda function to accomplish this.
=== How does GSNBouncerSignature work?
=== How does GSNRecipientSignature work?
`GSNBouncerSignature` decides whether or not to accept the relayed call based on the included signature.
`GSNRecipientSignature` decides whether or not to accept the relayed call based on the included signature.
The `acceptRelayedCall` implementation recovers the address from the signature of the relayed call parameters in `approvalData` and compares to the trusted signer.
If the included signature matches the trusted signer, the relayed call is approved.
On the other hand, when the included signature doesn't match the trusted signer, the relayed call gets rejected with an error code of `INVALID_SIGNER`.
=== How to use GSNBouncerSignature
=== How to use GSNRecipientSignature
You will need to create an off-chain service (e.g. backend server, microservice, or lambda function) that your dapp calls to sign (or not sign, based on your business logic) the relayed call parameters with your trusted signer account. The signature is then included as the `approvalData` in the relayed call.
Your GSN recipient contract needs to inherit from `GSNRecipient` and `GSNBouncerSignature`, as well as setting the trusted signer in the constructor of `GSNBouncerSignature` as per the following sample code below:
Instead of using `GSNRecipient` directly, your GSN recipient contract will instead inherit from `GSNRecipientSignature`, as well as setting the trusted signer in the constructor of `GSNRecipientSignature` as per the following sample code below:
[source,solidity]
----
contract MyContract is GSNRecipient, GSNBouncerSignature {
constructor(address trustedSigner) public GSNBouncerSignature(trustedSigner) {
import "@openzeppelin/contracts/GSN/GSNRecipientSignature";
contract MyContract is GSNRecipientSignature {
constructor(address trustedSigner) public GSNRecipientSignature(trustedSigner) {
}
}
----
== GSNBouncerERC20Fee
TIP: We wrote an in-depth guide on how to setup a signing server that works with `GSNRecipientSignature`, https://forum.openzeppelin.com/t/advanced-gsn-gsnrecipientsignature-sol/1414[check it out!]
== GSNRecipientERC20Fee
xref:api:GSN.adoc#GSNBouncerERC20Fee[`GSNBouncerERC20Fee`] is a bit more complex (but don't worry, it has already been written for you!). Unlike `GSNBouncerSignature`, `GSNBouncerERC20Fee` doesn't require any off-chain services.
xref:api:GSN.adoc#GSNRecipientERC20Fee[`GSNRecipientERC20Fee`] is a bit more complex (but don't worry, it has already been written for you!). Unlike `GSNRecipientSignature`, `GSNRecipientERC20Fee` doesn't require any off-chain services.
Instead of off-chain approving each relayed call, you will give special-purpose ERC20 tokens to your users. These tokens are then used as payment for relayed calls to your recipient contract.
Any user that has enough tokens to pay has their relayed calls automatically approved and the recipient contract will cover their transaction costs!
Each recipient contract has their own special-purpose token. The exchange rate from token to ether is 1:1, as the tokens are used to pay your contract to cover the gas fees when using the GSN.
`GSNBouncerERC20Fee` has an internal xref:api:GSN.adoc#GSNBouncerERC20Fee-_mint-address-uint256-[`_mint`] function. Firstly, you need to setup a way to call it (e.g. add a public function with some form of xref:access-control.adoc[access control] such as xref:api:access.adoc#MinterRole-onlyMinter--[`onlyMinter`]).
`GSNRecipientERC20Fee` has an internal xref:api:GSN.adoc#GSNRecipientERC20Fee-_mint-address-uint256-[`_mint`] function. Firstly, you need to setup a way to call it (e.g. add a public function with some form of xref:access-control.adoc[access control] such as xref:api:access.adoc#MinterRole-onlyMinter--[`onlyMinter`]).
Then, issue tokens to users based on your business logic. For example, you could mint a limited amount of tokens to new users, mint tokens when users buy them off-chain, give tokens based on a users subscription, etc.
NOTE: *Users do not need to call approve* on their tokens for your recipient contract to use them. They are a modified ERC20 variant that lets the recipient contract retrieve them.
=== How does GSNBouncerERC20Fee work?
=== How does GSNRecipientERC20Fee work?
`GSNBouncerERC20Fee` decides to approve or reject relayed calls based on the balance of the users tokens.
`GSNRecipientERC20Fee` decides to approve or reject relayed calls based on the balance of the users tokens.
The `acceptRelayedCall` function implementation checks the users token balance.
If the user doesn't have enough tokens the relayed call gets rejected with an error of `INSUFFICIENT_BALANCE`.
@ -96,14 +100,16 @@ NOTE: The gas cost estimation is not 100% accurate, we may tweak it further down
NOTE: Always use `_preRelayedCall` and `_postRelayedCall` functions. Internal `_preRelayedCall` and `_postRelayedCall` functions are used instead of public `preRelayedCall` and `postRelayedCall` functions, as the public functions are prevented from being called by non-RelayHub contracts.
=== How to use GSNBouncerERC20Fee
=== How to use GSNRecipientERC20Fee
Your GSN recipient contract needs to inherit from `GSNRecipient` and `GSNBouncerERC20Fee` along with appropriate xref:access-control.adoc[access control] (for token minting), set the token details in the constructor of `GSNBouncerERC20Fee` and create a public `mint` function suitably protected by your chosen access control as per the following sample code (which uses the xref:api:access.adoc#MinterRole[MinterRole]):
Your GSN recipient contract needs to inherit from `GSNRecipientERC20Fee` along with appropriate xref:access-control.adoc[access control] (for token minting), set the token details in the constructor of `GSNRecipientERC20Fee` and create a public `mint` function suitably protected by your chosen access control as per the following sample code (which uses the xref:api:access.adoc#MinterRole[MinterRole]):
[source,solidity]
----
contract MyContract is GSNRecipient, GSNBouncerERC20Fee, MinterRole {
constructor() public GSNBouncerERC20Fee("FeeToken", "FEE") {
import "@openzeppelin/contracts/GSN/GSNRecipientERC20Fee";
contract MyContract is GSNRecipientERC20Fee, MinterRole {
constructor() public GSNRecipientERC20Fee("FeeToken", "FEE") {
}
function mint(address account, uint256 amount) public onlyMinter {
@ -112,28 +118,26 @@ contract MyContract is GSNRecipient, GSNBouncerERC20Fee, MinterRole {
}
----
== Custom Bouncer
== Custom strategies
If the included strategies don't quite fit your business needs, you can also write your own! For example, your custom strategy could use a specified token to pay for relayed calls with a custom exchange rate to ether. Alternatively you could issue users who subscribe to your dapp ERC721 tokens, and accounts holding the subscription token could use your contract for free as part of the subscription. There are lots of potential options!
You can create your own Custom Bouncer! For example, your Custom Bouncer could use a specified token to pay for relayed calls with a custom exchange rate to ether. Alternatively you could issue users who subscribe to your dapp ERC721 tokens and accounts holding the subscription token could use your contract for free as part of the subscription. There are lots of potential options for your Custom Bouncer.
To write a custom strategy, simply inherit from `GSNRecipient` and implement the `acceptRelayedCall`, `_preRelayedCall` and `_postRelayedCall` functions.
Your Custom Bouncer can inherit from `GSNBouncerBase` and must implement the `acceptRelayedCall` function.
Your `acceptRelayedCall` implementation decides whether or not to accept the relayed call: return `_approveRelayedCall` to accept, and return `_rejectRelayedCall` with an error code to reject.
Your `acceptRelayedCall` implementation decides whether or not to accept the relayed call.
If your logic accepts the relayed call then you should return `_approveRelayedCall`.
If your logic rejects the relayed call then you should return `_rejectRelayedCall` with an error code.
See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/GSN/bouncers/GSNBouncerSignature.sol[GSNBouncerSignature.sol] as an example implementation.
Not all GSN strategies use `_preRelayedCall` and `_postRelayedCall` (though they must still be implemented, e.g. `GSNRecipientSignature` leaves them empty), but are useful when your strategy involves charging end users.
For Custom Bouncers charging end users, `_postRelayedCall` and `_preRelayedCall` should be implemented to handle the charging.
Your `_preRelayedCall` implementation should take the maximum possible charge, with your `_postRelayedCall` implementation refunding any difference from the actual charge once the relayed call has been made.
`_preRelayedCall` should take the maximum possible charge, with `_postRelayedCall` refunding any difference from the actual charge once the relayed call has been made.
When returning `_approveRelayedCall` to approve the relayed call, the end users address, `maxPossibleCharge`, `transactionFee` and `gasPrice` data can also be returned so that the data can be used in `_preRelayedCall` and `_postRelayedCall`.
See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v2.4.0/contracts/GSN/bouncers/GSNBouncerERC20Fee.sol[GSNBouncerERC20Fee.sol] as an example implementation.
See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v2.4.0/contracts/GSN/GSNRecipientERC20Fee.sol[the code for `GSNRecipientERC20Fee`] as an example implementation.
Your GSN recipient contract needs to inherit from `GSNRecipient` and your Custom Bouncer as per the following sample code:
Once your strategy is ready, all your GSN recipient needs to do is inherit from it:
[source,solidity]
----
contract MyContract is GSNRecipient, MyCustomBouncer {
constructor() public MyCustomBouncer() {
contract MyContract is MyCustomGSNStrategy {
constructor() public MyCustomGSNStrategy() {
}
}
----

@ -3,16 +3,16 @@ const gsn = require('@openzeppelin/gsn-helpers');
const { expect } = require('chai');
const GSNBouncerERC20FeeMock = artifacts.require('GSNBouncerERC20FeeMock');
const GSNRecipientERC20FeeMock = artifacts.require('GSNRecipientERC20FeeMock');
const ERC20Detailed = artifacts.require('ERC20Detailed');
const IRelayHub = artifacts.require('IRelayHub');
contract('GSNBouncerERC20Fee', function ([_, sender, other]) {
contract('GSNRecipientERC20Fee', function ([_, sender, other]) {
const name = 'FeeToken';
const symbol = 'FTKN';
beforeEach(async function () {
this.recipient = await GSNBouncerERC20FeeMock.new(name, symbol);
this.recipient = await GSNRecipientERC20FeeMock.new(name, symbol);
this.token = await ERC20Detailed.at(await this.recipient.token());
});

@ -4,11 +4,11 @@ const { fixSignature } = require('../helpers/sign');
const { utils: { toBN } } = require('web3');
const { ZERO_ADDRESS } = constants;
const GSNBouncerSignatureMock = artifacts.require('GSNBouncerSignatureMock');
const GSNRecipientSignatureMock = artifacts.require('GSNRecipientSignatureMock');
contract('GSNBouncerSignature', function ([_, signer, other]) {
contract('GSNRecipientSignature', function ([_, signer, other]) {
beforeEach(async function () {
this.recipient = await GSNBouncerSignatureMock.new(signer);
this.recipient = await GSNRecipientSignatureMock.new(signer);
});
context('when called directly', function () {
@ -21,10 +21,10 @@ contract('GSNBouncerSignature', function ([_, signer, other]) {
context('when constructor is called with a zero address', function () {
it('fails when constructor called with a zero address', async function () {
await expectRevert(
GSNBouncerSignatureMock.new(
GSNRecipientSignatureMock.new(
ZERO_ADDRESS
),
'GSNBouncerSignature: trusted signer is the zero address'
'GSNRecipientSignature: trusted signer is the zero address'
);
});
});
@ -66,7 +66,7 @@ contract('GSNBouncerSignature', function ([_, signer, other]) {
const { tx } = await this.recipient.mockFunction({ value: 0, useGSN: true, approveFunction });
await expectEvent.inTransaction(tx, GSNBouncerSignatureMock, 'MockFunctionCalled');
await expectEvent.inTransaction(tx, GSNRecipientSignatureMock, 'MockFunctionCalled');
});
it('rejects relay requests where all parameters are signed by an invalid signer', async function () {
Loading…
Cancel
Save