Merge branch 'master' into release-v2.4.0

release-v2.4.0
Francisco Giordano 6 years ago
commit c0042cd9d0
  1. 1
      .editorconfig
  2. 3
      CHANGELOG.md
  3. 15
      DOCUMENTATION.md
  4. 26
      contracts/GSN/GSNContext.sol
  5. 17
      contracts/GSN/GSNRecipient.sol
  6. 6
      contracts/GSN/README.adoc
  7. 31
      contracts/GSN/bouncers/GSNBouncerERC20Fee.sol
  8. 12
      contracts/GSN/bouncers/GSNBouncerSignature.sol
  9. 9
      contracts/math/SafeMath.sol
  10. 4
      contracts/mocks/GSNContextMock.sol
  11. 50
      contracts/mocks/SignatureBouncerMock.sol
  12. 3
      contracts/utils/Address.sol
  13. 41
      docs/modules/ROOT/pages/gsn-advanced.adoc
  14. 58
      docs/modules/ROOT/pages/gsn.adoc
  15. 2
      netlify.toml
  16. 2
      package.json
  17. 2
      scripts/docs.sh
  18. 3
      scripts/release/update-changelog-release-date.js
  19. 12
      test/cryptography/ECDSA.test.js
  20. 223
      test/drafts/SignatureBouncer.test.js

@ -6,7 +6,6 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

@ -10,7 +10,8 @@
* `Address.isContract`: switched from `extcodesize` to `extcodehash` for less gas usage. ([#1802](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1802))
* `SafeMath`: added custom error messages support for `sub`, `div` and `mod` functions. `ERC20` and `ERC777` updated to throw custom errors on subtraction overflows. ([#1828](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1828))
### Bugfixes
### Breaking changes in drafts:
* `SignatureBouncer` has been removed from the library, both to avoid confusions with the GSN Bouncers and `GSNBouncerSignature` and because the API was not very clear. ([#1879](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1879))
## 2.3.0 (2019-05-27)

@ -1,5 +1,4 @@
We're building an improved documentation website. It's still in development and
contributions will be really appreciated.
Documentation is hosted at https://docs.openzeppelin.com/contracts.
All of the content for the site is in this repository. The guides are in the
[docs](/docs) directory, and the API Reference is extracted from comments in
@ -9,11 +8,9 @@ repository you should be contributing to.
[`solidity-docgen`](https://github.com/OpenZeppelin/solidity-docgen) is the
program that extracts the API Reference from source code.
The [`openzeppelin-docsite`](https://github.com/OpenZeppelin/openzeppelin-docsite)
repository hosts the configuration for Docusaurus, the static site generator
that we use.
The [`docs.openzeppelin.com`](https://github.com/OpenZeppelin/docs.openzeppelin.com)
repository hosts the configuration for the entire site, which includes
documetation for all of the OpenZeppelin projects.
To run the docsite locally you should run `npm run docsite start` on this
repository. This will live reload as the guides are edited, but not with
changes to the source code comments, for that you need to restart the server.
This should be improved eventually (contributions welcome!).
To run the docs locally you should run `npm run docs start` on this
repository.

@ -11,36 +11,22 @@ import "./Context.sol";
* recipient contract: end users should use `GSNRecipient` instead.
*/
contract GSNContext is Context {
// We use a random storage slot to allow proxy contracts to enable GSN support in an upgrade without changing their
// storage layout. This value is calculated as: keccak256('gsn.relayhub.address'), minus 1.
bytes32 private constant RELAY_HUB_ADDRESS_STORAGE_SLOT = 0x06b7792c761dcc05af1761f0315ce8b01ac39c16cc934eb0b2f7a8e71414f262;
address internal _relayHub = 0xD216153c06E857cD7f72665E0aF1d7D82172F494;
event RelayHubChanged(address indexed oldRelayHub, address indexed newRelayHub);
constructor() internal {
_upgradeRelayHub(0xD216153c06E857cD7f72665E0aF1d7D82172F494);
}
function _getRelayHub() internal view returns (address relayHub) {
bytes32 slot = RELAY_HUB_ADDRESS_STORAGE_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly {
relayHub := sload(slot)
}
// solhint-disable-previous-line no-empty-blocks
}
function _upgradeRelayHub(address newRelayHub) internal {
address currentRelayHub = _getRelayHub();
address currentRelayHub = _relayHub;
require(newRelayHub != address(0), "GSNContext: new RelayHub is the zero address");
require(newRelayHub != currentRelayHub, "GSNContext: new RelayHub is the current one");
emit RelayHubChanged(currentRelayHub, newRelayHub);
bytes32 slot = RELAY_HUB_ADDRESS_STORAGE_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly {
sstore(slot, newRelayHub)
}
_relayHub = newRelayHub;
}
// Overrides for Context's functions: when called from RelayHub, sender and
@ -49,7 +35,7 @@ contract GSNContext is Context {
// when handling said data.
function _msgSender() internal view returns (address) {
if (msg.sender != _getRelayHub()) {
if (msg.sender != _relayHub) {
return msg.sender;
} else {
return _getRelayedCallSender();
@ -57,7 +43,7 @@ contract GSNContext is Context {
}
function _msgData() internal view returns (bytes memory) {
if (msg.sender != _getRelayHub()) {
if (msg.sender != _relayHub) {
return msg.data;
} else {
return _getRelayedCallData();

@ -11,10 +11,18 @@ import "./IRelayHub.sol";
* must do so themselves.
*/
contract GSNRecipient is IRelayRecipient, GSNContext, GSNBouncerBase {
/**
* @dev Returns the RelayHub address for this recipient contract.
*/
function getHubAddr() public view returns (address) {
return _getRelayHub();
return _relayHub;
}
/**
* @dev This function returns the version string of the RelayHub for which
* this recipient implementation was built. It's not currently used, but
* may be used by tooling.
*/
// This function is view for future-proofing, it may require reading from
// storage in the future.
function relayHubVersion() public view returns (string memory) {
@ -22,7 +30,12 @@ contract GSNRecipient is IRelayRecipient, GSNContext, GSNBouncerBase {
return "1.0.0";
}
/**
* @dev Triggers a withdraw of the recipient's deposits in RelayHub. Can
* be used by derived contracts to expose the functionality in an external
* interface.
*/
function _withdrawDeposits(uint256 amount, address payable payee) internal {
IRelayHub(_getRelayHub()).withdraw(amount, payee);
IRelayHub(_relayHub).withdraw(amount, payee);
}
}

@ -1,4 +1,8 @@
= GSN
= Gas Station Network
NOTE: This feature is being released in the next version of OpenZeppelin Contracts, available right now through `npm install @openzeppelin/contracts@next`.
TIP: Check out our guide on the xref:ROOT:gsn.adoc[basics of the GSN] as well as the xref:ROOT:gsn-advanced.adoc[more advanced topics].
== Recipient

@ -7,6 +7,15 @@ import "../../token/ERC20/SafeERC20.sol";
import "../../token/ERC20/ERC20.sol";
import "../../token/ERC20/ERC20Detailed.sol";
/**
* @dev A xref:ROOT:gsn-advanced.adoc#gsn-bouncers[GSN Bouncer] 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.
*
* The distribution strategy of the gas payment token to users is not defined by this contract. It's a mintable token
* 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 {
using SafeERC20 for __unstable__ERC20PrimaryAdmin;
using SafeMath for uint256;
@ -17,18 +26,31 @@ contract GSNBouncerERC20Fee is GSNBouncerBase {
__unstable__ERC20PrimaryAdmin private _token;
/**
* @dev The arguments to the constructor are the details that the gas payment token will have: `name`, `symbol`, and
* `decimals`.
*/
constructor(string memory name, string memory symbol, uint8 decimals) public {
_token = new __unstable__ERC20PrimaryAdmin(name, symbol, decimals);
}
/**
* @dev Returns the gas payment token.
*/
function token() public view returns (IERC20) {
return IERC20(_token);
}
/**
* @dev Internal function that mints the gas payment token. Derived contracts should expose this function in their public API, with proper access control mechanisms.
*/
function _mint(address account, uint256 amount) internal {
_token.mint(account, amount);
}
/**
* @dev Ensures that only users with enough gas payment token balance can have transactions relayed through the GSN.
*/
function acceptRelayedCall(
address,
address from,
@ -51,6 +73,12 @@ contract GSNBouncerERC20Fee is GSNBouncerBase {
return _approveRelayedCall(abi.encode(from, maxPossibleCharge, transactionFee, gasPrice));
}
/**
* @dev Implements the precharge to the user. The maximum possible charge (depending on gas limit, gas price, and
* fee) will be deducted from the user balance of gas payment token. Note that this is an overestimation of the
* actual charge, necessary because we cannot predict how much gas the execution will actually need. The remainder
* is returned to the user in {_postRelayedCall}.
*/
function _preRelayedCall(bytes memory context) internal returns (bytes32) {
(address from, uint256 maxPossibleCharge) = abi.decode(context, (address, uint256));
@ -58,6 +86,9 @@ contract GSNBouncerERC20Fee is GSNBouncerBase {
_token.safeTransferFrom(from, address(this), maxPossibleCharge);
}
/**
* @dev Returns to the user the extra amount that was previously charged, once the actual execution cost is known.
*/
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));

@ -3,6 +3,12 @@ pragma solidity ^0.5.0;
import "./GSNBouncerBase.sol";
import "../../cryptography/ECDSA.sol";
/**
* @dev A xref:ROOT:gsn-advanced.adoc#gsn-bouncers[GSN Bouncer] 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 {
using ECDSA for bytes32;
@ -12,10 +18,16 @@ contract GSNBouncerSignature is GSNBouncerBase {
INVALID_SIGNER
}
/**
* @dev Sets the trusted signer that is going to be producing signatures to approve relayed calls.
*/
constructor(address trustedSigner) public {
_trustedSigner = trustedSigner;
}
/**
* @dev Ensures that only transactions with a trusted signature can be relayed through the GSN.
*/
function acceptRelayedCall(
address relay,
address from,

@ -51,6 +51,9 @@ library SafeMath {
*
* Requirements:
* - Subtraction cannot overflow.
*
* NOTE: This is a feature of the next version of OpenZeppelin Contracts.
* @dev Get it via `npm install @openzeppelin/contracts@next`.
*/
function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b <= a, errorMessage);
@ -107,6 +110,9 @@ library SafeMath {
*
* Requirements:
* - The divisor cannot be zero.
* NOTE: This is a feature of the next version of OpenZeppelin Contracts.
* @dev Get it via `npm install @openzeppelin/contracts@next`.
*/
function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
@ -142,6 +148,9 @@ library SafeMath {
*
* Requirements:
* - The divisor cannot be zero.
*
* NOTE: This is a feature of the next version of OpenZeppelin Contracts.
* @dev Get it via `npm install @openzeppelin/contracts@next`.
*/
function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b != 0, errorMessage);

@ -7,7 +7,7 @@ import "../GSN/IRelayRecipient.sol";
// By inheriting from GSNContext, Context's internal functions are overridden automatically
contract GSNContextMock is ContextMock, GSNContext, IRelayRecipient {
function getHubAddr() public view returns (address) {
return _getRelayHub();
return _relayHub;
}
function acceptRelayedCall(
@ -37,7 +37,7 @@ contract GSNContextMock is ContextMock, GSNContext, IRelayRecipient {
}
function getRelayHub() public view returns (address) {
return _getRelayHub();
return _relayHub;
}
function upgradeRelayHub(address newRelayHub) public {

@ -1,50 +0,0 @@
pragma solidity ^0.5.0;
import "../drafts/SignatureBouncer.sol";
import "./SignerRoleMock.sol";
contract SignatureBouncerMock is SignatureBouncer, SignerRoleMock {
function checkValidSignature(address account, bytes memory signature)
public view returns (bool)
{
return _isValidSignature(account, signature);
}
function onlyWithValidSignature(bytes memory signature)
public onlyValidSignature(signature) view
{
// solhint-disable-previous-line no-empty-blocks
}
function checkValidSignatureAndMethod(address account, bytes memory signature)
public view returns (bool)
{
return _isValidSignatureAndMethod(account, signature);
}
function onlyWithValidSignatureAndMethod(bytes memory signature)
public onlyValidSignatureAndMethod(signature) view
{
// solhint-disable-previous-line no-empty-blocks
}
function checkValidSignatureAndData(address account, bytes memory, uint, bytes memory signature)
public view returns (bool)
{
return _isValidSignatureAndData(account, signature);
}
function onlyWithValidSignatureAndData(uint, bytes memory signature)
public onlyValidSignatureAndData(signature) view
{
// solhint-disable-previous-line no-empty-blocks
}
function theWrongMethod(bytes memory) public pure {
// solhint-disable-previous-line no-empty-blocks
}
function tooShortMsgData() public onlyValidSignatureAndData("") view {
// solhint-disable-previous-line no-empty-blocks
}
}

@ -33,6 +33,9 @@ library Address {
/**
* @dev Converts an `address` into `address payable`. Note that this is
* simply a type cast: the actual underlying value is not changed.
*
* NOTE: This is a feature of the next version of OpenZeppelin Contracts.
* @dev Get it via `npm install @openzeppelin/contracts@next`.
*/
function toPayable(address account) internal pure returns (address payable) {
return address(uint160(account));

@ -2,15 +2,16 @@
This guide shows you different strategies (Bouncers) to accept transactions via Gas Station Network.
First, we will explain the Bouncer concept and then we will showcase how to use the two most common strategies.
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.
If you're still learning about the basics of the Gas Station Network, you should head over to our xref:api:gsn.adoc[GSN Guide], which will help you get started from scratch.
[[gsn-bouncers]]
== GSN Bouncers
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 to spend the transactions subsidy.
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.
As we have seen in the Basic Guide, in order to use GSN, your contracts need to extend from GSN Recipient.
@ -24,7 +25,7 @@ The first can be done via the https://gsn.openzeppelin.com/recipients[GSN Tools]
The sender and data can be used safely when using `GSNRecipient` and _msgSender & _msgData.
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_.
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_.
We include two of them below, ready to use out of the box.
@ -34,20 +35,20 @@ This bouncer lets users call into your recipient contract via the GSN (charging
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.
This means that you need to set up a system where your trusted account signs call requests, if they are valid users.
This means that you need to set up a system where your trusted account signs call requests, as long as they are valid users.
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 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 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.
On the other hand, you need to have a backend server, microservice or lambda function to accomplish this.
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.
It decides whether or not accept the call based on the signature. No further gsn-actions need to be taken.
It decides whether or not to accept the call based on the signature. No further gsn-actions need to be taken.
It only relies on the approvalData and does not use callData.
@ -118,7 +119,7 @@ On the other hand, when the signatures don't match, the call gets rejected with
----
=== How to use it?
=== How to use it
Create your contract using the following:
@ -131,14 +132,14 @@ Create your contract using the following:
== GSNBouncerERC20Fee
This bouncer is a bit more complex (but don't worry we've already wrote it for you!). Unlike `GSNBouncerSignature`, this Bouncer doesn't require any off-chain services.
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!
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.
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.
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: *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.
@ -173,10 +174,10 @@ function acceptRelayedCall(
}
----
The bouncer rejects the tx if the real sender doesn't have enough tokens or it is not allowed to spend that amount.
If the sender can spend the tokens, the bouncers approves the transaction and overrides _confirmRelayedCall to make that data available to pre and post.
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.
Let's see now how we perform the token transfer inside the _preRelayedCall method.
Now, let's see how we perform the token transfer inside the _preRelayedCall method.
[source,solidity]
----
@ -189,7 +190,7 @@ function _preRelayedCall(bytes memory context) internal returns (bytes32) {
----
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.
Then, in the _postRelayedCall method, we calculate the actual amount - including the implementation and ERC transfers - and refund the difference.
[source,solidity]
----
@ -210,11 +211,11 @@ function _postRelayedCall(bytes memory context, bool, uint256 actualCharge, byte
This is required to protect the contract from exploits (this is really similar to how ether is locked in Ethereum transactions).
Please not how the gas cost estimation is not 100% accurate, we may tweak it further down the road.
Please note how the gas cost estimation is not 100% accurate, we may tweak it further down the road.
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.
=== How to use it?
=== How to use it
Create your contract using the following:
@ -233,6 +234,6 @@ Create your contract using the following:
You can use 'GSNBouncerBase' as an example to guide your Bouncer implementation.
The only thing you must do is to extend from `GSNRecipient` and implement the accept method.
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`.

@ -1,50 +1,13 @@
= The Gas Station Network
= Writing GSN-capable contracts
*https://gsn.openzeppelin.com[Website]*
The https://gsn.ethereum.org[Gas Station Network] allows you to build apps where you pay for your users transactions, so they do not need to hold Ether to pay for gas, easing their onboarding process. In this guide, we will learn how to write smart contracts that can receive transactions from the GSN, by using OpenZeppelin Contracts.
User onboarding is one of the hottest topics in Ethereum. UI/UX along with scalability have been identified as the main problems that prevent adoption. Meta-transactions are a key component to improve the user experience.
Here you'll learn all about the Gas Station Network (GSN) and how to write contracts that don't require their users to hold Ether to pay for gas.
If you're new to the GSN, you probably want to first take a look at the xref:ROOT:gsn/what-is-the-gsn.adoc[light overview of the system], to get a clearer picture of how gasless transactions are achieved. Otherwise, strap in!
If you're already up to speed with the workings of the GSN, feel free to skip to <<Receiving a relayed call>>, where we'll go over how to use the OpenZeppelin Contracts to easily write a GSNRecipient contract. Otherwise, strap in!
WARNING: This feature is under development, and will be released in the next version of `@openzeppelin/contracts`.
== Sending gas-less transactions
All Ethereum transactions use gas, and the sender of each transaction must have enough Ether to pay for the gas spent. Even though these gas costs are low for basic transactions (a couple of cents), getting this Ether is no easy task: dApp users often need to go through Know Your Customer and Anti Money-Laundering processes (KYC & AML), which not only takes time but often involves sending a selfie holding their passport over the Internet (!).
On top of that, they also need to provide financial information to be able to purchase Ether through an exchange.
Only the most hardcore users will put up with this hassle, and dApp adoption greatly suffers when Ether is required. We can do better.
**Enter meta-transactions**. This is a fancy name for a simple idea: a third-party can send another user's transactions and pay themselves for the gas cost. That's it! There's some tricky technical details, but those can be safely ignored when interacting with the GSN. This way, instead of your users calling into your contract (called the _recipient_) directly, someone else (we'll call them a _relayer_) will send their transaction and pay for the cost.
But why would they do such a thing?
== Incentives
Relayers are not running a charity: they're running a business. The reason why they'll gladly pay for your users' gas costs is because they will in turn charge your contract, the recipient. That way relayers get their money back, plus a bit extra as a _fee_ for their services.
This may sound strange at first, but paying for user onboarding is a very common business practice. Lots of money is spent on advertising, free trials, new user discounts, etc., all with the https://en.wikipedia.org/wiki/Customer_acquisition_cost[goal of user acquisition]. Compared to those, the cost of a couple of Ethereum transactions is actually very small.
Additionally, you can leverage the GSN in scenarios where your users pay you off-chain in advance (e.g. via credit card), with each GSN-call deducting from their balance on your system. The possibilities are endless!
=== Should I trust these relayers?
You don't need to! The GSN is set up in such a way where it's in the relayers' best interest to serve your requests, and there are measures in place to penalize them if they misbehave. All of this happens automatically, so you can safely start using their services worry-free.
== One contract to coordinate them all
There are many meta-transaction implementations out there, but the GSN has a unique detail that makes it special. Inside its core, a smart contract is responsible for keeping track of relayers, handling relayed transactions, charging their recipients, and generally ensuring all parties stay honest. This contract is called RelayHub, and there is a _single_ instance of it in the whole network (you don't need to deploy your own!). Think of it as a piece of public infrastructure, for all Ethereum users to benefit from.
One of RelayHub's jobs is to act as a, well, _hub_ for all relayers: they will advertise their services on this contract, and your users will query it to find the relayer that best suits their purposes. This is out of scope for this guide however, and is not something you need to worry about when writing a recipient contract. If you want to learn more about sending transactions via relayers, head to our https://github.com/OpenZeppelin/openzeppelin-gsn-provider[GSNProvider guide].
The other key task RelayHub carries out is the actual _relaying of transactions_, the sole purpose behind this whole system. Instead of calling a function in your contract directly, your users will request a relayer to do it for them, who will then execute RelayHub's relayCall function. RelayHub will verify that the transaction is legitimate (protecting both users and recipients from dishonest relayers), and then call into your contract as originally requested by your user. This requires your recipient trusting RelayHub to do the right thing, but since it is a smart contract, this is as simple as reading its source code!
NOTE: The RelayHub address will be the same in each network. Right now, the latest relay hub is live with this address `0xD216153c06E857cD7f72665E0aF1d7D82172F494`.
NOTE: This feature is being released in the next version of `@openzeppelin/contracts`, available right now through `npm install @openzeppelin/contracts@next`. It is also available on the `@openzeppelin/contracts-ethereum-package` variant.
== Receiving a relayed call
We've mentioned how the RelayHub, and not your user, the one that actually ends up calling a function in your contract. We will refer to this as the _relayed call_. OpenZeppelin Contracts includes a number of utilities to make receiving relayed calls as easy as developing a regular Solidity contract, without needing to worry about the low level details.
The first step to writing a recipient is to inherit from our GSNRecipient contract. If you're also inheriting from other OpenZeppelin contracts, such as ERC20 or Crowdsale, this will work just fine: adding GSNRecipient to all of your token or crowdsale functions will make them GSN-callable.
```solidity
@ -124,17 +87,6 @@ function postRelayedCall(
These functions allow you to implement, for instance, a flow where you charge your users for the relayed transactions in a custom token. You can lock some of their tokens in `pre`, and execute the actual charge in `post`. This is similar to how gas fees work in Ethereum: the network first locks enough ETH to pay for the transaction's gas limit at its gas price, and then pays for what it actually spent.
== Payment
By now you may be wondering how exactly relayers charge their recipients for gas costs and service fees. The answer is simple: each recipient must have funds deposited on RelayHub in advance, and payment is automatically handled on each relayed call.
You can head to the https://gsn.openzeppelin.com/recipients[GSN Recipient Tool] to check and top-up your contracts' balance, view previous charges, or do all of this programatically by calling `IRelayHub.depositFor` and `IRelayHub.balanceOf`.
Recipients may withdraw their balance from the system at any point, but remember that they will not be able to receive any further relayed calls!
== Further reading
* The https://medium.com/@rrecuero/eth-onboarding-solution-90607fb81380[GSN announcement post] provides a good *overview of the system*, along with some use cases to take inspiration from.
* If you want to learn how to use *OpenZeppelin Contract's pre-made accept and charge strategies*, head to the xref:gsn_advanced.adoc[Advanced GSN Guide].
* If instead you wish to know more about how to *use GSN from your application*, head to the https://github.com/OpenZeppelin/openzeppelin-gsn-provider[OpenZeppelin GSN provider guides].
* For information on how to *test GSN-enabled contracts*, go to the https://github.com/OpenZeppelin/openzeppelin-gsn-helpers[OpenZeppelin test helpers documentation].
Read our xref:gsn-advanced.adoc[guide on the payment strategies] (called _bouncers_) pre-built and shipped in OpenZeppelin Contracts, or check out xref:api:GSN.adoc[the API reference of the GSN base contracts].

@ -1,3 +1,3 @@
[build]
command = "npm run docsite build"
command = "npm run docs build"
publish = "openzeppelin-docs/build/site"

@ -13,7 +13,7 @@
"compile": "scripts/compile.sh",
"console": "truffle console",
"coverage": "scripts/coverage.sh",
"docsite": "scripts/docsite.sh",
"docs": "scripts/docs.sh",
"prepare-docs": "scripts/prepare-docs.sh",
"lint": "npm run lint:js && npm run lint:sol",
"lint:fix": "npm run lint:js:fix",

@ -1,7 +1,7 @@
#!/usr/bin/env bash
if [ "$1" != "build" -a "$1" != "start" ];then
echo "usage: npm run docsite (build|start)" >&2
echo "usage: npm run docs (build|start)" >&2
exit 1
fi

@ -7,7 +7,8 @@ const fs = require('fs');
const cp = require('child_process');
const pkg = require('../../package.json');
if (pkg.version.indexOf('-rc') !== -1) {
const suffix = process.env.PRERELEASE_SUFFIX || 'rc';
if (pkg.version.indexOf('-' + suffix) !== -1) {
process.exit(0);
}

@ -14,6 +14,18 @@ contract('ECDSA', function ([_, other]) {
this.ecdsa = await ECDSAMock.new();
});
context('recover with invalid signature', function () {
it('with short signature', async function () {
expect(await this.ecdsa.recover(TEST_MESSAGE, '0x1234')).to.equal(ZERO_ADDRESS);
});
it('with long signature', async function () {
// eslint-disable-next-line max-len
expect(await this.ecdsa.recover(TEST_MESSAGE, '0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'))
.to.equal(ZERO_ADDRESS);
});
});
context('recover with valid signature', function () {
context('with v0 signature', function () {
// Signature generated outside ganache with method web3.eth.sign(signer, message)

@ -1,223 +0,0 @@
const { expectRevert } = require('openzeppelin-test-helpers');
const { getSignFor } = require('../helpers/sign');
const { shouldBehaveLikePublicRole } = require('../behaviors/access/roles/PublicRole.behavior');
const { expect } = require('chai');
const SignatureBouncerMock = artifacts.require('SignatureBouncerMock');
const UINT_VALUE = 23;
const BYTES_VALUE = web3.utils.toHex('test');
const INVALID_SIGNATURE = '0xabcd';
contract('SignatureBouncer', function ([_, signer, otherSigner, other, authorizedUser, ...otherAccounts]) {
beforeEach(async function () {
this.sigBouncer = await SignatureBouncerMock.new({ from: signer });
this.signFor = getSignFor(this.sigBouncer, signer);
});
describe('signer role', function () {
beforeEach(async function () {
this.contract = this.sigBouncer;
await this.contract.addSigner(otherSigner, { from: signer });
});
shouldBehaveLikePublicRole(signer, otherSigner, otherAccounts, 'signer');
});
describe('modifiers', function () {
context('plain signature', function () {
it('allows valid signature for sender', async function () {
await this.sigBouncer.onlyWithValidSignature(await this.signFor(authorizedUser), { from: authorizedUser });
});
it('does not allow invalid signature for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignature(INVALID_SIGNATURE, { from: authorizedUser }),
'SignatureBouncer: invalid signature for caller'
);
});
it('does not allow valid signature for other sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignature(await this.signFor(authorizedUser), { from: other }),
'SignatureBouncer: invalid signature for caller'
);
});
it('does not allow valid signature for method for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignature(await this.signFor(authorizedUser, 'onlyWithValidSignature'),
{ from: authorizedUser }), 'SignatureBouncer: invalid signature for caller'
);
});
});
context('method signature', function () {
it('allows valid signature with correct method for sender', async function () {
await this.sigBouncer.onlyWithValidSignatureAndMethod(
await this.signFor(authorizedUser, 'onlyWithValidSignatureAndMethod'), { from: authorizedUser }
);
});
it('does not allow invalid signature with correct method for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndMethod(INVALID_SIGNATURE, { from: authorizedUser }),
'SignatureBouncer: invalid signature for caller and method'
);
});
it('does not allow valid signature with correct method for other sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndMethod(
await this.signFor(authorizedUser, 'onlyWithValidSignatureAndMethod'), { from: other }
),
'SignatureBouncer: invalid signature for caller and method'
);
});
it('does not allow valid method signature with incorrect method for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndMethod(await this.signFor(authorizedUser, 'theWrongMethod'),
{ from: authorizedUser }), 'SignatureBouncer: invalid signature for caller and method'
);
});
it('does not allow valid non-method signature method for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndMethod(await this.signFor(authorizedUser), { from: authorizedUser }),
'SignatureBouncer: invalid signature for caller and method'
);
});
});
context('method and data signature', function () {
it('allows valid signature with correct method and data for sender', async function () {
await this.sigBouncer.onlyWithValidSignatureAndData(UINT_VALUE,
await this.signFor(authorizedUser, 'onlyWithValidSignatureAndData', [UINT_VALUE]), { from: authorizedUser }
);
});
it('does not allow invalid signature with correct method and data for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndData(UINT_VALUE, INVALID_SIGNATURE, { from: authorizedUser }),
'SignatureBouncer: invalid signature for caller and data'
);
});
it('does not allow valid signature with correct method and incorrect data for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndData(UINT_VALUE + 10,
await this.signFor(authorizedUser, 'onlyWithValidSignatureAndData', [UINT_VALUE]),
{ from: authorizedUser }
), 'SignatureBouncer: invalid signature for caller and data'
);
});
it('does not allow valid signature with correct method and data for other sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndData(UINT_VALUE,
await this.signFor(authorizedUser, 'onlyWithValidSignatureAndData', [UINT_VALUE]),
{ from: other }
), 'SignatureBouncer: invalid signature for caller and data'
);
});
it('does not allow valid non-method signature for sender', async function () {
await expectRevert(
this.sigBouncer.onlyWithValidSignatureAndData(UINT_VALUE,
await this.signFor(authorizedUser), { from: authorizedUser }
), 'SignatureBouncer: invalid signature for caller and data'
);
});
it('does not allow msg.data shorter than SIGNATURE_SIZE', async function () {
await expectRevert(
this.sigBouncer.tooShortMsgData({ from: authorizedUser }), 'SignatureBouncer: data is too short'
);
});
});
});
context('signature validation', function () {
context('plain signature', function () {
it('validates valid signature for valid user', async function () {
expect(await this.sigBouncer.checkValidSignature(authorizedUser, await this.signFor(authorizedUser)))
.to.equal(true);
});
it('does not validate invalid signature for valid user', async function () {
expect(await this.sigBouncer.checkValidSignature(authorizedUser, INVALID_SIGNATURE)).to.equal(false);
});
it('does not validate valid signature for anyone', async function () {
expect(await this.sigBouncer.checkValidSignature(other, await this.signFor(authorizedUser))).to.equal(false);
});
it('does not validate valid signature for method for valid user', async function () {
expect(await this.sigBouncer.checkValidSignature(
authorizedUser, await this.signFor(authorizedUser, 'checkValidSignature'))
).to.equal(false);
});
});
context('method signature', function () {
it('validates valid signature with correct method for valid user', async function () {
expect(await this.sigBouncer.checkValidSignatureAndMethod(authorizedUser,
await this.signFor(authorizedUser, 'checkValidSignatureAndMethod'))
).to.equal(true);
});
it('does not validate invalid signature with correct method for valid user', async function () {
expect(await this.sigBouncer.checkValidSignatureAndMethod(authorizedUser, INVALID_SIGNATURE)).to.equal(false);
});
it('does not validate valid signature with correct method for anyone', async function () {
expect(await this.sigBouncer.checkValidSignatureAndMethod(other,
await this.signFor(authorizedUser, 'checkValidSignatureAndMethod'))
).to.equal(false);
});
it('does not validate valid non-method signature with correct method for valid user', async function () {
expect(await this.sigBouncer.checkValidSignatureAndMethod(authorizedUser, await this.signFor(authorizedUser))
).to.equal(false);
});
});
context('method and data signature', function () {
it('validates valid signature with correct method and data for valid user', async function () {
expect(await this.sigBouncer.checkValidSignatureAndData(authorizedUser, BYTES_VALUE, UINT_VALUE,
await this.signFor(authorizedUser, 'checkValidSignatureAndData', [authorizedUser, BYTES_VALUE, UINT_VALUE]))
).to.equal(true);
});
it('does not validate invalid signature with correct method and data for valid user', async function () {
expect(
await this.sigBouncer.checkValidSignatureAndData(authorizedUser, BYTES_VALUE, UINT_VALUE, INVALID_SIGNATURE)
).to.equal(false);
});
it('does not validate valid signature with correct method and incorrect data for valid user',
async function () {
expect(await this.sigBouncer.checkValidSignatureAndData(authorizedUser, BYTES_VALUE, UINT_VALUE + 10,
await this.signFor(authorizedUser, 'checkValidSignatureAndData', [authorizedUser, BYTES_VALUE, UINT_VALUE]))
).to.equal(false);
}
);
it('does not validate valid signature with correct method and data for anyone', async function () {
expect(await this.sigBouncer.checkValidSignatureAndData(other, BYTES_VALUE, UINT_VALUE,
await this.signFor(authorizedUser, 'checkValidSignatureAndData', [authorizedUser, BYTES_VALUE, UINT_VALUE]))
).that.equal(false);
});
it('does not validate valid non-method-data signature with correct method and data for valid user',
async function () {
expect(await this.sigBouncer.checkValidSignatureAndData(authorizedUser, BYTES_VALUE, UINT_VALUE,
await this.signFor(authorizedUser, 'checkValidSignatureAndData'))
).to.equal(false);
}
);
});
});
});
Loading…
Cancel
Save