Bladeren bron

Initial GSN support (beta) (#1844)

* Add base Context contract

* Add GSNContext and tests

* Add RelayHub deployment to tests

* Add RelayProvider integration, complete GSNContext tests

* Switch dependency to openzeppelin-gsn-provider

* Add default txfee to provider

* Add basic signing recipient

* Sign more values

* Add comment clarifying RelayHub's msg.data

* Make context constructors internal

* Rename SigningRecipient to GSNRecipientSignedData

* Add ERC20Charge recipients

* Harcode RelayHub address into GSNContext

* Fix Solidity linter errors

* Run server from binary, use gsn-helpers to fund it

* Migrate to published @openzeppelin/gsn-helpers

* Silence false-positive compiler warning

* Use GSN helper assertions

* Rename meta-tx to gsn, take out of drafts

* Merge ERC20 charge recipients into a single one

* Rename GSNRecipients to Bouncers

* Add GSNBouncerUtils to decouple the bouncers from GSNRecipient

* Add _upgradeRelayHub

* Store RelayHub address using unstructored storage

* Add IRelayHub

* Add _withdrawDeposits to GSNRecipient

* Add relayHub version to recipient

* Make _acceptRelayedCall and _declineRelayedCall easier to use

* Rename GSNBouncerUtils to GSNBouncerBase, make it IRelayRecipient

* Improve GSNBouncerBase, make pre and post sender-protected and optional

* Fix GSNBouncerERC20Fee, add tests

* Add missing GSNBouncerSignature test

* Override transferFrom in __unstable__ERC20PrimaryAdmin

* Fix gsn dependencies in package.json

* Rhub address slot reduced by 1

* Rename relay hub changed event

* Use released gsn-provider

* Run relayer with short sleep of 1s instead of 100ms

* update package-lock.json

* clear circle cache

* use optimized gsn-provider

* update to latest @openzeppelin/gsn-provider

* replace with gsn dev provider

* remove relay server

* rename arguments in approveFunction

* fix GSNBouncerSignature test

* change gsn txfee

* initialize development provider only once

* update RelayHub interface

* adapt to new IRelayHub.withdraw

* update @openzeppelin/gsn-helpers

* update relayhub singleton address

* fix helper name

* set up gsn provider for coverage too

* lint

* Revert "set up gsn provider for coverage too"

This reverts commit 8a7b5be5f942002710cba148f249cb888ee8e373.

* remove unused code

* add gsn provider to coverage

* move truffle contract options back out

* increase gas limit for coverage

* remove unreachable code

* add more gas for GSNContext test

* fix test suite name

* rename GSNBouncerBase internal API

* remove onlyRelayHub modifier

* add explicit inheritance

* remove redundant event

* update name of bouncers error codes enums

* add basic docs page for gsn contracts

* make gsn directory all caps

* add changelog entry

* lint

* enable test run to fail in coverage
Nicolás Venturo 6 jaren geleden
bovenliggende
commit
0ec1d761aa

+ 2 - 1
CHANGELOG.md

@@ -4,10 +4,11 @@
 
 ### New features:
  * `Address.toPayable`: added a helper to convert between address types without having to resort to low-level casting. ([#1773](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1773))
+ * Facilities to make metatransaction-enabled contracts through the Gas Station Network. ([#1844](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1844))
 
 ### Improvements:
  * `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)) 
+ * `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
 

+ 27 - 0
contracts/GSN/Context.sol

@@ -0,0 +1,27 @@
+pragma solidity ^0.5.0;
+
+/*
+ * @dev Provides information about the current execution context, including the
+ * sender of the transaction and its data. While these are generally available
+ * via msg.sender and msg.data, they not should not be accessed in such a direct
+ * manner, since when dealing with GSN meta-transactions the account sending and
+ * paying for execution may not be the actual sender (as far as an application
+ * is concerned).
+ *
+ * This contract is only required for intermediate, library-like contracts.
+ */
+contract Context {
+    // Empty internal constructor, to prevent people from mistakenly deploying
+    // an instance of this contract, with should be used via inheritance.
+    constructor () internal { }
+    // solhint-disable-previous-line no-empty-blocks
+
+    function _msgSender() internal view returns (address) {
+        return msg.sender;
+    }
+
+    function _msgData() internal view returns (bytes memory) {
+        this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
+        return msg.data;
+    }
+}

+ 102 - 0
contracts/GSN/GSNContext.sol

@@ -0,0 +1,102 @@
+pragma solidity ^0.5.0;
+
+import "./Context.sol";
+
+/*
+ * @dev Enables GSN support on `Context` contracts by recognizing calls from
+ * RelayHub and extracting the actual sender and call data from the received
+ * calldata.
+ *
+ * > This contract does not perform all required tasks to implement a GSN
+ * 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;
+
+    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)
+        }
+    }
+
+    function _upgradeRelayHub(address newRelayHub) internal {
+        address currentRelayHub = _getRelayHub();
+        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)
+        }
+    }
+
+    // Overrides for Context's functions: when called from RelayHub, sender and
+    // data require some pre-processing: the actual sender is stored at the end
+    // of the call data, which in turns means it needs to be removed from it
+    // when handling said data.
+
+    function _msgSender() internal view returns (address) {
+        if (msg.sender != _getRelayHub()) {
+            return msg.sender;
+        } else {
+            return _getRelayedCallSender();
+        }
+    }
+
+    function _msgData() internal view returns (bytes memory) {
+        if (msg.sender != _getRelayHub()) {
+            return msg.data;
+        } else {
+            return _getRelayedCallData();
+        }
+    }
+
+    function _getRelayedCallSender() private pure returns (address 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
+        // so would leave the address in the upper 20 bytes of the 32-byte word, which is inconvenient and would
+        // require bit shifting. We therefore subtract 12 from the read index so the address lands on the lower 20
+        // bytes. This can always be done due to the 32-byte prefix.
+
+        // The final memory read index is msg.data.length - 20 + 32 - 12 = msg.data.length. Using inline assembly is the
+        // easiest/most-efficient way to perform this operation.
+
+        // These fields are not accessible from assembly
+        bytes memory array = msg.data;
+        uint256 index = msg.data.length;
+
+        // solhint-disable-next-line no-inline-assembly
+        assembly {
+            // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those.
+            result := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff)
+        }
+        return result;
+    }
+
+    function _getRelayedCallData() private pure returns (bytes memory) {
+        // RelayHub appends the sender address at the end of the calldata, so in order to retrieve the actual msg.data,
+        // we must strip the last 20 bytes (length of an address type) from it.
+
+        uint256 actualDataLength = msg.data.length - 20;
+        bytes memory actualData = new bytes(actualDataLength);
+
+        for (uint256 i = 0; i < actualDataLength; ++i) {
+            actualData[i] = msg.data[i];
+        }
+
+        return actualData;
+    }
+}

+ 28 - 0
contracts/GSN/GSNRecipient.sol

@@ -0,0 +1,28 @@
+pragma solidity ^0.5.0;
+
+import "./IRelayRecipient.sol";
+import "./GSNContext.sol";
+import "./bouncers/GSNBouncerBase.sol";
+import "./IRelayHub.sol";
+
+/*
+ * @dev Base GSN recipient contract, adding the recipient interface and enabling
+ * GSN support. Not all interface methods are implemented, derived contracts
+ * must do so themselves.
+ */
+contract GSNRecipient is IRelayRecipient, GSNContext, GSNBouncerBase {
+    function getHubAddr() public view returns (address) {
+        return _getRelayHub();
+    }
+
+    // This function is view for future-proofing, it may require reading from
+    // storage in the future.
+    function relayHubVersion() public view returns (string memory) {
+        this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
+        return "1.0.0";
+    }
+
+    function _withdrawDeposits(uint256 amount, address payable payee) internal {
+        IRelayHub(_getRelayHub()).withdraw(amount, payee);
+    }
+}

+ 188 - 0
contracts/GSN/IRelayHub.sol

@@ -0,0 +1,188 @@
+pragma solidity ^0.5.0;
+
+contract IRelayHub {
+    // Relay management
+
+    // Add stake to a relay and sets its unstakeDelay.
+    // If the relay does not exist, it is created, and the caller
+    // of this function becomes its owner. If the relay already exists, only the owner can call this function. A relay
+    // cannot be its own owner.
+    // All Ether in this function call will be added to the relay's stake.
+    // Its unstake delay will be assigned to unstakeDelay, but the new value must be greater or equal to the current one.
+    // Emits a Staked event.
+    function stake(address relayaddr, uint256 unstakeDelay) external payable;
+
+    // Emited when a relay's stake or unstakeDelay are increased
+    event Staked(address indexed relay, uint256 stake, uint256 unstakeDelay);
+
+    // Registers the caller as a relay.
+    // The relay must be staked for, and not be a contract (i.e. this function must be called directly from an EOA).
+    // Emits a RelayAdded event.
+    // This function can be called multiple times, emitting new RelayAdded events. Note that the received transactionFee
+    // is not enforced by relayCall.
+    function registerRelay(uint256 transactionFee, string memory url) public;
+
+    // Emitted when a relay is registered or re-registerd. Looking at these events (and filtering out RelayRemoved
+    // events) lets a client discover the list of available relays.
+    event RelayAdded(address indexed relay, address indexed owner, uint256 transactionFee, uint256 stake, uint256 unstakeDelay, string url);
+
+    // Removes (deregisters) a relay. Unregistered (but staked for) relays can also be removed. Can only be called by
+    // the owner of the relay. After the relay's unstakeDelay has elapsed, unstake will be callable.
+    // Emits a RelayRemoved event.
+    function removeRelayByOwner(address relay) public;
+
+    // Emitted when a relay is removed (deregistered). unstakeTime is the time when unstake will be callable.
+    event RelayRemoved(address indexed relay, uint256 unstakeTime);
+
+    // Deletes the relay from the system, and gives back its stake to the owner. Can only be called by the relay owner,
+    // after unstakeDelay has elapsed since removeRelayByOwner was called.
+    // Emits an Unstaked event.
+    function unstake(address relay) public;
+
+    // Emitted when a relay is unstaked for, including the returned stake.
+    event Unstaked(address indexed relay, uint256 stake);
+
+    // States a relay can be in
+    enum RelayState {
+        Unknown, // The relay is unknown to the system: it has never been staked for
+        Staked, // The relay has been staked for, but it is not yet active
+        Registered, // The relay has registered itself, and is active (can relay calls)
+        Removed    // The relay has been removed by its owner and can no longer relay calls. It must wait for its unstakeDelay to elapse before it can unstake
+    }
+
+    // Returns a relay's status. Note that relays can be deleted when unstaked or penalized.
+    function getRelay(address relay) external view returns (uint256 totalStake, uint256 unstakeDelay, uint256 unstakeTime, address payable owner, RelayState state);
+
+    // Balance management
+
+    // Deposits ether for a contract, so that it can receive (and pay for) relayed transactions. Unused balance can only
+    // be withdrawn by the contract itself, by callingn withdraw.
+    // Emits a Deposited event.
+    function depositFor(address target) public payable;
+
+    // Emitted when depositFor is called, including the amount and account that was funded.
+    event Deposited(address indexed recipient, address indexed from, uint256 amount);
+
+    // Returns an account's deposits. These can be either a contnract's funds, or a relay owner's revenue.
+    function balanceOf(address target) external view returns (uint256);
+
+    // Withdraws from an account's balance, sending it back to it. Relay owners call this to retrieve their revenue, and
+    // contracts can also use it to reduce their funding.
+    // Emits a Withdrawn event.
+    function withdraw(uint256 amount, address payable dest) public;
+
+    // Emitted when an account withdraws funds from RelayHub.
+    event Withdrawn(address indexed account, address indexed dest, uint256 amount);
+
+    // Relaying
+
+    // Check if the RelayHub will accept a relayed operation. Multiple things must be true for this to happen:
+    //  - all arguments must be signed for by the sender (from)
+    //  - the sender's nonce must be the current one
+    //  - the recipient must accept this transaction (via acceptRelayedCall)
+    // Returns a PreconditionCheck value (OK when the transaction can be relayed), or a recipient-specific error code if
+    // it returns one in acceptRelayedCall.
+    function canRelay(
+        address relay,
+        address from,
+        address to,
+        bytes memory encodedFunction,
+        uint256 transactionFee,
+        uint256 gasPrice,
+        uint256 gasLimit,
+        uint256 nonce,
+        bytes memory signature,
+        bytes memory approvalData
+    ) public view returns (uint256 status, bytes memory recipientContext);
+
+    // Preconditions for relaying, checked by canRelay and returned as the corresponding numeric values.
+    enum PreconditionCheck {
+        OK,                         // All checks passed, the call can be relayed
+        WrongSignature,             // The transaction to relay is not signed by requested sender
+        WrongNonce,                 // The provided nonce has already been used by the sender
+        AcceptRelayedCallReverted,  // The recipient rejected this call via acceptRelayedCall
+        InvalidRecipientStatusCode  // The recipient returned an invalid (reserved) status code
+    }
+
+    // Relays a transaction. For this to suceed, multiple conditions must be met:
+    //  - canRelay must return PreconditionCheck.OK
+    //  - the sender must be a registered relay
+    //  - the transaction's gas price must be larger or equal to the one that was requested by the sender
+    //  - the transaction must have enough gas to not run out of gas if all internal transactions (calls to the
+    // recipient) use all gas available to them
+    //  - the recipient must have enough balance to pay the relay for the worst-case scenario (i.e. when all gas is
+    // spent)
+    //
+    // If all conditions are met, the call will be relayed and the recipient charged. preRelayedCall, the encoded
+    // function and postRelayedCall will be called in order.
+    //
+    // Arguments:
+    //  - from: the client originating the request
+    //  - recipient: the target IRelayRecipient contract
+    //  - encodedFunction: the function call to relay, including data
+    //  - transactionFee: fee (%) the relay takes over actual gas cost
+    //  - gasPrice: gas price the client is willing to pay
+    //  - gasLimit: gas to forward when calling the encoded function
+    //  - nonce: client's nonce
+    //  - signature: client's signature over all previous params, plus the relay and RelayHub addresses
+    //  - approvalData: dapp-specific data forwared to acceptRelayedCall. This value is *not* verified by the Hub, but
+    //    it still can be used for e.g. a signature.
+    //
+    // Emits a TransactionRelayed event.
+    function relayCall(
+        address from,
+        address to,
+        bytes memory encodedFunction,
+        uint256 transactionFee,
+        uint256 gasPrice,
+        uint256 gasLimit,
+        uint256 nonce,
+        bytes memory signature,
+        bytes memory approvalData
+    ) public;
+
+    // Emitted when an attempt to relay a call failed. This can happen due to incorrect relayCall arguments, or the
+    // recipient not accepting the relayed call. The actual relayed call was not executed, and the recipient not charged.
+    // The reason field contains an error code: values 1-10 correspond to PreconditionCheck entries, and values over 10
+    // are custom recipient error codes returned from acceptRelayedCall.
+    event CanRelayFailed(address indexed relay, address indexed from, address indexed to, bytes4 selector, uint256 reason);
+
+    // Emitted when a transaction is relayed. Note that the actual encoded function might be reverted: this will be
+    // indicated in the status field.
+    // Useful when monitoring a relay's operation and relayed calls to a contract.
+    // Charge is the ether value deducted from the recipient's balance, paid to the relay's owner.
+    event TransactionRelayed(address indexed relay, address indexed from, address indexed to, bytes4 selector, RelayCallStatus status, uint256 charge);
+
+    // Reason error codes for the TransactionRelayed event
+    enum RelayCallStatus {
+        OK,                      // The transaction was successfully relayed and execution successful - never included in the event
+        RelayedCallFailed,       // The transaction was relayed, but the relayed call failed
+        PreRelayedFailed,        // The transaction was not relayed due to preRelatedCall reverting
+        PostRelayedFailed,       // The transaction was relayed and reverted due to postRelatedCall reverting
+        RecipientBalanceChanged  // The transaction was relayed and reverted due to the recipient's balance changing
+    }
+
+    // Returns how much gas should be forwarded to a call to relayCall, in order to relay a transaction that will spend
+    // up to relayedCallStipend gas.
+    function requiredGas(uint256 relayedCallStipend) public view returns (uint256);
+
+    // Returns the maximum recipient charge, given the amount of gas forwarded, gas price and relay fee.
+    function maxPossibleCharge(uint256 relayedCallStipend, uint256 gasPrice, uint256 transactionFee) public view returns (uint256);
+
+    // Relay penalization. Any account can penalize relays, removing them from the system immediately, and rewarding the
+    // reporter with half of the relay's stake. The other half is burned so that, even if the relay penalizes itself, it
+    // still loses half of its stake.
+
+    // Penalize a relay that signed two transactions using the same nonce (making only the first one valid) and
+    // different data (gas price, gas limit, etc. may be different). The (unsigned) transaction data and signature for
+    // both transactions must be provided.
+    function penalizeRepeatedNonce(bytes memory unsignedTx1, bytes memory signature1, bytes memory unsignedTx2, bytes memory signature2) public;
+
+    // Penalize a relay that sent a transaction that didn't target RelayHub's registerRelay or relayCall.
+    function penalizeIllegalTransaction(bytes memory unsignedTx, bytes memory signature) public;
+
+    event Penalized(address indexed relay, address sender, uint256 amount);
+
+    function getNonce(address from) external view returns (uint256);
+}
+

+ 30 - 0
contracts/GSN/IRelayRecipient.sol

@@ -0,0 +1,30 @@
+pragma solidity ^0.5.0;
+
+/*
+ * @dev Interface for a contract that will be called via the GSN from RelayHub.
+ */
+contract IRelayRecipient {
+    /**
+     * @dev Returns the address of the RelayHub instance this recipient interacts with.
+     */
+    function getHubAddr() public view returns (address);
+
+    function acceptRelayedCall(
+        address relay,
+        address from,
+        bytes calldata encodedFunction,
+        uint256 transactionFee,
+        uint256 gasPrice,
+        uint256 gasLimit,
+        uint256 nonce,
+        bytes calldata approvalData,
+        uint256 maxPossibleCharge
+    )
+        external
+        view
+        returns (uint256, bytes memory);
+
+    function preRelayedCall(bytes calldata context) external returns (bytes32);
+
+    function postRelayedCall(bytes calldata context, bool success, uint actualCharge, bytes32 preRetVal) external;
+}

+ 10 - 0
contracts/GSN/README.adoc

@@ -0,0 +1,10 @@
+= GSN
+
+== Recipient
+
+{{GSNRecipient}}
+
+== Bouncers
+
+{{GSNBouncerERC20Fee}}
+{{GSNBouncerSignature}}

+ 92 - 0
contracts/GSN/bouncers/GSNBouncerBase.sol

@@ -0,0 +1,92 @@
+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;
+    }
+}

+ 121 - 0
contracts/GSN/bouncers/GSNBouncerERC20Fee.sol

@@ -0,0 +1,121 @@
+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";
+
+contract GSNBouncerERC20Fee is GSNBouncerBase {
+    using SafeERC20 for __unstable__ERC20PrimaryAdmin;
+    using SafeMath for uint256;
+
+    enum GSNBouncerERC20FeeErrorCodes {
+        INSUFFICIENT_BALANCE
+    }
+
+    __unstable__ERC20PrimaryAdmin private _token;
+
+    constructor(string memory name, string memory symbol, uint8 decimals) public {
+        _token = new __unstable__ERC20PrimaryAdmin(name, symbol, decimals);
+    }
+
+    function token() public view returns (IERC20) {
+        return IERC20(_token);
+    }
+
+    function _mint(address account, uint256 amount) internal {
+        _token.mint(account, amount);
+    }
+
+    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));
+        }
+
+        return _approveRelayedCall(abi.encode(from, maxPossibleCharge, transactionFee, gasPrice));
+    }
+
+    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 _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));
+    }
+}
+
+/**
+ * @title __unstable__ERC20PrimaryAdmin
+ * @dev An ERC20 token owned by another contract, which has minting permissions and can use transferFrom to receive
+ * anyone's tokens. This contract is an internal helper for GSNRecipientERC20Fee, and should not be used
+ * outside of this context.
+ */
+// solhint-disable-next-line contract-name-camelcase
+contract __unstable__ERC20PrimaryAdmin is ERC20, ERC20Detailed, Secondary {
+    uint256 private constant UINT256_MAX = 2**256 - 1;
+
+    constructor(string memory name, string memory symbol, uint8 decimals) public ERC20Detailed(name, symbol, decimals) {
+        // solhint-disable-previous-line no-empty-blocks
+    }
+
+    // The primary account (GSNRecipientERC20Fee) can mint tokens
+    function mint(address account, uint256 amount) public onlyPrimary {
+        _mint(account, amount);
+    }
+
+    // The primary account has 'infinite' allowance for all token holders
+    function allowance(address owner, address spender) public view returns (uint256) {
+        if (spender == primary()) {
+            return UINT256_MAX;
+        } else {
+            return super.allowance(owner, spender);
+        }
+    }
+
+    // Allowance for the primary account cannot be changed (it is always 'infinite')
+    function _approve(address owner, address spender, uint256 value) internal {
+        if (spender == primary()) {
+            return;
+        } else {
+            super._approve(owner, spender, value);
+        }
+    }
+
+    function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
+        if (recipient == primary()) {
+            _transfer(sender, recipient, amount);
+            return true;
+        } else {
+            return super.transferFrom(sender, recipient, amount);
+        }
+    }
+}

+ 51 - 0
contracts/GSN/bouncers/GSNBouncerSignature.sol

@@ -0,0 +1,51 @@
+pragma solidity ^0.5.0;
+
+import "./GSNBouncerBase.sol";
+import "../../cryptography/ECDSA.sol";
+
+contract GSNBouncerSignature is GSNBouncerBase {
+    using ECDSA for bytes32;
+
+    address private _trustedSigner;
+
+    enum GSNBouncerSignatureErrorCodes {
+        INVALID_SIGNER
+    }
+
+    constructor(address trustedSigner) public {
+        _trustedSigner = trustedSigner;
+    }
+
+    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(GSNBouncerSignatureErrorCodes.INVALID_SIGNER));
+        }
+    }
+}

+ 27 - 0
contracts/mocks/ContextMock.sol

@@ -0,0 +1,27 @@
+pragma solidity ^0.5.0;
+
+import "../GSN/Context.sol";
+
+contract ContextMock is Context {
+    event Sender(address sender);
+
+    function msgSender() public {
+        emit Sender(_msgSender());
+    }
+
+    event Data(bytes data, uint256 integerValue, string stringValue);
+
+    function msgData(uint256 integerValue, string memory stringValue) public {
+        emit Data(_msgData(), integerValue, stringValue);
+    }
+}
+
+contract ContextMockCaller {
+    function callSender(ContextMock context) public {
+        context.msgSender();
+    }
+
+    function callData(ContextMock context, uint256 integerValue, string memory stringValue) public {
+        context.msgData(integerValue, stringValue);
+    }
+}

+ 20 - 0
contracts/mocks/GSNBouncerERC20FeeMock.sol

@@ -0,0 +1,20 @@
+pragma solidity ^0.5.0;
+
+import "../GSN/GSNRecipient.sol";
+import "../GSN/bouncers/GSNBouncerERC20Fee.sol";
+
+contract GSNBouncerERC20FeeMock is GSNRecipient, GSNBouncerERC20Fee {
+    constructor(string memory name, string memory symbol, uint8 decimals) public GSNBouncerERC20Fee(name, symbol, decimals) {
+        // solhint-disable-previous-line no-empty-blocks
+    }
+
+    function mint(address account, uint256 amount) public {
+        _mint(account, amount);
+    }
+
+    event MockFunctionCalled(uint256 senderBalance);
+
+    function mockFunction() public {
+        emit MockFunctionCalled(token().balanceOf(_msgSender()));
+    }
+}

+ 16 - 0
contracts/mocks/GSNBouncerSignatureMock.sol

@@ -0,0 +1,16 @@
+pragma solidity ^0.5.0;
+
+import "../GSN/GSNRecipient.sol";
+import "../GSN/bouncers/GSNBouncerSignature.sol";
+
+contract GSNBouncerSignatureMock is GSNRecipient, GSNBouncerSignature {
+    constructor(address trustedSigner) public GSNBouncerSignature(trustedSigner) {
+        // solhint-disable-previous-line no-empty-blocks
+    }
+
+    event MockFunctionCalled();
+
+    function mockFunction() public {
+        emit MockFunctionCalled();
+    }
+}

+ 46 - 0
contracts/mocks/GSNContextMock.sol

@@ -0,0 +1,46 @@
+pragma solidity ^0.5.0;
+
+import "./ContextMock.sol";
+import "../GSN/GSNContext.sol";
+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();
+    }
+
+    function acceptRelayedCall(
+        address,
+        address,
+        bytes calldata,
+        uint256,
+        uint256,
+        uint256,
+        uint256,
+        bytes calldata,
+        uint256
+    )
+        external
+        view
+        returns (uint256, bytes memory)
+    {
+        return (0, "");
+    }
+
+    function preRelayedCall(bytes calldata) external returns (bytes32) {
+        // solhint-disable-previous-line no-empty-blocks
+    }
+
+    function postRelayedCall(bytes calldata, bool, uint256, bytes32) external {
+        // solhint-disable-previous-line no-empty-blocks
+    }
+
+    function getRelayHub() public view returns (address) {
+        return _getRelayHub();
+    }
+
+    function upgradeRelayHub(address newRelayHub) public {
+        return _upgradeRelayHub(newRelayHub);
+    }
+}

+ 25 - 0
contracts/mocks/GSNRecipientMock.sol

@@ -0,0 +1,25 @@
+pragma solidity ^0.5.0;
+
+import "../GSN/GSNRecipient.sol";
+
+contract GSNRecipientMock is GSNRecipient {
+    function withdrawDeposits(uint256 amount, address payable payee) public {
+        _withdrawDeposits(amount, payee);
+    }
+
+    function acceptRelayedCall(address, address, bytes calldata, uint256, uint256, uint256, uint256, bytes calldata, uint256)
+        external
+        view
+        returns (uint256, bytes memory)
+    {
+        return (0, "");
+    }
+
+    function preRelayedCall(bytes calldata) external returns (bytes32) {
+        // solhint-disable-previous-line no-empty-blocks
+    }
+
+    function postRelayedCall(bytes calldata, bool, uint256, bytes32) external {
+        // solhint-disable-previous-line no-empty-blocks
+    }
+}

File diff suppressed because it is too large
+ 857 - 35
package-lock.json


+ 2 - 0
package.json

@@ -44,6 +44,8 @@
   },
   "homepage": "https://github.com/OpenZeppelin/openzeppelin-contracts",
   "devDependencies": {
+    "@openzeppelin/gsn-helpers": "^0.1.4",
+    "@openzeppelin/gsn-provider": "^0.1.4",
     "chai": "^4.2.0",
     "concurrently": "^4.1.0",
     "eslint": "^4.19.1",

+ 6 - 2
scripts/coverage.sh

@@ -1,8 +1,12 @@
 #!/usr/bin/env bash
 
-set -o errexit
+set -o errexit -o pipefail
 
-SOLIDITY_COVERAGE=true scripts/test.sh
+log() {
+  echo "$*" >&2
+}
+
+SOLIDITY_COVERAGE=true scripts/test.sh || log "Test run failed"
 
 if [ "$CI" = true ]; then
   curl -s https://codecov.io/bash | bash -s -- -C "$CIRCLE_SHA1"

+ 18 - 2
scripts/test.sh

@@ -23,9 +23,13 @@ ganache_running() {
   nc -z localhost "$ganache_port"
 }
 
+relayer_running() {
+  nc -z localhost "$relayer_port"
+}
+
 start_ganache() {
-  # We define 10 accounts with balance 1M ether, needed for high-value tests.
   local accounts=(
+    # 10 accounts with balance 1M ether, needed for high-value tests.
     --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000"
     --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000"
     --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000"
@@ -36,10 +40,14 @@ start_ganache() {
     --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000"
     --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000"
     --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000"
+    # 3 accounts to be used for GSN matters.
+    --account="0x956b91cb2344d7863ea89e6945b753ca32f6d74bb97a59e59e04903ded14ad00,1000000000000000000000000"
+    --account="0x956b91cb2344d7863ea89e6945b753ca32f6d74bb97a59e59e04903ded14ad01,1000000000000000000000000"
+    --account="0x956b91cb2344d7863ea89e6945b753ca32f6d74bb97a59e59e04903ded14ad02,1000000000000000000000000"
   )
 
   if [ "$SOLIDITY_COVERAGE" = true ]; then
-    npx ganache-cli-coverage --emitFreeLogs true --allowUnlimitedContractSize true --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null &
+    npx ganache-cli-coverage --emitFreeLogs true --allowUnlimitedContractSize true --gasLimit 0xfffffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null &
   else
     npx ganache-cli --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null &
   fi
@@ -55,6 +63,12 @@ start_ganache() {
   echo "Ganache launched!"
 }
 
+setup_relayhub() {
+  npx oz-gsn deploy-relay-hub \
+    --ethereumNodeURL "http://localhost:$ganache_port" \
+    --from "0xbb49ad04422f9fa6a217f3ed82261b942f6981f7"
+}
+
 if ganache_running; then
   echo "Using existing ganache instance"
 else
@@ -64,6 +78,8 @@ fi
 
 npx truffle version
 
+setup_relayhub
+
 if [ "$SOLIDITY_COVERAGE" = true ]; then
   npx solidity-coverage
 else

+ 42 - 0
test/GSN/Context.behavior.js

@@ -0,0 +1,42 @@
+const { BN, expectEvent } = require('openzeppelin-test-helpers');
+
+const ContextMock = artifacts.require('ContextMock');
+
+function shouldBehaveLikeRegularContext (sender) {
+  describe('msgSender', function () {
+    it('returns the transaction sender when called from an EOA', async function () {
+      const { logs } = await this.context.msgSender({ from: sender });
+      expectEvent.inLogs(logs, 'Sender', { sender });
+    });
+
+    it('returns the transaction sender when from another contract', async function () {
+      const { tx } = await this.caller.callSender(this.context.address, { from: sender });
+      await expectEvent.inTransaction(tx, ContextMock, 'Sender', { sender: this.caller.address });
+    });
+  });
+
+  describe('msgData', function () {
+    const integerValue = new BN('42');
+    const stringValue = 'OpenZeppelin';
+
+    let callData;
+
+    beforeEach(async function () {
+      callData = this.context.contract.methods.msgData(integerValue.toString(), stringValue).encodeABI();
+    });
+
+    it('returns the transaction data when called from an EOA', async function () {
+      const { logs } = await this.context.msgData(integerValue, stringValue);
+      expectEvent.inLogs(logs, 'Data', { data: callData, integerValue, stringValue });
+    });
+
+    it('returns the transaction sender when from another contract', async function () {
+      const { tx } = await this.caller.callData(this.context.address, integerValue, stringValue);
+      await expectEvent.inTransaction(tx, ContextMock, 'Data', { data: callData, integerValue, stringValue });
+    });
+  });
+}
+
+module.exports = {
+  shouldBehaveLikeRegularContext,
+};

+ 15 - 0
test/GSN/Context.test.js

@@ -0,0 +1,15 @@
+require('openzeppelin-test-helpers');
+
+const ContextMock = artifacts.require('ContextMock');
+const ContextMockCaller = artifacts.require('ContextMockCaller');
+
+const { shouldBehaveLikeRegularContext } = require('./Context.behavior');
+
+contract('Context', function ([_, sender]) {
+  beforeEach(async function () {
+    this.context = await ContextMock.new();
+    this.caller = await ContextMockCaller.new();
+  });
+
+  shouldBehaveLikeRegularContext(sender);
+});

+ 69 - 0
test/GSN/GSNBouncerERC20Fee.test.js

@@ -0,0 +1,69 @@
+const { BN, ether, expectEvent } = require('openzeppelin-test-helpers');
+const gsn = require('@openzeppelin/gsn-helpers');
+
+const { expect } = require('chai');
+
+const GSNBouncerERC20FeeMock = artifacts.require('GSNBouncerERC20FeeMock');
+const ERC20Detailed = artifacts.require('ERC20Detailed');
+const IRelayHub = artifacts.require('IRelayHub');
+
+contract('GSNBouncerERC20Fee', function ([_, sender, other]) {
+  const name = 'FeeToken';
+  const symbol = 'FTKN';
+  const decimals = new BN('18');
+
+  beforeEach(async function () {
+    this.recipient = await GSNBouncerERC20FeeMock.new(name, symbol, decimals);
+    this.token = await ERC20Detailed.at(await this.recipient.token());
+  });
+
+  describe('token', function () {
+    it('has a name', async function () {
+      expect(await this.token.name()).to.equal(name);
+    });
+
+    it('has a symbol', async function () {
+      expect(await this.token.symbol()).to.equal(symbol);
+    });
+
+    it('has decimals', async function () {
+      expect(await this.token.decimals()).to.be.bignumber.equal(decimals);
+    });
+  });
+
+  context('when called directly', function () {
+    it('mock function can be called', async function () {
+      const { logs } = await this.recipient.mockFunction();
+      expectEvent.inLogs(logs, 'MockFunctionCalled');
+    });
+  });
+
+  context('when relay-called', function () {
+    beforeEach(async function () {
+      await gsn.fundRecipient(web3, { recipient: this.recipient.address });
+      this.relayHub = await IRelayHub.at('0xD216153c06E857cD7f72665E0aF1d7D82172F494');
+    });
+
+    it('charges the sender for GSN fees in tokens', async function () {
+      // The recipient will be charged from its RelayHub balance, and in turn charge the sender from its sender balance.
+      // Both amounts should be roughly equal.
+
+      // The sender has a balance in tokens, not ether, but since the exchange rate is 1:1, this works fine.
+      const senderPreBalance = ether('2');
+      await this.recipient.mint(sender, senderPreBalance);
+
+      const recipientPreBalance = await this.relayHub.balanceOf(this.recipient.address);
+
+      const { tx } = await this.recipient.mockFunction({ from: sender, useGSN: true });
+      await expectEvent.inTransaction(tx, IRelayHub, 'TransactionRelayed', { status: '0' });
+
+      const senderPostBalance = await this.token.balanceOf(sender);
+      const recipientPostBalance = await this.relayHub.balanceOf(this.recipient.address);
+
+      const senderCharge = senderPreBalance.sub(senderPostBalance);
+      const recipientCharge = recipientPreBalance.sub(recipientPostBalance);
+
+      expect(senderCharge).to.be.bignumber.closeTo(recipientCharge, recipientCharge.divn(10));
+    });
+  });
+});

+ 73 - 0
test/GSN/GSNBouncerSignature.test.js

@@ -0,0 +1,73 @@
+const { expectEvent } = require('openzeppelin-test-helpers');
+const gsn = require('@openzeppelin/gsn-helpers');
+const { fixSignature } = require('../helpers/sign');
+const { utils: { toBN } } = require('web3');
+
+const GSNBouncerSignatureMock = artifacts.require('GSNBouncerSignatureMock');
+
+contract('GSNBouncerSignature', function ([_, signer, other]) {
+  beforeEach(async function () {
+    this.recipient = await GSNBouncerSignatureMock.new(signer);
+  });
+
+  context('when called directly', function () {
+    it('mock function can be called', async function () {
+      const { logs } = await this.recipient.mockFunction();
+      expectEvent.inLogs(logs, 'MockFunctionCalled');
+    });
+  });
+
+  context('when relay-called', function () {
+    beforeEach(async function () {
+      await gsn.fundRecipient(web3, { recipient: this.recipient.address });
+    });
+
+    it('rejects unsigned relay requests', async function () {
+      await gsn.expectError(this.recipient.mockFunction({ value: 0, useGSN: true }));
+    });
+
+    it('rejects relay requests where some parameters are signed', async function () {
+      const approveFunction = async (data) =>
+        fixSignature(
+          await web3.eth.sign(
+            web3.utils.soliditySha3(
+              // the nonce is not signed
+              data.relayerAddress, data.from, data.encodedFunctionCall, data.txFee, data.gasPrice, data.gas
+            ), signer
+          )
+        );
+
+      await gsn.expectError(this.recipient.mockFunction({ value: 0, useGSN: true, approveFunction }));
+    });
+
+    it('accepts relay requests where all parameters are signed', async function () {
+      const approveFunction = async (data) =>
+        fixSignature(
+          await web3.eth.sign(
+            web3.utils.soliditySha3(
+              // eslint-disable-next-line max-len
+              data.relayerAddress, data.from, data.encodedFunctionCall, toBN(data.txFee), toBN(data.gasPrice), toBN(data.gas), toBN(data.nonce), data.relayHubAddress, data.to
+            ), signer
+          )
+        );
+
+      const { tx } = await this.recipient.mockFunction({ value: 0, useGSN: true, approveFunction });
+
+      await expectEvent.inTransaction(tx, GSNBouncerSignatureMock, 'MockFunctionCalled');
+    });
+
+    it('rejects relay requests where all parameters are signed by an invalid signer', async function () {
+      const approveFunction = async (data) =>
+        fixSignature(
+          await web3.eth.sign(
+            web3.utils.soliditySha3(
+              // eslint-disable-next-line max-len
+              data.relay_address, data.from, data.encodedFunctionCall, data.txfee, data.gasPrice, data.gas, data.nonce, data.relayHubAddress, data.to
+            ), other
+          )
+        );
+
+      await gsn.expectError(this.recipient.mockFunction({ value: 0, useGSN: true, approveFunction }));
+    });
+  });
+});

+ 78 - 0
test/GSN/GSNContext.test.js

@@ -0,0 +1,78 @@
+const { BN, constants, expectEvent, expectRevert } = require('openzeppelin-test-helpers');
+const { ZERO_ADDRESS } = constants;
+const gsn = require('@openzeppelin/gsn-helpers');
+
+const GSNContextMock = artifacts.require('GSNContextMock');
+const ContextMockCaller = artifacts.require('ContextMockCaller');
+
+const { shouldBehaveLikeRegularContext } = require('./Context.behavior');
+
+contract('GSNContext', function ([_, deployer, sender, newRelayHub]) {
+  beforeEach(async function () {
+    this.context = await GSNContextMock.new();
+    this.caller = await ContextMockCaller.new();
+  });
+
+  describe('get/set RelayHub', function () {
+    const singletonRelayHub = '0xD216153c06E857cD7f72665E0aF1d7D82172F494';
+
+    it('initially returns the singleton instance address', async function () {
+      expect(await this.context.getRelayHub()).to.equal(singletonRelayHub);
+    });
+
+    it('can be upgraded to a new RelayHub', async function () {
+      const { logs } = await this.context.upgradeRelayHub(newRelayHub);
+      expectEvent.inLogs(logs, 'RelayHubChanged', { oldRelayHub: singletonRelayHub, newRelayHub });
+    });
+
+    it('cannot upgrade to the same RelayHub', async function () {
+      await expectRevert(
+        this.context.upgradeRelayHub(singletonRelayHub),
+        'GSNContext: new RelayHub is the current one'
+      );
+    });
+
+    it('cannot upgrade to the zero address', async function () {
+      await expectRevert(this.context.upgradeRelayHub(ZERO_ADDRESS), 'GSNContext: new RelayHub is the zero address');
+    });
+
+    context('with new RelayHub', function () {
+      beforeEach(async function () {
+        await this.context.upgradeRelayHub(newRelayHub);
+      });
+
+      it('returns the new instance address', async function () {
+        expect(await this.context.getRelayHub()).to.equal(newRelayHub);
+      });
+    });
+  });
+
+  context('when called directly', function () {
+    shouldBehaveLikeRegularContext(sender);
+  });
+
+  context('when receiving a relayed call', function () {
+    beforeEach(async function () {
+      await gsn.fundRecipient(web3, { recipient: this.context.address });
+    });
+
+    describe('msgSender', function () {
+      it('returns the relayed transaction original sender', async function () {
+        const { tx } = await this.context.msgSender({ from: sender, useGSN: true });
+        await expectEvent.inTransaction(tx, GSNContextMock, 'Sender', { sender });
+      });
+    });
+
+    describe('msgData', function () {
+      it('returns the relayed transaction original data', async function () {
+        const integerValue = new BN('42');
+        const stringValue = 'OpenZeppelin';
+        const callData = this.context.contract.methods.msgData(integerValue.toString(), stringValue).encodeABI();
+
+        // The provider doesn't properly estimate gas for a relayed call, so we need to manually set a higher value
+        const { tx } = await this.context.msgData(integerValue, stringValue, { gas: 1000000, useGSN: true });
+        await expectEvent.inTransaction(tx, GSNContextMock, 'Data', { data: callData, integerValue, stringValue });
+      });
+    });
+  });
+});

+ 44 - 0
test/GSN/GSNRecipient.test.js

@@ -0,0 +1,44 @@
+const { balance, ether, expectRevert } = require('openzeppelin-test-helpers');
+const gsn = require('@openzeppelin/gsn-helpers');
+
+const { expect } = require('chai');
+
+const GSNRecipientMock = artifacts.require('GSNRecipientMock');
+
+contract('GSNRecipient', function ([_, payee]) {
+  beforeEach(async function () {
+    this.recipient = await GSNRecipientMock.new();
+  });
+
+  it('returns the RelayHub address address', async function () {
+    expect(await this.recipient.getHubAddr()).to.equal('0xD216153c06E857cD7f72665E0aF1d7D82172F494');
+  });
+
+  it('returns the compatible RelayHub version', async function () {
+    expect(await this.recipient.relayHubVersion()).to.equal('1.0.0');
+  });
+
+  context('with deposited funds', async function () {
+    const amount = ether('1');
+
+    beforeEach(async function () {
+      await gsn.fundRecipient(web3, { recipient: this.recipient.address, amount });
+    });
+
+    it('funds can be withdrawn', async function () {
+      const balanceTracker = await balance.tracker(payee);
+      await this.recipient.withdrawDeposits(amount, payee);
+      expect(await balanceTracker.delta()).to.be.bignumber.equal(amount);
+    });
+
+    it('partial funds can be withdrawn', async function () {
+      const balanceTracker = await balance.tracker(payee);
+      await this.recipient.withdrawDeposits(amount.divn(2), payee);
+      expect(await balanceTracker.delta()).to.be.bignumber.equal(amount.divn(2));
+    });
+
+    it('reverts on overwithdrawals', async function () {
+      await expectRevert(this.recipient.withdrawDeposits(amount.addn(1), payee), 'insufficient funds');
+    });
+  });
+});

+ 16 - 5
truffle-config.js

@@ -1,4 +1,5 @@
 require('chai/register-should');
+const { GSNDevProvider } = require('@openzeppelin/gsn-provider');
 
 const solcStable = {
   version: '0.5.7',
@@ -14,16 +15,26 @@ const useSolcNightly = process.env.SOLC_NIGHTLY === 'true';
 module.exports = {
   networks: {
     development: {
-      host: 'localhost',
-      port: 8545,
+      provider: new GSNDevProvider('http://localhost:8545', {
+        txfee: 70,
+        useGSN: false,
+        // The last two accounts defined in test.sh
+        ownerAddress: '0x26be9c03ca7f61ad3d716253ee1edcae22734698',
+        relayerAddress: '0xdc5fd04802ea70f6e27aec12d56716624c98e749',
+      }),
       network_id: '*', // eslint-disable-line camelcase
     },
     coverage: {
-      host: 'localhost',
-      network_id: '*', // eslint-disable-line camelcase
-      port: 8555,
+      provider: new GSNDevProvider('http://localhost:8555', {
+        txfee: 70,
+        useGSN: false,
+        // The last two accounts defined in test.sh
+        ownerAddress: '0x26be9c03ca7f61ad3d716253ee1edcae22734698',
+        relayerAddress: '0xdc5fd04802ea70f6e27aec12d56716624c98e749',
+      }),
       gas: 0xfffffffffff,
       gasPrice: 0x01,
+      network_id: '*', // eslint-disable-line camelcase
     },
   },
 

Some files were not shown because too many files changed in this diff