Skip to content

Initial GSN support (beta) #1844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 79 commits into from
Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
2e28b70
Add base Context contract
nventuro Jul 19, 2019
f12245b
Add GSNContext and tests
nventuro Jul 22, 2019
50d596a
Add RelayHub deployment to tests
nventuro Jul 23, 2019
2b1cc85
Add RelayProvider integration, complete GSNContext tests
nventuro Jul 24, 2019
07e37ef
Switch dependency to openzeppelin-gsn-provider
nventuro Jul 25, 2019
df4f2c2
Add default txfee to provider
nventuro Jul 26, 2019
167510a
Add basic signing recipient
nventuro Jul 26, 2019
dc35dff
Sign more values
nventuro Jul 26, 2019
e52ec1a
Add comment clarifying RelayHub's msg.data
nventuro Jul 29, 2019
7363e46
Make context constructors internal
nventuro Jul 29, 2019
67e534f
Rename SigningRecipient to GSNRecipientSignedData
nventuro Jul 29, 2019
f8b97c2
Add ERC20Charge recipients
nventuro Jul 30, 2019
544a56f
Harcode RelayHub address into GSNContext
nventuro Jul 31, 2019
8b3377e
Fix Solidity linter errors
nventuro Jul 31, 2019
8a5d21b
Run server from binary, use gsn-helpers to fund it
nventuro Jul 31, 2019
dc8adcd
Migrate to published @openzeppelin/gsn-helpers
nventuro Aug 1, 2019
82ca6ab
Silence false-positive compiler warning
nventuro Aug 1, 2019
e5ca467
Use GSN helper assertions
nventuro Aug 1, 2019
6871bb7
Rename meta-tx to gsn, take out of drafts
nventuro Aug 1, 2019
108c998
Merge ERC20 charge recipients into a single one
nventuro Aug 1, 2019
5c0e49b
Rename GSNRecipients to Bouncers
nventuro Aug 1, 2019
9847c98
Add GSNBouncerUtils to decouple the bouncers from GSNRecipient
nventuro Aug 1, 2019
af81255
Add _upgradeRelayHub
nventuro Aug 1, 2019
02c8e13
Store RelayHub address using unstructored storage
nventuro Aug 1, 2019
dc9139a
Add IRelayHub
nventuro Aug 2, 2019
081ddc6
Add _withdrawDeposits to GSNRecipient
nventuro Aug 2, 2019
d5af791
Add relayHub version to recipient
nventuro Aug 2, 2019
0302cf1
Make _acceptRelayedCall and _declineRelayedCall easier to use
nventuro Aug 2, 2019
988b6f0
Rename GSNBouncerUtils to GSNBouncerBase, make it IRelayRecipient
nventuro Aug 2, 2019
ca59d08
Improve GSNBouncerBase, make pre and post sender-protected and optional
nventuro Aug 2, 2019
d10109f
Fix GSNBouncerERC20Fee, add tests
nventuro Aug 2, 2019
0ef0023
Add missing GSNBouncerSignature test
nventuro Aug 2, 2019
e3034b0
Override transferFrom in __unstable__ERC20PrimaryAdmin
nventuro Aug 2, 2019
0b3cf83
Fix gsn dependencies in package.json
spalladino Aug 2, 2019
d7d4ff1
Rhub address slot reduced by 1
spalladino Aug 2, 2019
e42d51f
Rename relay hub changed event
spalladino Aug 2, 2019
de0d458
Use released gsn-provider
spalladino Aug 2, 2019
af92387
Run relayer with short sleep of 1s instead of 100ms
spalladino Aug 2, 2019
eebd131
Merge branch 'master' of github.com:OpenZeppelin/openzeppelin-contrac…
frangio Aug 7, 2019
c57bbe6
update package-lock.json
frangio Aug 7, 2019
bcb6b6d
clear circle cache
frangio Aug 7, 2019
25f3ed7
Merge remote-tracking branch 'upstream/master' into gsn-beta
frangio Aug 7, 2019
339250e
Merge branch 'master' of github.com:OpenZeppelin/openzeppelin-contrac…
frangio Aug 8, 2019
98d422e
Merge branch 'master' of github.com:OpenZeppelin/openzeppelin-contrac…
frangio Aug 8, 2019
1108426
use optimized gsn-provider
frangio Aug 8, 2019
c493683
update to latest @openzeppelin/gsn-provider
frangio Aug 9, 2019
68de4a3
replace with gsn dev provider
frangio Aug 9, 2019
00b2818
remove relay server
frangio Aug 9, 2019
1bee370
rename arguments in approveFunction
frangio Aug 9, 2019
e69599e
fix GSNBouncerSignature test
frangio Aug 9, 2019
83eb525
change gsn txfee
frangio Aug 9, 2019
9e25a2c
initialize development provider only once
frangio Aug 9, 2019
f18f937
update RelayHub interface
frangio Aug 9, 2019
9b52cda
adapt to new IRelayHub.withdraw
frangio Aug 9, 2019
b7d3c38
Merge branch 'master' into gsn-beta
frangio Aug 9, 2019
3b9e5af
update @openzeppelin/gsn-helpers
frangio Aug 9, 2019
dc32422
update relayhub singleton address
frangio Aug 9, 2019
73798a8
fix helper name
frangio Aug 9, 2019
8a7b5be
set up gsn provider for coverage too
frangio Aug 9, 2019
944cadb
lint
frangio Aug 9, 2019
0805666
Revert "set up gsn provider for coverage too"
frangio Aug 9, 2019
5921789
remove unused code
frangio Aug 9, 2019
56c8522
add gsn provider to coverage
frangio Aug 12, 2019
79f5db1
move truffle contract options back out
frangio Aug 12, 2019
5af42ff
increase gas limit for coverage
frangio Aug 12, 2019
6bc7622
remove unreachable code
frangio Aug 12, 2019
515d40d
add more gas for GSNContext test
frangio Aug 12, 2019
0ecb521
fix test suite name
frangio Aug 12, 2019
d9cc437
rename GSNBouncerBase internal API
frangio Aug 12, 2019
768b17d
remove onlyRelayHub modifier
frangio Aug 12, 2019
4312948
add explicit inheritance
frangio Aug 12, 2019
f1716b2
remove redundant event
frangio Aug 12, 2019
f02fa1d
update name of bouncers error codes enums
frangio Aug 12, 2019
018065b
add basic docs page for gsn contracts
frangio Aug 12, 2019
2d0b43b
make gsn directory all caps
frangio Aug 12, 2019
76f03d1
Merge branch 'master' into gsn-beta
frangio Aug 12, 2019
74cbca9
add changelog entry
frangio Aug 12, 2019
f73dbb9
lint
frangio Aug 12, 2019
eaed1c4
enable test run to fail in coverage
frangio Aug 12, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions contracts/GSN/Context.sol
Original file line number Diff line number Diff line change
@@ -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 changes: 102 additions & 0 deletions contracts/GSN/GSNContext.sol
Original file line number Diff line number Diff line change
@@ -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 changes: 28 additions & 0 deletions contracts/GSN/GSNRecipient.sol
Original file line number Diff line number Diff line change
@@ -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 changes: 188 additions & 0 deletions contracts/GSN/IRelayHub.sol
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions contracts/GSN/IRelayRecipient.sol
Original file line number Diff line number Diff line change
@@ -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;
}
Loading