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 8a7b5be5f9.
* 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
This commit is contained in:
committed by
Francisco Giordano
parent
e9cd1b5b44
commit
0ec1d761aa
@ -4,6 +4,7 @@
|
||||
|
||||
### 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))
|
||||
|
||||
27
contracts/GSN/Context.sol
Normal file
27
contracts/GSN/Context.sol
Normal file
@ -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
contracts/GSN/GSNContext.sol
Normal file
102
contracts/GSN/GSNContext.sol
Normal file
@ -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
contracts/GSN/GSNRecipient.sol
Normal file
28
contracts/GSN/GSNRecipient.sol
Normal file
@ -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
contracts/GSN/IRelayHub.sol
Normal file
188
contracts/GSN/IRelayHub.sol
Normal file
@ -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
contracts/GSN/IRelayRecipient.sol
Normal file
30
contracts/GSN/IRelayRecipient.sol
Normal file
@ -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
contracts/GSN/README.adoc
Normal file
10
contracts/GSN/README.adoc
Normal file
@ -0,0 +1,10 @@
|
||||
= GSN
|
||||
|
||||
== Recipient
|
||||
|
||||
{{GSNRecipient}}
|
||||
|
||||
== Bouncers
|
||||
|
||||
{{GSNBouncerERC20Fee}}
|
||||
{{GSNBouncerSignature}}
|
||||
92
contracts/GSN/bouncers/GSNBouncerBase.sol
Normal file
92
contracts/GSN/bouncers/GSNBouncerBase.sol
Normal file
@ -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
contracts/GSN/bouncers/GSNBouncerERC20Fee.sol
Normal file
121
contracts/GSN/bouncers/GSNBouncerERC20Fee.sol
Normal file
@ -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
contracts/GSN/bouncers/GSNBouncerSignature.sol
Normal file
51
contracts/GSN/bouncers/GSNBouncerSignature.sol
Normal file
@ -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
contracts/mocks/ContextMock.sol
Normal file
27
contracts/mocks/ContextMock.sol
Normal file
@ -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
contracts/mocks/GSNBouncerERC20FeeMock.sol
Normal file
20
contracts/mocks/GSNBouncerERC20FeeMock.sol
Normal file
@ -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
contracts/mocks/GSNBouncerSignatureMock.sol
Normal file
16
contracts/mocks/GSNBouncerSignatureMock.sol
Normal file
@ -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
contracts/mocks/GSNContextMock.sol
Normal file
46
contracts/mocks/GSNContextMock.sol
Normal file
@ -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
contracts/mocks/GSNRecipientMock.sol
Normal file
25
contracts/mocks/GSNRecipientMock.sol
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
3892
package-lock.json
generated
3892
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
test/GSN/Context.behavior.js
Normal file
42
test/GSN/Context.behavior.js
Normal file
@ -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
test/GSN/Context.test.js
Normal file
15
test/GSN/Context.test.js
Normal file
@ -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
test/GSN/GSNBouncerERC20Fee.test.js
Normal file
69
test/GSN/GSNBouncerERC20Fee.test.js
Normal file
@ -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
test/GSN/GSNBouncerSignature.test.js
Normal file
73
test/GSN/GSNBouncerSignature.test.js
Normal file
@ -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
test/GSN/GSNContext.test.js
Normal file
78
test/GSN/GSNContext.test.js
Normal file
@ -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
test/GSN/GSNRecipient.test.js
Normal file
44
test/GSN/GSNRecipient.test.js
Normal file
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user