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
pull/1877/head^2
parent
e9cd1b5b44
commit
0ec1d761aa
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
|
@ -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; |
||||
} |
@ -0,0 +1,10 @@ |
||||
= GSN |
||||
|
||||
== Recipient |
||||
|
||||
{{GSNRecipient}} |
||||
|
||||
== Bouncers |
||||
|
||||
{{GSNBouncerERC20Fee}} |
||||
{{GSNBouncerSignature}} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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)); |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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())); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
pragma solidity ^0.5.0; |
||||
|
||||
import "../GSN/GSNRecipient.sol"; |
||||
|
||||
contract GSNRecipientMock is GSNRecipient { |
||||
function withdrawDeposits(uint256 amount, address payable payee) public { |
||||
_withdrawDeposits(amount, payee); |
||||
} |
||||
|
||||
function acceptRelayedCall(address, address, bytes calldata, uint256, uint256, uint256, uint256, bytes calldata, uint256) |
||||
external |
||||
view |
||||
returns (uint256, bytes memory) |
||||
{ |
||||
return (0, ""); |
||||
} |
||||
|
||||
function preRelayedCall(bytes calldata) external returns (bytes32) { |
||||
// solhint-disable-previous-line no-empty-blocks |
||||
} |
||||
|
||||
function postRelayedCall(bytes calldata, bool, uint256, bytes32) external { |
||||
// solhint-disable-previous-line no-empty-blocks |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -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, |
||||
}; |
@ -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); |
||||
}); |
@ -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)); |
||||
}); |
||||
}); |
||||
}); |
@ -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 })); |
||||
}); |
||||
}); |
||||
}); |
@ -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 }); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue