diff --git a/docs/modules/ROOT/pages/gsn-bouncers.adoc b/docs/modules/ROOT/pages/gsn-bouncers.adoc index 7a3affca36a..d43808f17c6 100644 --- a/docs/modules/ROOT/pages/gsn-bouncers.adoc +++ b/docs/modules/ROOT/pages/gsn-bouncers.adoc @@ -1,239 +1,141 @@ -= Advanced GSN Guide (Bouncers) += GSN Bouncers -This guide shows you different strategies (Bouncers) to accept transactions via Gas Station Network. +This guide shows you different strategies to accept relayed calls via the Gas Station Network (GSN) using GSN Bouncers. 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. -Finally, we will cover how to create your own custom Bouncer. - -If you're still learning about the basics of the Gas Station Network, you should head over to our xref:gsn.adoc[GSN Guide], which will help you get started from scratch. +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 +== GSN Bouncers explained -A *GSN Bouncer* decides which transaction gets approved and which transaction gets rejected. Bouncers are a key concept within GSN. Dapps need Bouncers to prevent malicious users from spending the subsidies for the transactions. +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. -As we have seen in the Basic Guide, in order to use GSN, your contracts need to extend from GSN Recipient. +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: -* It needs to have funds deposited on its RelayHub. -* It needs to handle `msg.sender` and `msg.data` differently -* It needs to decide how to approve and reject transactions. +1. It needs to have funds deposited on its RelayHub. +2. It needs to handle `msg.sender` and `msg.data` differently +3. It needs to decide how to approve and reject relayed calls. -The first can be done via the https://gsn.openzeppelin.com/recipients[GSN Tools] or programatically with our SDK. +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 sender and data can be used safely when using `GSNRecipient` and _msgSender & _msgData. +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`]. -The third is a bit more complex. The GSN Recipient, by default, will accept and pay for all transactions. 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 _GSNBouncers_. +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_. -We include two of them below, ready to use out of the box. +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. == GSNBouncerSignature -This bouncer lets users call into your recipient contract via the GSN (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#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_. -The signature used to create the transaction must be added to the contract as a trusted signer. If it is not the same, this bouncer will not accept the transaction. +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. -This means that you need to set up a system where your trusted account signs call requests, as long as they are valid users. +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). -The definition of a valid user depends on your system, but an example is users that have completed their sign up via some kind of oauth and validation, e.g., gone through a captcha or validated their email address. -You could restrict it further and let new users send a specific number of transactions (e.g., 5 requests via the GSN, at which point they need to create a wallet). -Alternatively, you could charge them off-chain (e.g., via credit card) for credit on your system and let them run GSN calls until said credit runs out. +The definition of a valid user depends on your system, but an example is users that have completed their sign up via some kind of https://en.wikipedia.org/wiki/OAuth[OAuth] and validation, e.g. gone through a captcha or validated their email address. +You could restrict it further and let new users send a specific number of relayed calls (e.g. limit to 5 relayed calls via the GSN, at which point the user needs to create a wallet). +Alternatively, you could charge the user off-chain (e.g. via credit card) for credit on your system and let them create relayed calls until their credit runs out. -The great thing about this setup is that *your contract doesn't need to change*. All you're doing is changing the backend logic conditions under which users call into your contract for free. +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 it work? - -Here is the definition of acceptRelayedCall function. +=== How does GSNBouncerSignature work? -It decides whether or not to accept the call based on the signature. No further gsn-actions need to be taken. +`GSNBouncerSignature` decides whether or not to accept the relayed call based on the included signature. -It only relies on the approvalData and does not use callData. +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`. -[source,solidity] ----- -function acceptRelayedCall( - address relay, - address from, - bytes calldata encodedFunction, - uint256 transactionFee, - uint256 gasPrice, - uint256 gasLimit, - uint256 nonce, - bytes calldata approvalData, - uint256 - ) - external - view - returns (uint256, bytes memory) - { - bytes memory blob = abi.encodePacked( - relay, - from, - encodedFunction, - transactionFee, - gasPrice, - gasLimit, - nonce, // Prevents replays on RelayHub - getHubAddr(), // Prevents replays in multiple RelayHubs - address(this) // Prevents replays in multiple recipients - ); - if (keccak256(blob).toEthSignedMessageHash().recover(approvalData) == _trustedSigner) { - return _approveRelayedCall(); - } else { - return _rejectRelayedCall(uint256(GSNRecipientSignedDataErrorCodes.INVALID_SIGNER)); - } - } ----- +=== How to use GSNBouncerSignature -==== Approve/Reject +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. -When the signatures match, the function returns the following: +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: [source,solidity] ---- - - return _approveRelayedCall(); - - // Defined on base class GSNBouncerBase - // uint256 constant private RELAYED_CALL_ACCEPTED = 0; - // function _approveRelayedCall(bytes memory context) internal pure returns (uint256, bytes memory) { - // return (RELAYED_CALL_ACCEPTED, context); - // } +contract MyContract is GSNRecipient, GSNBouncerSignature { + constructor(address trustedSigner) public GSNBouncerSignature(trustedSigner) { + } +} ---- -On the other hand, when the signatures don't match, the call gets rejected with the following: +== GSNBouncerERC20Fee -[source,solidity] ----- +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. +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! - return _rejectRelayedCall(uint256(GSNBouncerSignatureErrorCodes.INVALID_SIGNER)); +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. - // Defined on base class GSNBouncerBase - // uint256 constant private RELAYED_CALL_REJECTED = 11; - // function _rejectRelayedCall(uint256 errorCode) internal pure returns (uint256, bytes memory) { - // return (RELAYED_CALL_REJECTED + errorCode, ""); - // } ----- +`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`]). +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 to use it +=== How does GSNBouncerERC20Fee work? -Create your contract using the following: +`GSNBouncerERC20Fee` decides to approve or reject relayed calls based on the balance of the users tokens. -[source,solidity] ----- - mycontract is GSNRecipient, GSNBouncerSignature { - constructor(trusted_address) // that's it - } ----- +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`. +If there are enough tokens, the relayed call is approved with the end users address, `maxPossibleCharge`, `transactionFee` and `gasPrice` data being returned so it can be used in `_preRelayedCall` and `_postRelayedCall`. -== GSNBouncerERC20Fee +In `_preRelayedCall` function the `maxPossibleCharge` amount of tokens is transferred to the recipient contract. +The maximum amount of tokens required is transferred assuming that the relayed call will use all the gas available. +Then, in the `_postRelayedCall` function, the actual amount is calculated, including the recipient contract implementation and ERC20 token transfers, and the difference is refunded. -This bouncer is a bit more complex (but don't worry, we've already written it for you!). Unlike `GSNBouncerSignature`, this Bouncer doesn't require any off-chain services. -Instead of manually approving each transaction, you will give tokens to your users. These tokens are then used to pay for GSN calls to your recipient contract. -Any user that has enough tokens is automatically approved and the recipient contract will cover his transaction costs! +The maximum amount of tokens required is transferred in `_preRelayedCall` to protect the contract from exploits (this is really similar to how ether is locked in Ethereum transactions). -This bouncer charges users for the ether cost your recipient will incur. Each recipient contract has their own unique token, with a baked-in exchange rate of 1:1 to ether, since they act as an ether replacement when using the GSN. +NOTE: The gas cost estimation is not 100% accurate, we may tweak it further down the road. -The recipient has an internal mint function. Firstly, you need to setup a way to call it (e.g., add a public function with onlyOwner or some other form of access control). -Then, issue tokens to users based on your business logic. For example, you could mint limited tokens to new users, mint tokens when they buy them off-chain, give tokens based on the user subscription, etc. +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. -NOTE: *Users do not need call approve* on their tokens for your recipient to use them. They are a modified ERC20 variant that lets the recipient contract retrieve them. +=== How to use GSNBouncerERC20Fee -=== How does it work? +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]): -Let's look at how this Bouncer decides to approve or reject transactions. +NOTE: The token must have decimals of 18 to match that of ether, due to the baked-in exchange rate of 1:1. [source,solidity] ---- -function acceptRelayedCall( - address, - address from, - bytes calldata, - uint256 transactionFee, - uint256 gasPrice, - uint256, - uint256, - bytes calldata, - uint256 maxPossibleCharge -) - external - view - returns (uint256, bytes memory) -{ - if (_token.balanceOf(from) < maxPossibleCharge) { - return _rejectRelayedCall(uint256(GSNBouncerERC20FeeErrorCodes.INSUFFICIENT_BALANCE)); - } else if (_token.allowance(from, address(this)) < maxPossibleCharge) { - return _rejectRelayedCall(uint256(GSNBouncerERC20FeeErrorCodes.INSUFFICIENT_ALLOWANCE)); +contract MyContract is GSNRecipient, GSNBouncerERC20Fee, MinterRole { + constructor() public GSNBouncerERC20Fee("FeeToken", "FEE", 18) { } - return _approveRelayedCall(abi.encode(from, maxPossibleCharge, transactionFee, gasPrice)); -} ----- - -The bouncer rejects the tx if the real sender doesn't have enough tokens or they are not allowed to spend that amount. -If the sender can spend the tokens, the bouncers approve the transaction and overrides _confirmRelayedCall to make that data available to pre and post. - -Now, let's see how we perform the token transfer inside the _preRelayedCall method. - -[source,solidity] ----- -function _preRelayedCall(bytes memory context) internal returns (bytes32) { - (address from, uint256 maxPossibleCharge) = abi.decode(context, (address, uint256)); - - // The maximum token charge is pre-charged from the user - _token.safeTransferFrom(from, address(this), maxPossibleCharge); + function mint(address account, uint256 amount) public onlyMinter { + _mint(account, amount); + } } ---- -We transfer the max amount of tokens assuming that the call will use all the gas available. -Then, in the _postRelayedCall method, we calculate the actual amount - including the implementation and ERC transfers - and refund the difference. - -[source,solidity] ----- -function _postRelayedCall(bytes memory context, bool, uint256 actualCharge, bytes32) internal { - (address from, uint256 maxPossibleCharge, uint256 transactionFee, uint256 gasPrice) = - abi.decode(context, (address, uint256, uint256, uint256)); - - // actualCharge is an _estimated_ charge, which assumes postRelayedCall will use all available gas. - // This implementation's gas cost can be roughly estimated as 10k gas, for the two SSTORE operations in an - // ERC20 transfer. - uint256 overestimation = _computeCharge(POST_RELAYED_CALL_MAX_GAS.sub(10000), gasPrice, transactionFee); - actualCharge = actualCharge.sub(overestimation); - - // After the relayed call has been executed and the actual charge estimated, the excess pre-charge is returned - _token.safeTransfer(from, maxPossibleCharge.sub(actualCharge)); -} ----- +== Custom Bouncer -This is required to protect the contract from exploits (this is really similar to how ether is locked in Ethereum transactions). +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. -Please note how the gas cost estimation is not 100% accurate, we may tweak it further down the road. +Your Custom Bouncer can inherit from `GSNBouncerBase` and must implement the `acceptRelayedCall` function. -NOTE: `_preRelayedCall` and `_postRelayedCall` are used instead of preRelayedCall and postRelayedCall. This prevents them from being called by non-relayhub. Always use _pre and _post methods. +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. -=== How to use it +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. +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. -Create your contract using the following: +Your GSN recipient contract needs to inherit from `GSNRecipient` and your Custom Bouncer as per the following sample code: [source,solidity] ---- - mycontract is GSNRecipient, GSNBouncerERC20Fee { - constructor(name symbol decimals) - - mint() { - _mint() +contract MyContract is GSNRecipient, MyCustomBouncer { + constructor() public MyCustomBouncer() { } - } +} ---- - -== Create your custom Bouncer [optional, for power users] - -You can use 'GSNBouncerBase' as an example to guide your Bouncer implementation. - -The only thing you must do is extend from `GSNRecipient` and implement the accept method. - -Depending on your logic, you may need to implement `_postRelayedCall` and `_preRelayedCall`.