Commit 0ec1d761 by Nicolás Venturo Committed by Francisco Giordano

Initial GSN support (beta) (#1844)

* Add base Context contract

* Add GSNContext and tests

* Add RelayHub deployment to tests

* Add RelayProvider integration, complete GSNContext tests

* Switch dependency to openzeppelin-gsn-provider

* Add default txfee to provider

* Add basic signing recipient

* Sign more values

* Add comment clarifying RelayHub's msg.data

* Make context constructors internal

* Rename SigningRecipient to GSNRecipientSignedData

* Add ERC20Charge recipients

* Harcode RelayHub address into GSNContext

* Fix Solidity linter errors

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

* Migrate to published @openzeppelin/gsn-helpers

* Silence false-positive compiler warning

* Use GSN helper assertions

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

* Merge ERC20 charge recipients into a single one

* Rename GSNRecipients to Bouncers

* Add GSNBouncerUtils to decouple the bouncers from GSNRecipient

* Add _upgradeRelayHub

* Store RelayHub address using unstructored storage

* Add IRelayHub

* Add _withdrawDeposits to GSNRecipient

* Add relayHub version to recipient

* Make _acceptRelayedCall and _declineRelayedCall easier to use

* Rename GSNBouncerUtils to GSNBouncerBase, make it IRelayRecipient

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

* Fix GSNBouncerERC20Fee, add tests

* Add missing GSNBouncerSignature test

* Override transferFrom in __unstable__ERC20PrimaryAdmin

* Fix gsn dependencies in package.json

* Rhub address slot reduced by 1

* Rename relay hub changed event

* Use released gsn-provider

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

* update package-lock.json

* clear circle cache

* use optimized gsn-provider

* update to latest @openzeppelin/gsn-provider

* replace with gsn dev provider

* remove relay server

* rename arguments in approveFunction

* fix GSNBouncerSignature test

* change gsn txfee

* initialize development provider only once

* update RelayHub interface

* adapt to new IRelayHub.withdraw

* update @openzeppelin/gsn-helpers

* update relayhub singleton address

* fix helper name

* set up gsn provider for coverage too

* lint

* Revert "set up gsn provider for coverage too"

This reverts commit 8a7b5be5f942002710cba148f249cb888ee8e373.

* remove unused code

* add gsn provider to coverage

* move truffle contract options back out

* increase gas limit for coverage

* remove unreachable code

* add more gas for GSNContext test

* fix test suite name

* rename GSNBouncerBase internal API

* remove onlyRelayHub modifier

* add explicit inheritance

* remove redundant event

* update name of bouncers error codes enums

* add basic docs page for gsn contracts

* make gsn directory all caps

* add changelog entry

* lint

* enable test run to fail in coverage
parent e9cd1b5b
......@@ -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))
......
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;
}
}
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;
}
}
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);
}
}
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;
}
= GSN
== Recipient
{{GSNRecipient}}
== Bouncers
{{GSNBouncerERC20Fee}}
{{GSNBouncerSignature}}
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;
}
}
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);
}
}
}
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));
}
}
}
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);
}
}
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()));
}
}
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();
}
}
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);
}
}
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
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -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",
......
#!/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
......
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,
};
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);
});
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));
});
});
});
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 }));
});
});
});
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 });
});
});
});
});
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');
});
});
});
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
},
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment