Unverified Commit f7c82526 by Hadrien Croubois Committed by GitHub

Remove GSNv1 contracts (#2521)

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
parent e66e3ca5
......@@ -7,6 +7,7 @@
* `ERC20`: Removed the `_setDecimals` function and the storage slot associated to decimals. ([#2502](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2502))
* `Strings`: addition of a `toHexString` function. ([#2504](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2504))
* `EnumerableMap`: change implementation to optimize for `key → value` lookups instead of enumeration. ([#2518](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2518))
* `GSN`: Deprecate GSNv1 support in favor of upcomming support for GSNv2. ([#2521](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2521))
## 3.4.0 (2021-02-02)
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../utils/Context.sol";
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../utils/Context.sol";
import "./IRelayRecipient.sol";
import "./IRelayHub.sol";
/**
* @dev Base GSN recipient contract: includes the {IRelayRecipient} interface
* and enables GSN support on all contracts in the inheritance tree.
*
* TIP: This contract is abstract. The functions {IRelayRecipient-acceptRelayedCall},
* {_preRelayedCall}, and {_postRelayedCall} are not implemented and must be
* provided by derived contracts. See the
* xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategies] for more
* information on how to use the pre-built {GSNRecipientSignature} and
* {GSNRecipientERC20Fee}, or how to write your own.
*/
abstract contract GSNRecipient is IRelayRecipient, Context {
// Default RelayHub address, deployed on mainnet and all testnets at the same address
address private _relayHub = 0xD216153c06E857cD7f72665E0aF1d7D82172F494;
uint256 constant private _RELAYED_CALL_ACCEPTED = 0;
uint256 constant private _RELAYED_CALL_REJECTED = 11;
// How much gas is forwarded to postRelayedCall
uint256 constant internal _POST_RELAYED_CALL_MAX_GAS = 100000;
/**
* @dev Emitted when a contract changes its {IRelayHub} contract to a new one.
*/
event RelayHubChanged(address indexed oldRelayHub, address indexed newRelayHub);
/**
* @dev Returns the address of the {IRelayHub} contract for this recipient.
*/
function getHubAddr() public view virtual override returns (address) {
return _relayHub;
}
/**
* @dev Switches to a new {IRelayHub} instance. This method is added for future-proofing: there's no reason to not
* use the default instance.
*
* IMPORTANT: After upgrading, the {GSNRecipient} will no longer be able to receive relayed calls from the old
* {IRelayHub} instance. Additionally, all funds should be previously withdrawn via {_withdrawDeposits}.
*/
function _upgradeRelayHub(address newRelayHub) internal virtual {
address currentRelayHub = _relayHub;
require(newRelayHub != address(0), "GSNRecipient: new RelayHub is the zero address");
require(newRelayHub != currentRelayHub, "GSNRecipient: new RelayHub is the current one");
emit RelayHubChanged(currentRelayHub, newRelayHub);
_relayHub = newRelayHub;
}
/**
* @dev Returns the version string of the {IRelayHub} for which this recipient implementation was built. If
* {_upgradeRelayHub} is used, the new {IRelayHub} instance should be compatible with this version.
*/
// This function is view for future-proofing, it may require reading from
// storage in the future.
function relayHubVersion() public view virtual returns (string memory) {
this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
return "1.0.0";
}
/**
* @dev Withdraws the recipient's deposits in `RelayHub`.
*
* Derived contracts should expose this in an external interface with proper access control.
*/
function _withdrawDeposits(uint256 amount, address payable payee) internal virtual {
IRelayHub(getHubAddr()).withdraw(amount, payee);
}
// 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.
/**
* @dev Replacement for msg.sender. Returns the actual sender of a transaction: msg.sender for regular transactions,
* and the end-user for GSN relayed calls (where msg.sender is actually `RelayHub`).
*
* IMPORTANT: Contracts derived from {GSNRecipient} should never use `msg.sender`, and use {_msgSender} instead.
*/
function _msgSender() internal view virtual override returns (address msgSender) {
if (msg.sender == getHubAddr()) {
assembly { msgSender := shr(96, calldataload(sub(calldatasize(), 20))) }
} else {
return msg.sender;
}
}
/**
* @dev Replacement for msg.data. Returns the actual calldata of a transaction: msg.data for regular transactions,
* and a reduced version for GSN relayed calls (where msg.data contains additional information).
*
* IMPORTANT: Contracts derived from {GSNRecipient} should never use `msg.data`, and use {_msgData} instead.
*/
function _msgData() internal view virtual override returns (bytes calldata) {
if (msg.sender == getHubAddr()) {
return msg.data[:msg.data.length - 20];
} else {
return msg.data;
}
}
// 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 overridden directly, use `_preRelayedCall` instead.
*
* * Requirements:
*
* - the caller must be the `RelayHub` contract.
*/
function preRelayedCall(bytes memory context) public virtual override returns (bytes32) {
require(msg.sender == getHubAddr(), "GSNRecipient: caller is not RelayHub");
return _preRelayedCall(context);
}
/**
* @dev See `IRelayRecipient.preRelayedCall`.
*
* Called by `GSNRecipient.preRelayedCall`, which asserts the caller is the `RelayHub` contract. Derived contracts
* must implement this function with any relayed-call preprocessing they may wish to do.
*
*/
function _preRelayedCall(bytes memory context) internal virtual returns (bytes32);
/**
* @dev See `IRelayRecipient.postRelayedCall`.
*
* This function should not be overridden directly, use `_postRelayedCall` instead.
*
* * Requirements:
*
* - the caller must be the `RelayHub` contract.
*/
function postRelayedCall(bytes memory context, bool success, uint256 actualCharge, bytes32 preRetVal) public virtual override {
require(msg.sender == getHubAddr(), "GSNRecipient: caller is not RelayHub");
_postRelayedCall(context, success, actualCharge, preRetVal);
}
/**
* @dev See `IRelayRecipient.postRelayedCall`.
*
* Called by `GSNRecipient.postRelayedCall`, which asserts the caller is the `RelayHub` contract. Derived contracts
* must implement this function with any relayed-call postprocessing they may wish to do.
*
*/
function _postRelayedCall(bytes memory context, bool success, uint256 actualCharge, bytes32 preRetVal) internal virtual;
/**
* @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 virtual returns (uint256, bytes memory) {
return _approveRelayedCall("");
}
/**
* @dev See `GSNRecipient._approveRelayedCall`.
*
* This overload forwards `context` to _preRelayedCall and _postRelayedCall.
*/
function _approveRelayedCall(bytes memory context) internal pure virtual 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 virtual returns (uint256, bytes memory) {
return (_RELAYED_CALL_REJECTED + errorCode, "");
}
/*
* @dev Calculates how much RelayHub will charge a recipient for using `gas` at a `gasPrice`, given a relayer's
* `serviceFee`.
*/
function _computeCharge(uint256 gas, uint256 gasPrice, uint256 serviceFee) internal pure virtual 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;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./GSNRecipient.sol";
import "../access/Ownable.sol";
import "../token/ERC20/SafeERC20.sol";
import "../token/ERC20/ERC20.sol";
/**
* @dev A xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategy] that charges transaction fees in a special purpose ERC20
* token, which we refer to as the gas payment token. The amount charged is exactly the amount of Ether charged to the
* recipient. This means that the token is essentially pegged to the value of Ether.
*
* 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 GSNRecipientERC20Fee is GSNRecipient {
using SafeERC20 for __unstable__ERC20Owned;
enum GSNRecipientERC20FeeErrorCodes {
INSUFFICIENT_BALANCE
}
__unstable__ERC20Owned private _token;
/**
* @dev The arguments to the constructor are the details that the gas payment token will have: `name` and `symbol`. `decimals` is hard-coded to 18.
*/
constructor(string memory name, string memory symbol) {
_token = new __unstable__ERC20Owned(name, symbol);
}
/**
* @dev Returns the gas payment token.
*/
function token() public view virtual returns (__unstable__ERC20Owned) {
return _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 virtual {
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,
bytes memory,
uint256 transactionFee,
uint256 gasPrice,
uint256,
uint256,
bytes memory,
uint256 maxPossibleCharge
)
public
view
virtual
override
returns (uint256, bytes memory)
{
if (token().balanceOf(from) < maxPossibleCharge) {
return _rejectRelayedCall(uint256(GSNRecipientERC20FeeErrorCodes.INSUFFICIENT_BALANCE));
}
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 virtual override 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);
return 0;
}
/**
* @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 virtual override {
(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 - 10000, gasPrice, transactionFee);
actualCharge = actualCharge - overestimation;
// After the relayed call has been executed and the actual charge estimated, the excess pre-charge is returned
token().safeTransfer(from, maxPossibleCharge - actualCharge);
}
}
/**
* @title __unstable__ERC20Owned
* @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__ERC20Owned is ERC20, Ownable {
uint256 private constant _UINT256_MAX = 2**256 - 1;
constructor(string memory name, string memory symbol) ERC20(name, symbol) { }
// The owner (GSNRecipientERC20Fee) can mint tokens
function mint(address account, uint256 amount) public virtual onlyOwner {
_mint(account, amount);
}
// The owner has 'infinite' allowance for all token holders
function allowance(address tokenOwner, address spender) public view virtual override returns (uint256) {
if (spender == owner()) {
return _UINT256_MAX;
} else {
return super.allowance(tokenOwner, spender);
}
}
// Allowance for the owner cannot be changed (it is always 'infinite')
function _approve(address tokenOwner, address spender, uint256 value) internal virtual override {
if (spender == owner()) {
return;
} else {
super._approve(tokenOwner, spender, value);
}
}
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
if (recipient == owner()) {
_transfer(sender, recipient, amount);
return true;
} else {
return super.transferFrom(sender, recipient, amount);
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./GSNRecipient.sol";
import "../cryptography/ECDSA.sol";
/**
* @dev A xref:ROOT:gsn-strategies.adoc#gsn-strategies[GSN strategy] that allows relayed transactions through when they are
* accompanied by the signature of a trusted signer. The intent is for this signature to be generated by a server that
* performs validations off-chain. Note that nothing is charged to the user in this scheme. Thus, the server should make
* sure to account for this in their economic and threat model.
*/
contract GSNRecipientSignature is GSNRecipient {
using ECDSA for bytes32;
address private _trustedSigner;
enum GSNRecipientSignatureErrorCodes {
INVALID_SIGNER
}
/**
* @dev Sets the trusted signer that is going to be producing signatures to approve relayed calls.
*/
constructor(address trustedSigner) {
require(trustedSigner != address(0), "GSNRecipientSignature: trusted signer is the zero address");
_trustedSigner = trustedSigner;
}
/**
* @dev Ensures that only transactions with a trusted signature can be relayed through the GSN.
*/
function acceptRelayedCall(
address relay,
address from,
bytes memory encodedFunction,
uint256 transactionFee,
uint256 gasPrice,
uint256 gasLimit,
uint256 nonce,
bytes memory approvalData,
uint256
)
public
view
virtual
override
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(GSNRecipientSignatureErrorCodes.INVALID_SIGNER));
}
}
function _preRelayedCall(bytes memory) internal virtual override returns (bytes32) { }
function _postRelayedCall(bytes memory, bool, uint256, bytes32) internal virtual override { }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Base interface for a contract that will be called via the GSN from {IRelayHub}.
*
* TIP: You don't need to write an implementation yourself! Inherit from {GSNRecipient} instead.
*/
interface IRelayRecipient {
/**
* @dev Returns the address of the {IRelayHub} instance this recipient interacts with.
*/
function getHubAddr() external view returns (address);
/**
* @dev Called by {IRelayHub} to validate if this recipient accepts being charged for a relayed call. Note that the
* recipient will be charged regardless of the execution result of the relayed call (i.e. if it reverts or not).
*
* The relay request was originated by `from` and will be served by `relay`. `encodedFunction` is the relayed call
* calldata, so its first four bytes are the function selector. The relayed call will be forwarded `gasLimit` gas,
* and the transaction executed with a gas price of at least `gasPrice`. ``relay``'s fee is `transactionFee`, and the
* recipient will be charged at most `maxPossibleCharge` (in wei). `nonce` is the sender's (`from`) nonce for
* replay attack protection in {IRelayHub}, and `approvalData` is a optional parameter that can be used to hold a signature
* over all or some of the previous values.
*
* Returns a tuple, where the first value is used to indicate approval (0) or rejection (custom non-zero error code,
* values 1 to 10 are reserved) and the second one is data to be passed to the other {IRelayRecipient} functions.
*
* {acceptRelayedCall} is called with 50k gas: if it runs out during execution, the request will be considered
* rejected. A regular revert will also trigger a rejection.
*/
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);
/**
* @dev Called by {IRelayHub} on approved relay call requests, before the relayed call is executed. This allows to e.g.
* pre-charge the sender of the transaction.
*
* `context` is the second value returned in the tuple by {acceptRelayedCall}.
*
* Returns a value to be passed to {postRelayedCall}.
*
* {preRelayedCall} is called with 100k gas: if it runs out during execution or otherwise reverts, the relayed call
* will not be executed, but the recipient will still be charged for the transaction's cost.
*/
function preRelayedCall(bytes calldata context) external returns (bytes32);
/**
* @dev Called by {IRelayHub} on approved relay call requests, after the relayed call is executed. This allows to e.g.
* charge the user for the relayed call costs, return any overcharges from {preRelayedCall}, or perform
* contract-specific bookkeeping.
*
* `context` is the second value returned in the tuple by {acceptRelayedCall}. `success` is the execution status of
* the relayed call. `actualCharge` is an estimate of how much the recipient will be charged for the transaction,
* not including any gas used by {postRelayedCall} itself. `preRetVal` is {preRelayedCall}'s return value.
*
*
* {postRelayedCall} is called with 100k gas: if it runs out during execution or otherwise reverts, the relayed call
* and the call to {preRelayedCall} will be reverted retroactively, but the recipient will still be charged for the
* transaction's cost.
*/
function postRelayedCall(bytes calldata context, bool success, uint256 actualCharge, bytes32 preRetVal) external;
}
= Gas Station Network (GSN)
[.readme-notice]
NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/gsn
This set of contracts provide all the tools required to make a contract callable via the https://gsn.openzeppelin.com[Gas Station Network].
TIP: If you're new to the GSN, head over to our xref:learn::sending-gasless-transactions.adoc[overview of the system] and basic guide to xref:ROOT:gsn.adoc[creating a GSN-capable contract].
The core contract a recipient must inherit from is {GSNRecipient}: it includes all necessary interfaces, as well as some helper methods to make interacting with the GSN easier.
Utilities to make writing xref:ROOT:gsn-strategies.adoc[GSN strategies] easy are available in {GSNRecipient}, or you can simply use one of our pre-made strategies:
* {GSNRecipientERC20Fee} charges the end user for gas costs in an application-specific xref:ROOT:tokens.adoc#ERC20[ERC20 token]
* {GSNRecipientSignature} accepts all relayed calls that have been signed by a trusted third party (e.g. a private key in a backend)
You can also take a look at the two contract interfaces that make up the GSN protocol: {IRelayRecipient} and {IRelayHub}, but you won't need to use those directly.
== Recipient
{{GSNRecipient}}
== Strategies
{{GSNRecipientSignature}}
{{GSNRecipientERC20Fee}}
== Protocol
{{IRelayRecipient}}
{{IRelayHub}}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../token/ERC721/ERC721.sol";
import "../GSN/GSNRecipient.sol";
import "../GSN/GSNRecipientSignature.sol";
/**
* @title ERC721GSNRecipientMock
* A simple ERC721 mock that has GSN support enabled
*/
contract ERC721GSNRecipientMock is ERC721, GSNRecipient, GSNRecipientSignature {
constructor(string memory name, string memory symbol, address trustedSigner)
ERC721(name, symbol)
GSNRecipientSignature(trustedSigner)
{ }
function mint(uint256 tokenId) public {
_mint(_msgSender(), tokenId);
}
function _msgSender() internal view override(Context, GSNRecipient) returns (address) {
return GSNRecipient._msgSender();
}
function _msgData() internal view override(Context, GSNRecipient) returns (bytes memory) {
return GSNRecipient._msgData();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../GSN/GSNRecipient.sol";
import "../GSN/GSNRecipientERC20Fee.sol";
contract GSNRecipientERC20FeeMock is GSNRecipient, GSNRecipientERC20Fee {
constructor(string memory name, string memory symbol) GSNRecipientERC20Fee(name, symbol) { }
function mint(address account, uint256 amount) public {
_mint(account, amount);
}
event MockFunctionCalled(uint256 senderBalance);
function mockFunction() public {
emit MockFunctionCalled(token().balanceOf(_msgSender()));
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ContextMock.sol";
import "../GSN/GSNRecipient.sol";
// By inheriting from GSNRecipient, Context's internal functions are overridden automatically
contract GSNRecipientMock is ContextMock, 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
pure
override
returns (uint256, bytes memory)
{
return (0, "");
}
function _preRelayedCall(bytes memory) internal override returns (bytes32) { }
function _postRelayedCall(bytes memory, bool, uint256, bytes32) internal override { }
function upgradeRelayHub(address newRelayHub) public {
return _upgradeRelayHub(newRelayHub);
}
function _msgSender() internal override(Context, GSNRecipient) view virtual returns (address) {
return GSNRecipient._msgSender();
}
function _msgData() internal override(Context, GSNRecipient) view virtual returns (bytes calldata) {
return GSNRecipient._msgData();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../GSN/GSNRecipient.sol";
import "../GSN/GSNRecipientSignature.sol";
contract GSNRecipientSignatureMock is GSNRecipient, GSNRecipientSignature {
constructor(address trustedSigner) GSNRecipientSignature(trustedSigner) { }
event MockFunctionCalled();
function mockFunction() public {
emit MockFunctionCalled();
}
}
......@@ -6,7 +6,7 @@ pragma solidity ^0.8.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 should not be accessed in such a direct
* manner, since when dealing with GSN meta-transactions the account sending and
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
......
......@@ -13,7 +13,4 @@
** xref:erc777.adoc[ERC777]
** xref:erc1155.adoc[ERC1155]
* xref:gsn.adoc[Gas Station Network]
** xref:gsn-strategies.adoc[Strategies]
* xref:utilities.adoc[Utilities]
= Writing GSN-capable contracts
The https://gsn.openzeppelin.com[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.
If you're new to the GSN, you probably want to first take a look at the xref:learn::sending-gasless-transactions.adoc[overview of the system] to get a clearer picture of how gasless transactions are achieved. Otherwise, strap in!
== Receiving a Relayed Call
The first step to writing a recipient is to inherit from our GSNRecipient contract. If you're also inheriting from other contracts, such as ERC20, this will work just fine: adding GSNRecipient will make all of your token functions GSN-callable.
```solidity
import "@openzeppelin/contracts/GSN/GSNRecipient.sol";
contract MyContract is GSNRecipient, ... {
}
```
=== `msg.sender` and `msg.data`
There's only one extra detail you need to take care of when working with GSN recipient contracts: _you must never use `msg.sender` or `msg.data` directly_. On relayed calls, `msg.sender` will be `RelayHub` instead of your user! This doesn't mean however you won't be able to retrieve your users' addresses: `GSNRecipient` provides two functions, `_msgSender()` and `_msgData()`, which are drop-in replacements for `msg.sender` and `msg.data` while taking care of the low-level details. As long as you use these two functions instead of the original getters, you're good to go!
WARNING: Third-party contracts you inherit from may not use these replacement functions, making them unsafe to use when mixed with `GSNRecipient`. If in doubt, head on over to our https://forum.openzeppelin.com/c/support[support forum].
=== Accepting and Charging
Unlike regular contract function calls, each relayed call has an additional number of steps it must go through, which are functions of the `IRelayRecipient` interface `RelayHub` will call on your contract. `GSNRecipient` includes this interface but no implementation: most of writing a recipient involves handling these function calls. They are designed to provide flexibility, but basic recipients can safely ignore most of them while still being secure and sound.
The OpenZeppelin Contracts provide a number of tried-and-tested approaches for you to use out of the box, but you should still have a basic idea of what's going on under the hood.
==== `acceptRelayedCall`
First, RelayHub will ask your recipient contract if it wants to receive a relayed call. Recall that you will be charged for incurred gas costs by the relayer, so you should only accept calls that you're willing to pay for!
[source,solidity]
----
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);
----
There are multiple ways to make this work, including:
. having a whitelist of trusted users
. only accepting calls to an onboarding function
. charging users in tokens (possibly issued by you)
. delegating the acceptance logic off-chain
All relayed call requests can be rejected at no cost to the recipient.
In this function, you return a number indicating whether you accept the call (0) or not (any other number). You can also return some arbitrary data that will get passed along to the following two functions (pre and post) as an execution context.
=== pre and postRelayedCall
After a relayed call is accepted, RelayHub will give your contract two opportunities to charge your user for their call, perform some bookkeeping, etc.: _before_ and _after_ the actual relayed call is made. These functions are aptly named `preRelayedCall` and `postRelayedCall`.
[source,solidity]
----
function preRelayedCall(bytes calldata context) external returns (bytes32);
----
`preRelayedCall` will inform you of the maximum cost the call may have, and can be used to charge the user in advance. This is useful if the user may spend their allowance as part of the call, so you can lock some funds here.
[source,solidity]
----
function postRelayedCall(
bytes calldata context,
bool success,
uint256 actualCharge,
bytes32 preRetVal
) external;
----
`postRelayedCall` will give you an accurate estimate of the transaction cost, making it a natural place to charge users. It will also let you know if the relayed call reverted or not. This allows you, for instance, to not charge users for reverted calls - but remember that you will be charged by the relayer nonetheless.
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.
== Further reading
Read our xref:gsn-strategies.adoc[guide on the payment strategies] pre-built and shipped in OpenZeppelin Contracts, or check out xref:api:GSN.adoc[the API reference of the GSN base contracts].
......@@ -5,7 +5,6 @@
* Implementations of standards like xref:erc20.adoc[ERC20] and xref:erc721.adoc[ERC721].
* Flexible xref:access-control.adoc[role-based permissioning] scheme.
* Reusable xref:utilities.adoc[Solidity components] to build custom contracts and complex decentralized systems.
* First-class integration with the xref:gsn.adoc[Gas Station Network] for systems with no gas fees!
* https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/audit[Audited] by leading security firms (_last full audit on v2.0.0_).
== Overview
......@@ -49,7 +48,6 @@ The guides in the sidebar will teach about different concepts, and how to use th
* xref:access-control.adoc[Access Control]: decide who can perform each of the actions on your system.
* xref:tokens.adoc[Tokens]: create tradable assets or collectibles, like the well known xref:erc20.adoc[ERC20] and xref:erc721.adoc[ERC721] standards.
* xref:gsn.adoc[Gas Station Network]: let your users interact with your contracts without having to pay for gas themselves.
* xref:utilities.adoc[Utilities]: generic useful tools, including non-overflowing math, signature verification, and trustless paying systems.
The xref:api:token/ERC20.adoc[full API] is also thoroughly documented, and serves as a great reference when developing your smart contract application. You can also ask for help or follow Contracts' development in the https://forum.openzeppelin.com[community forum].
......@@ -59,4 +57,3 @@ Finally, you may want to take a look at the https://blog.openzeppelin.com/guides
* https://blog.openzeppelin.com/the-hitchhikers-guide-to-smart-contracts-in-ethereum-848f08001f05[The Hitchhiker’s Guide to Smart Contracts in Ethereum] will help you get an overview of the various tools available for smart contract development, and help you set up your environment.
* https://blog.openzeppelin.com/a-gentle-introduction-to-ethereum-programming-part-1-783cc7796094[A Gentle Introduction to Ethereum Programming, Part 1] provides very useful information on an introductory level, including many basic concepts from the Ethereum platform.
* For a more in-depth dive, you may read the guide https://blog.openzeppelin.com/designing-the-architecture-for-your-ethereum-application-9cec086f8317[Designing the architecture for your Ethereum application], which discusses how to better structure your application and its relationship to the real world.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -50,8 +50,6 @@
"@nomiclabs/hardhat-truffle5": "^2.0.0",
"@nomiclabs/hardhat-web3": "^2.0.0",
"@openzeppelin/docs-utils": "^0.1.0",
"@openzeppelin/gsn-helpers": "^0.2.3",
"@openzeppelin/gsn-provider": "^0.1.10",
"@openzeppelin/test-helpers": "^0.5.9",
"chai": "^4.2.0",
"eslint": "^6.5.1",
......
const { constants, expectEvent } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
const gsn = require('@openzeppelin/gsn-helpers');
const { fixSignature } = require('../helpers/sign');
const { setGSNProvider } = require('../helpers/set-gsn-provider');
const { utils: { toBN } } = require('web3');
const ERC721GSNRecipientMock = artifacts.require('ERC721GSNRecipientMock');
contract('ERC721GSNRecipient (integration)', function (accounts) {
const [ signer, sender ] = accounts;
const name = 'Non Fungible Token';
const symbol = 'NFT';
const tokenId = '42';
before(function () {
setGSNProvider(ERC721GSNRecipientMock, accounts);
});
beforeEach(async function () {
this.token = await ERC721GSNRecipientMock.new(name, symbol, signer);
});
async function testMintToken (token, from, tokenId, options = {}) {
const { tx } = await token.mint(tokenId, { from, ...options });
await expectEvent.inTransaction(tx, ERC721GSNRecipientMock, 'Transfer', { from: ZERO_ADDRESS, to: from, tokenId });
}
context('when called directly', function () {
it('sender can mint tokens', async function () {
await testMintToken(this.token, sender, tokenId);
});
});
context('when relay-called', function () {
beforeEach(async function () {
await gsn.fundRecipient(web3, { recipient: this.token.address });
});
it('sender can mint tokens', 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, this.token.address,
), signer,
),
);
await testMintToken(this.token, sender, tokenId, { useGSN: true, approveFunction });
});
});
});
const { balance, BN, constants, ether, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { ZERO_ADDRESS } = constants;
const gsn = require('@openzeppelin/gsn-helpers');
const { setGSNProvider } = require('../helpers/set-gsn-provider');
const { expect } = require('chai');
const GSNRecipientMock = artifacts.require('GSNRecipientMock');
const ContextMockCaller = artifacts.require('ContextMockCaller');
const { shouldBehaveLikeRegularContext } = require('./Context.behavior');
contract('GSNRecipient', function (accounts) {
const [ payee, sender, newRelayHub ] = accounts;
before(function () {
setGSNProvider(GSNRecipientMock, accounts);
setGSNProvider(ContextMockCaller, accounts);
});
beforeEach(async function () {
this.recipient = await GSNRecipientMock.new();
});
it('returns the compatible RelayHub version', async function () {
expect(await this.recipient.relayHubVersion()).to.equal('1.0.0');
});
describe('get/set RelayHub', function () {
const singletonRelayHub = '0xD216153c06E857cD7f72665E0aF1d7D82172F494';
it('initially returns the singleton instance address', async function () {
expect(await this.recipient.getHubAddr()).to.equal(singletonRelayHub);
});
it('can be upgraded to a new RelayHub', async function () {
const { logs } = await this.recipient.upgradeRelayHub(newRelayHub);
expectEvent.inLogs(logs, 'RelayHubChanged', { oldRelayHub: singletonRelayHub, newRelayHub });
});
it('cannot upgrade to the same RelayHub', async function () {
await expectRevert(
this.recipient.upgradeRelayHub(singletonRelayHub),
'GSNRecipient: new RelayHub is the current one',
);
});
it('cannot upgrade to the zero address', async function () {
await expectRevert(
this.recipient.upgradeRelayHub(ZERO_ADDRESS), 'GSNRecipient: new RelayHub is the zero address',
);
});
context('with new RelayHub', function () {
beforeEach(async function () {
await this.recipient.upgradeRelayHub(newRelayHub);
});
it('returns the new instance address', async function () {
expect(await this.recipient.getHubAddr()).to.equal(newRelayHub);
});
});
});
context('when called directly', function () {
beforeEach(async function () {
this.context = this.recipient; // The Context behavior expects the contract in this.context
this.caller = await ContextMockCaller.new();
});
shouldBehaveLikeRegularContext(sender);
});
context('when receiving a relayed call', function () {
beforeEach(async function () {
await gsn.fundRecipient(web3, { recipient: this.recipient.address });
});
describe('msgSender', function () {
it('returns the relayed transaction original sender', async function () {
const { tx } = await this.recipient.msgSender({ from: sender, useGSN: true });
await expectEvent.inTransaction(tx, GSNRecipientMock, '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.recipient.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.recipient.msgData(integerValue, stringValue, { gas: 1000000, useGSN: true });
await expectEvent.inTransaction(tx, GSNRecipientMock, 'Data', { data: callData, integerValue, stringValue });
});
});
});
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');
});
});
});
const { ether, expectEvent } = require('@openzeppelin/test-helpers');
const gsn = require('@openzeppelin/gsn-helpers');
const { setGSNProvider } = require('../helpers/set-gsn-provider');
const { expect } = require('chai');
const GSNRecipientERC20FeeMock = artifacts.require('GSNRecipientERC20FeeMock');
const ERC20 = artifacts.require('ERC20');
const IRelayHub = artifacts.require('IRelayHub');
contract('GSNRecipientERC20Fee', function (accounts) {
const [ sender ] = accounts;
const name = 'FeeToken';
const symbol = 'FTKN';
before(function () {
setGSNProvider(GSNRecipientERC20FeeMock, accounts);
setGSNProvider(ERC20, accounts);
setGSNProvider(IRelayHub, accounts);
});
beforeEach(async function () {
this.recipient = await GSNRecipientERC20FeeMock.new(name, symbol);
this.token = await ERC20.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 18 decimals', async function () {
expect(await this.token.decimals()).to.be.bignumber.equal('18');
});
});
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));
});
});
});
const { expectEvent, expectRevert, constants } = require('@openzeppelin/test-helpers');
const gsn = require('@openzeppelin/gsn-helpers');
const { fixSignature } = require('../helpers/sign');
const { setGSNProvider } = require('../helpers/set-gsn-provider');
const { utils: { toBN } } = require('web3');
const { ZERO_ADDRESS } = constants;
const GSNRecipientSignatureMock = artifacts.require('GSNRecipientSignatureMock');
contract('GSNRecipientSignature', function (accounts) {
const [ signer, other ] = accounts;
before(function () {
setGSNProvider(GSNRecipientSignatureMock, accounts);
});
beforeEach(async function () {
this.recipient = await GSNRecipientSignatureMock.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 constructor is called with a zero address', function () {
it('fails when constructor called with a zero address', async function () {
await expectRevert(
GSNRecipientSignatureMock.new(
ZERO_ADDRESS,
),
'GSNRecipientSignature: trusted signer is the zero address',
);
});
});
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
// eslint-disable-next-line max-len
data.relayerAddress, data.from, data.encodedFunctionCall, toBN(data.txFee), toBN(data.gasPrice), toBN(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, GSNRecipientSignatureMock, '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.relayerAddress, data.from, data.encodedFunctionCall, toBN(data.txFee), toBN(data.gasPrice), toBN(data.gas), toBN(data.nonce), data.relayHubAddress, data.to,
), other,
),
);
await gsn.expectError(this.recipient.mockFunction({ value: 0, useGSN: true, approveFunction }));
});
});
});
const { GSNDevProvider } = require('@openzeppelin/gsn-provider');
function setGSNProvider (Contract, accounts) {
const baseProvider = Contract.currentProvider;
Contract.setProvider(
new GSNDevProvider(baseProvider, {
txfee: 70,
useGSN: false,
ownerAddress: accounts[8],
relayerAddress: accounts[9],
}),
);
};
module.exports = { setGSNProvider };
const { deployRelayHub } = require('@openzeppelin/gsn-helpers');
before('deploy GSN RelayHub', async function () {
const [defaultSender] = await web3.eth.getAccounts();
await deployRelayHub(web3, { from: defaultSender });
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment