Add GSN support for erc20-to-native bridge mode (#571)

This commit is contained in:
Leonid Tyurin 2021-05-06 23:31:11 +03:00 committed by GitHub
parent 07afe27eb4
commit f6758222d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 26422 additions and 39207 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ deploy/*.env*
!deploy/.env.example
upgrade/*.env*
!upgrade/.env.example
.vscode

View File

@ -0,0 +1,72 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
import "../upgradeable_contracts/Ownable.sol";
import "./interfaces/GsnTypes.sol";
import "./interfaces/IPaymaster.sol";
import "./interfaces/IRelayHub.sol";
import "./utils/GsnEip712Library.sol";
import "./forwarder/IForwarder.sol";
/**
* Abstract base class to be inherited by a concrete Paymaster
* A subclass must implement:
* - preRelayedCall
* - postRelayedCall
*/
contract BasePaymaster is IPaymaster, Ownable {
IRelayHub internal relayHub;
IForwarder public trustedForwarder;
function getHubAddr() public view returns (address) {
return address(relayHub);
}
//overhead of forwarder verify+signature, plus hub overhead.
uint256 public constant FORWARDER_HUB_OVERHEAD = 50000;
//These parameters are documented in IPaymaster.GasLimits
uint256 public constant PRE_RELAYED_CALL_GAS_LIMIT = 100000;
uint256 public constant POST_RELAYED_CALL_GAS_LIMIT = 110000;
uint256 public constant PAYMASTER_ACCEPTANCE_BUDGET = PRE_RELAYED_CALL_GAS_LIMIT + FORWARDER_HUB_OVERHEAD;
function getGasLimits() external view returns (IPaymaster.GasLimits) {
return
IPaymaster.GasLimits(PAYMASTER_ACCEPTANCE_BUDGET, PRE_RELAYED_CALL_GAS_LIMIT, POST_RELAYED_CALL_GAS_LIMIT);
}
// this method must be called from preRelayedCall to validate that the forwarder
// is approved by the paymaster as well as by the recipient contract.
function _verifyForwarder(GsnTypes.RelayRequest relayRequest) public view {
require(address(trustedForwarder) == relayRequest.relayData.forwarder, "Forwarder is not trusted");
GsnEip712Library.verifyForwarderTrusted(relayRequest);
}
/*
* modifier to be used by recipients as access control protection for preRelayedCall & postRelayedCall
*/
modifier relayHubOnly() {
require(msg.sender == getHubAddr(), "Function can only be called by RelayHub");
_;
}
function setRelayHub(IRelayHub hub) public onlyOwner {
relayHub = hub;
}
function setTrustedForwarder(IForwarder forwarder) public onlyOwner {
trustedForwarder = forwarder;
}
// check current deposit on relay hub.
function getRelayHubDeposit() public view returns (uint256) {
return relayHub.balanceOf(address(this));
}
// withdraw deposit from relayHub
function withdrawRelayHubDepositTo(uint256 amount, address target) public onlyOwner {
relayHub.withdraw(amount, target);
}
}

View File

@ -0,0 +1,57 @@
// SPDX-License-Identifier:MIT
// solhint-disable no-inline-assembly
pragma solidity 0.4.24;
import "./interfaces/IRelayRecipient.sol";
/**
* A base contract to be inherited by any contract that want to receive relayed transactions
* A subclass must use "_msgSender()" instead of "msg.sender"
*/
contract BaseRelayRecipient is IRelayRecipient {
/**
* return the sender of this call.
* if the call came through our trusted forwarder, return the original sender.
* otherwise, return `msg.sender`.
* should be used in the contract anywhere instead of msg.sender
*/
function _msgSender() internal view returns (address ret) {
if (msg.data.length >= 24 && isTrustedForwarder(msg.sender)) {
// At this point we know that the sender is a trusted forwarder,
// so we trust that the last bytes of msg.data are the verified sender address.
// extract sender address from the end of msg.data
assembly {
ret := shr(96, calldataload(sub(calldatasize, 20)))
}
} else {
return msg.sender;
}
}
/**
* return the msg.data of this call.
* if the call came through our trusted forwarder, then the real sender was appended as the last 20 bytes
* of the msg.data - so this method will strip those 20 bytes off.
* otherwise, return `msg.data`
* should be used in the contract instead of msg.data, where the difference matters (e.g. when explicitly
* signing or hashing the
*/
function _msgData() internal view returns (bytes memory ret) {
if (msg.data.length >= 24 && isTrustedForwarder(msg.sender)) {
// At this point we know that the sender is a trusted forwarder,
// we copy the msg.data , except the last 20 bytes (and update the total length)
assembly {
let ptr := mload(0x40)
// copy only size-20 bytes
let size := sub(calldatasize, 20)
// structure RLP data as <offset> <length> <bytes>
mstore(ptr, 0x20)
mstore(add(ptr, 32), size)
calldatacopy(add(ptr, 64), 0, size)
return(ptr, add(size, 64))
}
} else {
return msg.data;
}
}
}

View File

@ -0,0 +1,68 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
contract IForwarder {
struct ForwardRequest {
address from;
address to;
uint256 value;
uint256 gas;
uint256 nonce;
bytes data;
}
function getNonce(address from) external view returns (uint256);
/**
* verify the transaction would execute.
* validate the signature and the nonce of the request.
* revert if either signature or nonce are incorrect.
*/
function verify(
ForwardRequest forwardRequest,
bytes32 domainSeparator,
bytes32 requestTypeHash,
bytes suffixData,
bytes signature
) external view;
/**
* execute a transaction
* @param forwardRequest - all transaction parameters
* @param domainSeparator - domain used when signing this request
* @param requestTypeHash - request type used when signing this request.
* @param suffixData - the extension data used when signing this request.
* @param signature - signature to validate.
*
* the transaction is verified, and then executed.
* the success and ret of "call" are returned.
* This method would revert only verification errors. target errors
* are reported using the returned "success" and ret string
*/
function execute(
ForwardRequest forwardRequest,
bytes32 domainSeparator,
bytes32 requestTypeHash,
bytes suffixData,
bytes signature
) external payable returns (bool success, bytes memory ret);
/**
* Register a new Request typehash.
* @param typeName - the name of the request type.
* @param typeSuffix - anything after the generic params can be empty string (if no extra fields are needed)
* if it does contain a value, then a comma is added first.
*/
function registerRequestType(string typeName, string typeSuffix) external;
/**
* Register a new domain separator.
* The domain separator must have the following fields: name,version,chainId, verifyingContract.
* the chainId is the current network's chainId, and the verifyingContract is this forwarder.
* This method is given the domain name and version to create and register the domain separator value.
* @param name the domain's display name
* @param version the domain/protocol version
*/
function registerDomainSeparator(string name, string version) external;
}

View File

@ -0,0 +1,23 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
import "../forwarder/IForwarder.sol";
contract GsnTypes {
struct RelayData {
uint256 gasPrice;
uint256 pctRelayFee;
uint256 baseRelayFee;
address relayWorker;
address paymaster;
bytes paymasterData;
uint256 clientId;
address forwarder;
}
//note: must start with the ForwardRequest to be an extension of the generic forwarder
struct RelayRequest {
IForwarder.ForwardRequest request;
RelayData relayData;
}
}

View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
interface IKnowForwarderAddress {
/**
* return the forwarder we trust to forward relayed transactions to us.
* the forwarder is required to verify the sender's signature, and verify
* the call is not a replay.
*/
function getTrustedForwarder() external view returns (address);
}

View File

@ -0,0 +1,112 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
import "./GsnTypes.sol";
contract IPaymaster {
/**
* @param acceptanceBudget -
* Paymaster expected gas budget to accept (or reject) a request
* This a gas required by any calculations that might need to reject the
* transaction, by preRelayedCall, forwarder and recipient.
* See value in BasePaymaster.PAYMASTER_ACCEPTANCE_BUDGET
* Transaction that gets rejected above that gas usage is on the paymaster's expense.
* As long this value is above preRelayedCallGasLimit (see defaults in BasePaymaster), the
* Paymaster is guaranteed it will never pay for rejected transactions.
* If this value is below preRelayedCallGasLimt, it might might make Paymaster open to a "griefing" attack.
*
* Specifying value too high might make the call rejected by some relayers.
*
* From a Relay's point of view, this is the highest gas value a paymaster might "grief" the relay,
* since the paymaster will pay anything above that (regardless if the tx reverts)
*
* @param preRelayedCallGasLimit - the max gas usage of preRelayedCall. any revert (including OOG)
* of preRelayedCall is a reject by the paymaster.
* as long as acceptanceBudget is above preRelayedCallGasLimit, any such revert (including OOG)
* is not payed by the paymaster.
* @param postRelayedCallGasLimit - the max gas usage of postRelayedCall.
* note that an OOG will revert the transaction, but the paymaster already committed to pay,
* so the relay will get compensated, at the expense of the paymaster
*/
struct GasLimits {
uint256 acceptanceBudget;
uint256 preRelayedCallGasLimit;
uint256 postRelayedCallGasLimit;
}
/**
* Return the GasLimits constants used by the Paymaster.
*/
function getGasLimits() external view returns (GasLimits memory limits);
/**
* return the relayHub of this contract.
*/
function getHubAddr() public view returns (address);
/**
* Can be used to determine if the contract can pay for incoming calls before making any.
* @return the paymaster's deposit in the RelayHub.
*/
function getRelayHubDeposit() public view returns (uint256);
/**
* Called by Relay (and RelayHub), to validate if the paymaster agrees to pay for this call.
*
* MUST be protected with relayHubOnly() in case it modifies state.
*
* The Paymaster rejects by the following "revert" operations
* - preRelayedCall() method reverts
* - the forwarder reverts because of nonce or signature error
* - the paymaster returned "rejectOnRecipientRevert", and the recipient contract reverted.
* In any of the above cases, all paymaster calls (and recipient call) are reverted.
* In any other case, the paymaster agrees to pay for the gas cost of the transaction (note
* that this includes also postRelayedCall revert)
*
* The rejectOnRecipientRevert flag means the Paymaster "delegate" the rejection to the recipient
* code. It also means the Paymaster trust the recipient to reject fast: both preRelayedCall,
* forwarder check and receipient checks must fit into the GasLimits.acceptanceBudget,
* otherwise the TX is paid by the Paymaster.
*
* @param relayRequest - the full relay request structure
* @param signature - user's EIP712-compatible signature of the {@link relayRequest}.
* Note that in most cases the paymaster shouldn't try use it at all. It is always checked
* by the forwarder immediately after preRelayedCall returns.
* @param approvalData - extra dapp-specific data (e.g. signature from trusted party)
* @param maxPossibleGas - based on values returned from {@link getGasLimits},
* the RelayHub will calculate the maximum possible amount of gas the user may be charged for.
* In order to convert this value to wei, the Paymaster has to call "relayHub.calculateCharge()"
* return:
* a context to be passed to postRelayedCall
* rejectOnRecipientRevert - TRUE if paymaster want to reject the TX if the recipient reverts.
* FALSE means that rejects by the recipient will be completed on chain, and paid by the paymaster.
* (note that in the latter case, the preRelayedCall and postRelayedCall are not reverted).
*/
function preRelayedCall(
GsnTypes.RelayRequest relayRequest,
bytes signature,
bytes approvalData,
uint256 maxPossibleGas
) public returns (bytes memory context, bool rejectOnRecipientRevert);
/**
* This method is called after the actual relayed function call.
* It may be used to record the transaction (e.g. charge the caller by some contract logic) for this call.
*
* MUST be protected with relayHubOnly() in case it modifies state.
*
* @param context - the call context, as returned by the preRelayedCall
* @param success - true if the relayed call succeeded, false if it reverted
* @param gasUseWithoutPost - the actual amount of gas used by the entire transaction, EXCEPT
* the gas used by the postRelayedCall itself.
* @param relayData - the relay params of the request. can be used by relayHub.calculateCharge()
*
* Revert in this functions causes a revert of the client's relayed call (and preRelayedCall(), but the Paymaster
* is still committed to pay the relay for the entire transaction.
*/
function postRelayedCall(bytes context, bool success, uint256 gasUseWithoutPost, GsnTypes.RelayData relayData)
public;
function versionPaymaster() external view returns (string memory);
}

View File

@ -0,0 +1,171 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
import "./GsnTypes.sol";
import "./IStakeManager.sol";
contract IRelayHub {
/// Emitted when a relay server registers or updates its details
/// Looking at these events lets a client discover relay servers
event RelayServerRegistered(
address indexed relayManager,
uint256 baseRelayFee,
uint256 pctRelayFee,
string relayUrl
);
/// Emitted when relays are added by a relayManager
event RelayWorkersAdded(address indexed relayManager, address[] newRelayWorkers, uint256 workersCount);
// Emitted when an account withdraws funds from RelayHub.
event Withdrawn(address indexed account, address indexed dest, uint256 amount);
// Emitted when depositFor is called, including the amount and account that was funded.
event Deposited(address indexed paymaster, address indexed from, uint256 amount);
/// Emitted when an attempt to relay a call fails and Paymaster does not accept the transaction.
/// The actual relayed call was not executed, and the recipient not charged.
/// @param reason contains a revert reason returned from preRelayedCall or forwarder.
event TransactionRejectedByPaymaster(
address indexed relayManager,
address indexed paymaster,
address indexed from,
address to,
address relayWorker,
bytes4 selector,
uint256 innerGasUsed,
bytes 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 manager.
event TransactionRelayed(
address indexed relayManager,
address indexed relayWorker,
address indexed from,
address to,
address paymaster,
bytes4 selector,
RelayCallStatus status,
uint256 charge
);
event TransactionResult(RelayCallStatus status, bytes returnValue);
/// Reason error codes for the TransactionRelayed event
/// @param OK - the transaction was successfully relayed and execution successful - never included in the event
/// @param RelayedCallFailed - the transaction was relayed, but the relayed call failed
/// @param RejectedByPreRelayed - the transaction was not relayed due to preRelatedCall reverting
/// @param RejectedByForwarder - the transaction was not relayed due to forwarder check (signature,nonce)
/// @param PostRelayedFailed - the transaction was relayed and reverted due to postRelatedCall reverting
/// @param PaymasterBalanceChanged - the transaction was relayed and reverted due to the paymaster balance change
enum RelayCallStatus {
OK,
RelayedCallFailed,
RejectedByPreRelayed,
RejectedByForwarder,
RejectedByRecipientRevert,
PostRelayedFailed,
PaymasterBalanceChanged
}
/// Add new worker addresses controlled by sender who must be a staked Relay Manager address.
/// Emits a RelayWorkersAdded event.
/// This function can be called multiple times, emitting new events
function addRelayWorkers(address[] newRelayWorkers) external;
function registerRelayServer(uint256 baseRelayFee, uint256 pctRelayFee, string url) external;
// 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 calling withdraw.
// Emits a Deposited event.
function depositFor(address target) external payable;
// Withdraws from an account's balance, sending it back to it. Relay managers 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 dest) external;
// Relaying
/// Relays a transaction. For this to succeed, multiple conditions must be met:
/// - Paymaster's "acceptRelayCall" method must succeed and not revert
/// - the sender must be a registered Relay Worker that the user signed
/// - the transaction's gas price must be equal or larger than the one that was signed by the sender
/// - the transaction must have enough gas to run all internal transactions if they use all gas available to them
/// - the Paymaster must have enough balance to pay the Relay Worker for the scenario when all gas is spent
///
/// If all conditions are met, the call will be relayed and the recipient charged.
///
/// Arguments:
/// @param relayRequest - all details of the requested relayed call
/// @param signature - client's EIP-712 signature over the relayRequest struct
/// @param approvalData dapp-specific data forwarded to preRelayedCall.
/// This value is *not* verified by the Hub. For example, it can be used to pass a signature to the Paymaster
/// @param externalGasLimit - the value passed as gasLimit to the transaction.
///
/// Emits a TransactionRelayed event.
function relayCall(
uint256 paymasterMaxAcceptanceBudget,
GsnTypes.RelayRequest relayRequest,
bytes signature,
bytes approvalData,
uint256 externalGasLimit
) external returns (bool paymasterAccepted, bytes memory returnValue);
function penalize(address relayWorker, address beneficiary) external;
/// The fee is expressed as a base fee in wei plus percentage on actual charge.
/// E.g. a value of 40 stands for a 40% fee, so the recipient will be
/// charged for 1.4 times the spent amount.
function calculateCharge(uint256 gasUsed, GsnTypes.RelayData relayData) external view returns (uint256);
/* getters */
/// Returns the stake manager of this RelayHub.
function stakeManager() external view returns (IStakeManager);
function penalizer() external view returns (address);
/// Returns an account's deposits. It can be either a deposit of a paymaster, or a revenue of a relay manager.
function balanceOf(address target) external view returns (uint256);
// Minimum stake a relay can have. An attack to the network will never cost less than half this value.
function minimumStake() external view returns (uint256);
// Minimum unstake delay blocks of a relay manager's stake on the StakeManager
function minimumUnstakeDelay() external view returns (uint256);
// Maximum funds that can be deposited at once. Prevents user error by disallowing large deposits.
function maximumRecipientDeposit() external view returns (uint256);
//gas overhead to calculate gasUseWithoutPost
function postOverhead() external view returns (uint256);
// Gas set aside for all relayCall() instructions to prevent unexpected out-of-gas exceptions
function gasReserve() external view returns (uint256);
// maximum number of worker account allowed per manager
function maxWorkerCount() external view returns (uint256);
function workerToManager(address worker) external view returns (address);
function workerCount(address manager) external view returns (uint256);
function isRelayManagerStaked(address relayManager) external view returns (bool);
/**
* @dev the total gas overhead of relayCall(), before the first gasleft() and after the last gasleft().
* Assume that relay has non-zero balance (costs 15'000 more otherwise).
*/
// Gas cost of all relayCall() instructions after actual 'calculateCharge()'
function gasOverhead() external view returns (uint256);
function versionHub() external view returns (string memory);
}

View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
/**
* a contract must implement this interface in order to support relayed transaction.
* It is better to inherit the BaseRelayRecipient as its implementation.
*/
contract IRelayRecipient {
/**
* return if the forwarder is trusted to forward relayed transactions to us.
* the forwarder is required to verify the sender's signature, and verify
* the call is not a replay.
*/
function isTrustedForwarder(address forwarder) public view returns (bool);
/**
* return the sender of this call.
* if the call came through our trusted forwarder, then the real sender is appended as the last 20 bytes
* of the msg.data.
* otherwise, return `msg.sender`
* should be used in the contract anywhere instead of msg.sender
*/
function _msgSender() internal view returns (address);
/**
* return the msg.data of this call.
* if the call came through our trusted forwarder, then the real sender was appended as the last 20 bytes
* of the msg.data - so this method will strip those 20 bytes off.
* otherwise, return `msg.data`
* should be used in the contract instead of msg.data, where the difference matters (e.g. when explicitly
* signing or hashing the
*/
function _msgData() internal view returns (bytes memory);
function versionRecipient() external view returns (string memory);
}

View File

@ -0,0 +1,70 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
contract IStakeManager {
/// Emitted when a stake or unstakeDelay are initialized or increased
event StakeAdded(address indexed relayManager, address indexed owner, uint256 stake, uint256 unstakeDelay);
/// Emitted once a stake is scheduled for withdrawal
event StakeUnlocked(address indexed relayManager, address indexed owner, uint256 withdrawBlock);
/// Emitted when owner withdraws relayManager funds
event StakeWithdrawn(address indexed relayManager, address indexed owner, uint256 amount);
/// Emitted when an authorized Relay Hub penalizes a relayManager
event StakePenalized(address indexed relayManager, address indexed beneficiary, uint256 reward);
event HubAuthorized(address indexed relayManager, address indexed relayHub);
event HubUnauthorized(address indexed relayManager, address indexed relayHub, uint256 removalBlock);
/// @param stake - amount of ether staked for this relay
/// @param unstakeDelay - number of blocks to elapse before the owner can retrieve the stake after calling 'unlock'
/// @param withdrawBlock - first block number 'withdraw' will be callable, or zero if the unlock has not been called
/// @param owner - address that receives revenue and manages relayManager's stake
struct StakeInfo {
uint256 stake;
uint256 unstakeDelay;
uint256 withdrawBlock;
address owner;
}
struct RelayHubInfo {
uint256 removalBlock;
}
/// Put a stake for a relayManager and set its unstake delay.
/// If the entry does not exist, it is created, and the caller of this function becomes its owner.
/// If the entry already exists, only the owner can call this function.
/// @param relayManager - address that represents a stake entry and controls relay registrations on relay hubs
/// @param unstakeDelay - number of blocks to elapse before the owner can retrieve the stake after calling 'unlock'
function stakeForAddress(address relayManager, uint256 unstakeDelay) external payable;
function unlockStake(address relayManager) external;
function withdrawStake(address relayManager) external;
function authorizeHubByOwner(address relayManager, address relayHub) external;
function authorizeHubByManager(address relayHub) external;
function unauthorizeHubByOwner(address relayManager, address relayHub) external;
function unauthorizeHubByManager(address relayHub) external;
function isRelayManagerStaked(address relayManager, address relayHub, uint256 minAmount, uint256 minUnstakeDelay)
external
view
returns (bool);
/// Slash the stake of the relay relayManager. In order to prevent stake kidnapping, burns half of stake on the way.
/// @param relayManager - entry to penalize
/// @param beneficiary - address that receives half of the penalty amount
/// @param amount - amount to withdraw from stake
function penalizeRelayManager(address relayManager, address beneficiary, uint256 amount) external;
function getStakeInfo(address relayManager) external view returns (StakeInfo memory stakeInfo);
function versionSM() external view returns (string memory);
}

View File

@ -0,0 +1,161 @@
pragma solidity 0.4.24;
interface IUniswapV2Router02 {
function factory() external returns (address);
function WETH() external returns (address);
function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);
function addLiquidityETH(
address token,
uint256 amountTokenDesired,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) external returns (uint256 amountToken, uint256 amountETH, uint256 liquidity);
function removeLiquidity(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
) external returns (uint256 amountA, uint256 amountB);
function removeLiquidityETH(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) external returns (uint256 amountToken, uint256 amountETH);
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint256 liquidity,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline,
bool approveMax,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 amountA, uint256 amountB);
function removeLiquidityETHWithPermit(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline,
bool approveMax,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 amountToken, uint256 amountETH);
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
function swapTokensForExactTokens(
uint256 amountOut,
uint256 amountInMax,
address[] path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline)
external
payable
returns (uint256[] memory amounts);
function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] path, address to, uint256 deadline)
external
returns (uint256[] memory amounts);
function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)
external
returns (uint256[] memory amounts);
function swapETHForExactTokens(uint256 amountOut, address[] path, address to, uint256 deadline)
external
returns (uint256[] memory amounts);
function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external returns (uint256 amountB);
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) external returns (uint256 amountOut);
function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) external returns (uint256 amountIn);
function getAmountsOut(uint256 amountIn, address[] path) external view returns (uint256[] memory amounts);
function getAmountsIn(uint256 amountOut, address[] path) external view returns (uint256[] memory amounts);
function removeLiquidityETHSupportingFeeOnTransferTokens(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) external returns (uint256 amountETH);
function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline,
bool approveMax,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256 amountETH);
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] path,
address to,
uint256 deadline
) external;
function swapExactETHForTokensSupportingFeeOnTransferTokens(
uint256 amountOutMin,
address[] path,
address to,
uint256 deadline
) external;
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] path,
address to,
uint256 deadline
) external;
}

View File

@ -0,0 +1,155 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "../BasePaymaster.sol";
import "./IUniswapRouter02.sol";
import "../../upgradeable_contracts/GSNForeignERC20Bridge.sol";
contract TokenPaymaster is BasePaymaster {
address private token;
IUniswapV2Router02 private router;
address private bridge;
address[] private tokenWethPair = new address[](2);
uint256 public postGasUsage = 300000;
constructor(address _relayHub, address _forwarder, address _token, address _router, address _bridge) public {
_setOwner(msg.sender);
relayHub = IRelayHub(_relayHub);
trustedForwarder = IForwarder(_forwarder);
token = _token;
router = IUniswapV2Router02(_router);
bridge = _bridge;
tokenWethPair[0] = token;
tokenWethPair[1] = router.WETH();
}
function setToken(address t) external onlyOwner {
token = t;
}
function setRouter(IUniswapV2Router02 r) external onlyOwner {
router = r;
}
function setBridge(address b) external onlyOwner {
bridge = b;
}
function versionPaymaster() external view returns (string memory) {
return "2.0.0+opengsn.tokengsn.ipaymaster";
}
function() external payable {
require(address(relayHub) != address(0), "relay hub address not set");
relayHub.depositFor.value(msg.value)(address(this));
}
function deposit() external payable {
require(address(relayHub) != address(0), "relay hub address not set");
relayHub.depositFor.value(msg.value)(address(this));
}
function getGasLimits() external view returns (IPaymaster.GasLimits memory limits) {
return
IPaymaster.GasLimits(
PAYMASTER_ACCEPTANCE_BUDGET,
PRE_RELAYED_CALL_GAS_LIMIT,
postGasUsage // maximum postRelayedCall gasLimit
);
}
function erc20() internal view returns (ERC20) {
return ERC20(token);
}
function readBytes32(bytes memory b, uint256 index) internal pure returns (bytes32 res) {
require(b.length >= index + 32, "data too short");
assembly {
res := mload(add(b, add(index, 32)))
}
}
function preRelayedCall(
GsnTypes.RelayRequest relayRequest,
bytes signature,
bytes approvalData,
uint256 maxPossibleGas
) public returns (bytes memory context, bool revertOnRecipientRevert) {
(signature, approvalData);
_verifyForwarder(relayRequest);
bytes memory reqData = relayRequest.request.data;
// Verify that `executeSignaturesGSN` is called
bytes4 functionSelector;
assembly {
functionSelector := mload(add(reqData, 32))
}
require(bridge == relayRequest.request.to, "accepts only bridge calls");
require(functionSelector == GSNForeignERC20Bridge(bridge).executeSignaturesGSN.selector, "not allowed target");
// Get 3rd argument of executeSignaturesGSN, i.e. maxTokensFee
uint256 maxTokensFee = uint256(readBytes32(reqData, 4 + 32 + 32));
uint256 potentialWeiIncome = router.getAmountsOut(maxTokensFee, tokenWethPair)[1];
uint256 maxFee = relayHub.calculateCharge(maxPossibleGas, relayRequest.relayData);
require(potentialWeiIncome >= maxFee, "tokens fee can't cover expenses");
// Recipient should match the sender
uint256 msgIdx = uint256(readBytes32(reqData, 4));
address recipient = address(readBytes32(reqData, 4 + msgIdx + 20));
require(recipient == relayRequest.request.from, "sender does not match recipient");
return (abi.encodePacked(relayRequest.request.from, maxPossibleGas, maxTokensFee), true);
}
function min(uint256 a, uint256 b) private pure returns (uint256) {
return a < b ? a : b;
}
function setPostGasUsage(uint256 gasUsage) external onlyOwner {
postGasUsage = gasUsage;
}
function postRelayedCall(bytes context, bool success, uint256 gasUseWithoutPost, GsnTypes.RelayData relayData)
public
{
(success);
// Extract data from context
address to;
uint256 maxPossibleGas;
uint256 maxTokensFee;
assembly {
to := shr(96, mload(add(context, 32)))
maxPossibleGas := mload(add(context, 52))
maxTokensFee := mload(add(context, 84))
}
// Calculate commission
// We already approved the transaction to use no more than `maxPossibleGas`.
// With `postGasUsage` variable we can regulate the commission that users will take.
// If `postGasUsage` is less than the actual gas usage of the `postRelayedCall`
// the paymaster will lose ETH after each transaction
// If `postGasUsage` is more than the actual gas usage of the `postRelayedCall`
// the paymaster will earn ETH after each transaction
// So, in real case scenario it should be chosen accurately
uint256 chargeWei = relayHub.calculateCharge(min(gasUseWithoutPost + postGasUsage, maxPossibleGas), relayData);
// Uniswap
require(erc20().approve(address(router), maxTokensFee), "approve failed");
// NOTE: Received eth automatically converts to relayhub deposit
uint256 spentTokens = router.swapTokensForExactETH(
chargeWei,
maxTokensFee,
tokenWethPair,
address(this),
block.timestamp
)[0];
// Send rest of tokens to user
if (spentTokens < maxTokensFee) {
require(erc20().transfer(to, maxTokensFee - spentTokens));
}
}
}

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier:MIT
pragma solidity 0.4.24;
pragma experimental ABIEncoderV2;
import "../interfaces/GsnTypes.sol";
import "../interfaces/IRelayRecipient.sol";
import "../forwarder/IForwarder.sol";
/**
* Bridge Library to map GSN RelayRequest into a call of a Forwarder
*/
library GsnEip712Library {
//verify that the recipient trusts the given forwarder
// MUST be called by paymaster
function verifyForwarderTrusted(GsnTypes.RelayRequest relayRequest) internal view {
require(IRelayRecipient(relayRequest.request.to).isTrustedForwarder(relayRequest.relayData.forwarder));
}
}

View File

@ -0,0 +1,36 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.4.24;
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "../gsn/token_paymaster/TokenPaymaster.sol";
contract UniswapRouterMock {
function WETH() external view returns (address) {
return address(this);
}
function getAmountsOut(uint256 amountIn, address[] path) external pure returns (uint256[] memory amounts) {
amounts = new uint256[](2);
amounts[0] = amountIn;
amounts[1] = amountIn / 100;
// 1 wei == 100 tokens
}
function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] path, address to, uint256 deadline)
external
returns (uint256[] memory amounts)
{
uint256 ethToSend = amountOut;
uint256 tokensToTake = ethToSend * 100;
ERC20(path[0]).transferFrom(msg.sender, address(this), tokensToTake);
require(msg.sender.call.value(ethToSend)());
amounts = new uint256[](2);
amounts[0] = tokensToTake;
amounts[1] = ethToSend;
}
function() external payable {}
}

View File

@ -0,0 +1,71 @@
pragma solidity 0.4.24;
import "./BasicForeignBridge.sol";
import "./ERC20Bridge.sol";
import "../gsn/BaseRelayRecipient.sol";
import "../gsn/interfaces/IKnowForwarderAddress.sol";
contract GSNForeignERC20Bridge is BasicForeignBridge, ERC20Bridge, BaseRelayRecipient, IKnowForwarderAddress {
bytes32 internal constant PAYMASTER = 0xfefcc139ed357999ed60c6a013947328d52e7d9751e93fd0274a2bfae5cbcb12; // keccak256(abi.encodePacked("paymaster"))
bytes32 internal constant TRUSTED_FORWARDER = 0x222cb212229f0f9bcd249029717af6845ea3d3a84f22b54e5744ac25ef224c92; // keccak256(abi.encodePacked("trustedForwarder"))
function versionRecipient() external view returns (string memory) {
return "1.0.1";
}
function getTrustedForwarder() external view returns (address) {
return addressStorage[TRUSTED_FORWARDER];
}
function setTrustedForwarder(address _trustedForwarder) public onlyOwner {
addressStorage[TRUSTED_FORWARDER] = _trustedForwarder;
}
function isTrustedForwarder(address forwarder) public view returns (bool) {
return forwarder == addressStorage[TRUSTED_FORWARDER];
}
function setPayMaster(address _paymaster) public onlyOwner {
addressStorage[PAYMASTER] = _paymaster;
}
/**
* @param message same as in `executeSignatures`
* @param signatures same as in `executeSignatures`
* @param maxTokensFee maximum amount of foreign tokens that user allows to take
* as a commission
*/
function executeSignaturesGSN(bytes message, bytes signatures, uint256 maxTokensFee) external {
require(msg.sender == addressStorage[TRUSTED_FORWARDER], "invalid forwarder");
Message.hasEnoughValidSignatures(message, signatures, validatorContract(), false);
address recipient;
uint256 amount;
bytes32 txHash;
address contractAddress;
(recipient, amount, txHash, contractAddress) = Message.parseMessage(message);
if (withinExecutionLimit(amount)) {
require(maxTokensFee <= amount);
require(contractAddress == address(this));
require(!relayedMessages(txHash));
setRelayedMessages(txHash, true);
require(onExecuteMessageGSN(recipient, amount, maxTokensFee));
emit RelayedMessage(recipient, amount, txHash);
} else {
onFailedMessage(recipient, amount, txHash);
}
}
function onExecuteMessageGSN(address recipient, uint256 amount, uint256 fee) internal returns (bool) {
addTotalExecutedPerDay(getCurrentDay(), amount);
// Send maxTokensFee to paymaster
uint256 unshiftMaxFee = _unshiftValue(fee);
bool first = erc20token().transfer(addressStorage[PAYMASTER], unshiftMaxFee);
// Send rest of tokens to user
uint256 unshiftLeft = _unshiftValue(amount - fee);
bool second = erc20token().transfer(recipient, unshiftLeft);
return first && second;
}
}

View File

@ -4,8 +4,9 @@ import "../BasicForeignBridge.sol";
import "../ERC20Bridge.sol";
import "../OtherSideBridgeStorage.sol";
import "./CompoundConnector.sol";
import "../GSNForeignERC20Bridge.sol";
contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideBridgeStorage, CompoundConnector {
contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideBridgeStorage, CompoundConnector, GSNForeignERC20Bridge {
function initialize(
address _validatorContract,
address _erc20token,
@ -73,6 +74,23 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB
claimValues(_token, _to);
}
function onExecuteMessageGSN(address recipient, uint256 amount, uint256 fee) internal returns (bool) {
addTotalExecutedPerDay(getCurrentDay(), amount);
uint256 unshiftMaxFee = _unshiftValue(fee);
uint256 unshiftLeft = _unshiftValue(amount - fee);
ERC20 token = erc20token();
ensureEnoughTokens(token, unshiftMaxFee + unshiftLeft);
// Send maxTokensFee to paymaster
bool first = token.transfer(addressStorage[PAYMASTER], unshiftMaxFee);
// Send rest of tokens to user
bool second = token.transfer(recipient, unshiftLeft);
return first && second;
}
function onExecuteMessage(
address _recipient,
uint256 _amount,
@ -82,14 +100,18 @@ contract ForeignBridgeErcToNative is BasicForeignBridge, ERC20Bridge, OtherSideB
uint256 amount = _unshiftValue(_amount);
ERC20 token = erc20token();
ensureEnoughTokens(token, amount);
return token.transfer(_recipient, amount);
}
function ensureEnoughTokens(ERC20 token, uint256 amount) internal {
uint256 currentBalance = token.balanceOf(address(this));
if (currentBalance < amount) {
uint256 withdrawAmount = (amount - currentBalance).add(minCashThreshold(address(token)));
_withdraw(address(token), withdrawAmount);
}
return token.transfer(_recipient, amount);
}
function onFailedMessage(address, uint256, bytes32) internal {

64234
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,8 @@
"dependencies": {
"@0x/sol-coverage": "^4.0.10",
"@0x/sol-trace": "^3.0.10",
"@opengsn/gsn": "^2.1.0",
"array-flat-polyfill": "^1.0.1",
"openzeppelin-solidity": "1.12.0",
"truffle": "^5.3.2",
"truffle-flattener": "^1.4.2",

View File

@ -304,7 +304,9 @@ contract('ForeignAMB', async accounts => {
bridgeId = web3.utils.soliditySha3(paddedChainId + foreignBridge.address.slice(2)).slice(10, 50)
})
it('call requireToPassMessage(address, bytes, uint256)', async () => {
const tx = await foreignBridge.methods['requireToPassMessage(address,bytes,uint256)'](
const tx = await foreignBridge.methods[
'requireToPassMessage(address,bytes,uint256)'
](
'0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955',
'0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03',
1535604485,

View File

@ -281,7 +281,9 @@ contract('HomeAMB', async accounts => {
bridgeId = web3.utils.soliditySha3(paddedChainId + homeBridge.address.slice(2)).slice(10, 50)
})
it('call requireToPassMessage(address, bytes, uint256)', async () => {
const tx = await homeBridge.methods['requireToPassMessage(address,bytes,uint256)'](
const tx = await homeBridge.methods[
'requireToPassMessage(address,bytes,uint256)'
](
'0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955',
'0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03',
1535604485,
@ -308,7 +310,9 @@ contract('HomeAMB', async accounts => {
})
it('call requireToPassMessage(address, bytes, uint256) should fail', async () => {
// Should fail because gas < minimumGasUsage
await homeBridge.methods['requireToPassMessage(address,bytes,uint256)'](
await homeBridge.methods[
'requireToPassMessage(address,bytes,uint256)'
](
'0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955',
'0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03',
10,
@ -316,7 +320,9 @@ contract('HomeAMB', async accounts => {
).should.be.rejectedWith(ERROR_MSG)
// Should fail because gas > maxGasPerTx
await homeBridge.methods['requireToPassMessage(address,bytes,uint256)'](
await homeBridge.methods[
'requireToPassMessage(address,bytes,uint256)'
](
'0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955',
'0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03',
twoEther,
@ -327,7 +333,9 @@ contract('HomeAMB', async accounts => {
expect(await homeBridge.maxGasPerTx()).to.be.bignumber.equal(ZERO)
// Should fail because maxGasPerTx = 0 so gas > maxGasPerTx
await homeBridge.methods['requireToPassMessage(address,bytes,uint256)'](
await homeBridge.methods[
'requireToPassMessage(address,bytes,uint256)'
](
'0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955',
'0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03',
oneEther,
@ -337,7 +345,9 @@ contract('HomeAMB', async accounts => {
await homeBridge.setMaxGasPerTx(oneEther).should.be.fulfilled
expect(await homeBridge.maxGasPerTx()).to.be.bignumber.equal(oneEther)
await homeBridge.methods['requireToPassMessage(address,bytes,uint256)'](
await homeBridge.methods[
'requireToPassMessage(address,bytes,uint256)'
](
'0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955',
'0xb1591967aed668a4b27645ff40c444892d91bf5951b382995d4d4f6ee3a2ce03',
oneEther,

View File

@ -2,7 +2,7 @@ version: '3.8'
services:
ganache:
image: trufflesuite/ganache-cli
command: --deterministic --gasLimit 20000000 --allowUnlimitedContractSize
command: --deterministic --gasLimit 20000000 --allowUnlimitedContractSize --chainId 1337
ports:
- 8545:8545
compound:

View File

@ -0,0 +1,262 @@
// Required for opengsn proper work
require('array-flat-polyfill')
const ForeignBridge = artifacts.require('ForeignBridgeErcToNative.sol')
const ForeignBridgeErcToNativeMock = artifacts.require('ForeignBridgeErcToNativeMock.sol')
const BridgeValidators = artifacts.require('BridgeValidators.sol')
const ERC677BridgeToken = artifacts.require('ERC677BridgeToken.sol')
const UniswapRouterMock = artifacts.require('UniswapRouterMock.sol')
const TokenPaymaster = artifacts.require('TokenPaymaster.sol')
// GSN
const { RelayProvider } = require('@opengsn/gsn')
const { GsnTestEnvironment } = require('@opengsn/gsn/dist/GsnTestEnvironment')
const { toBN, ERROR_MSG } = require('../setup')
const {
createMessage,
sign,
signatureToVRS,
ether,
packSignatures,
evalMetrics,
paymasterError
} = require('../helpers/helpers')
const requireBlockConfirmations = 8
const gasPrice = web3.utils.toWei('1', 'gwei')
const homeDailyLimit = ether('1001')
const homeMaxPerTx = ether('1000')
const maxPerTx = homeMaxPerTx
const minPerTx = ether('0.01')
const dailyLimit = homeDailyLimit
const ZERO = toBN(0)
const decimalShiftZero = 0
const FIVE_ETHER = ether('5')
const GSNGasLimit = 500000
function createEmptyAccount(relayer) {
const GSNUser = web3.eth.accounts.create()
relayer.addAccount(GSNUser.privateKey)
return GSNUser.address
}
contract('ForeignBridge_ERC20_to_Native_GSN', async accounts => {
let validatorContract
let authorities
let owner
let token
let otherSideBridge
let router
let paymaster
let RelayHubAddress
let ForwarderAddress
before(async () => {
// Deploy GSN
const env = await GsnTestEnvironment.startGsn('localhost')
RelayHubAddress = env.contractsDeployment.relayHubAddress
ForwarderAddress = env.contractsDeployment.forwarderAddress
validatorContract = await BridgeValidators.new()
authorities = [accounts[1], accounts[2]]
owner = accounts[0]
await validatorContract.initialize(1, authorities, owner)
otherSideBridge = await ForeignBridge.new()
})
after(async () => {
await GsnTestEnvironment.stopGsn()
})
describe('#executeSignaturesGSN', async () => {
const BRIDGE_TOKENS = ether('300')
const REQUESTED_TOKENS = BRIDGE_TOKENS
let foreignBridge
let GSNRelayer
let GSNSigner
beforeEach(async () => {
token = await ERC677BridgeToken.new('Some ERC20', 'RSZT', 18)
ForeignBridgeErcToNativeMock.web3.setProvider(web3.currentProvider)
foreignBridge = await ForeignBridgeErcToNativeMock.new()
await foreignBridge.initialize(
validatorContract.address,
token.address,
requireBlockConfirmations,
gasPrice,
[dailyLimit, maxPerTx, minPerTx],
[homeDailyLimit, homeMaxPerTx],
owner,
decimalShiftZero,
otherSideBridge.address
)
router = await UniswapRouterMock.new()
paymaster = await TokenPaymaster.new(
RelayHubAddress,
ForwarderAddress,
token.address,
router.address,
foreignBridge.address
)
await paymaster.setPostGasUsage(250000)
await foreignBridge.setTrustedForwarder(ForwarderAddress)
await foreignBridge.setPayMaster(paymaster.address)
await token.mint(foreignBridge.address, BRIDGE_TOKENS)
// Give Router 1 ether
await web3.eth.sendTransaction({
from: accounts[0],
to: router.address,
value: ether('1')
})
// Give Paymaster 1 ether
await web3.eth.sendTransaction({
from: accounts[0],
to: paymaster.address,
value: ether('1')
})
// GSN configuration
GSNRelayer = await RelayProvider.newProvider({
provider: web3.currentProvider,
config: {
loggerConfigration: {
logLevel: 'debug'
},
paymasterAddress: paymaster.address
}
}).init()
GSNSigner = createEmptyAccount(GSNRelayer)
// From now on all calls will be relayed through GSN.
// If you want to omit GSN specify
// { useGSN: false } in transaction details
ForeignBridgeErcToNativeMock.web3.setProvider(GSNRelayer)
})
it('should allow to executeSignaturesGSN', async () => {
const recipientAccount = GSNSigner
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
const MAX_COMMISSION = FIVE_ETHER
const [
userTokenBalanceBefore,
userEthBalanceBefore,
,
pmTokenBalanceBefore,
pmDepositBalanceBefore,
userTokenBalanceAfter,
,
bridgeTokenBalanceAfter,
pmTokenBalanceAfter,
pmDepositBalanceAfter
] = await evalMetrics(
async () => {
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
const signature = await sign(authorities[0], message)
const oneSignature = packSignatures([signatureToVRS(signature)])
await foreignBridge.executeSignaturesGSN(message, oneSignature, MAX_COMMISSION, {
from: recipientAccount,
gas: GSNGasLimit
})
},
async () => token.balanceOf(recipientAccount),
async () => toBN(await web3.eth.getBalance(recipientAccount)),
async () => token.balanceOf(foreignBridge.address),
async () => token.balanceOf(paymaster.address),
async () => paymaster.getRelayHubDeposit()
)
userEthBalanceBefore.should.be.bignumber.equal(ZERO)
userTokenBalanceBefore.should.be.bignumber.equal(ZERO)
userTokenBalanceAfter.should.be.bignumber.gte(REQUESTED_TOKENS.sub(MAX_COMMISSION))
pmDepositBalanceAfter.should.be.bignumber.gte(pmDepositBalanceBefore)
pmTokenBalanceBefore.should.be.bignumber.equal(ZERO)
pmTokenBalanceAfter.should.be.bignumber.equal(ZERO)
bridgeTokenBalanceAfter.should.be.bignumber.equal(BRIDGE_TOKENS.sub(REQUESTED_TOKENS))
true.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
})
it('should reject insufficient fee', async () => {
const from = createEmptyAccount(GSNRelayer)
const recipientAccount = from
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
const signature = await sign(authorities[0], message)
const oneSignature = packSignatures([signatureToVRS(signature)])
const err = await foreignBridge.executeSignaturesGSN(message, oneSignature, ZERO, {
from: recipientAccount,
gas: GSNGasLimit
}).should.be.rejected
// NOTE: we don't use `err.reason` because after transaction failed
// truffle contract makes eth_call to get error reason and it makes this call
// not through GSN but directly with web3 provider.
// `err.reason` is always the same - 'invalid forwarder'
err.message.should.include(paymasterError("tokens fee can't cover expenses"))
false.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
})
it('should not allow second withdraw (replay attack) with same transactionHash but different recipient', async () => {
// tx 1
const from = createEmptyAccount(GSNRelayer)
const transactionHash = '0x35d3818e50234655f6aebb2a1cfbf30f59568d8a4ec72066fac5a25dbe7b8121'
const message = createMessage(from, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
const signature = await sign(authorities[0], message)
const oneSignature = packSignatures([signatureToVRS(signature)])
false.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
// Check if from not equal tokens reciever
await foreignBridge.executeSignaturesGSN(message, oneSignature, FIVE_ETHER, { from, gas: GSNGasLimit }).should.be
.fulfilled
// tx 2
await token.mint(foreignBridge.address, BRIDGE_TOKENS)
const from2 = createEmptyAccount(GSNRelayer)
const message2 = createMessage(from2, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
const signature2 = await sign(authorities[0], message2)
const oneSignature2 = packSignatures([signatureToVRS(signature2)])
true.should.be.equal(await foreignBridge.relayedMessages(transactionHash))
const pmDepositBefore = await paymaster.getRelayHubDeposit()
await foreignBridge.executeSignaturesGSN(message2, oneSignature2, FIVE_ETHER, { from: from2, gas: GSNGasLimit })
.should.be.rejected
const pmDepositAfter = await paymaster.getRelayHubDeposit()
pmDepositAfter.should.be.bignumber.equal(pmDepositBefore)
})
it('should reject calls to other functions', async () => {
const recipientAccount = createEmptyAccount(GSNRelayer)
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
const signature = await sign(authorities[0], message)
const oneSignature = packSignatures([signatureToVRS(signature)])
const err = await foreignBridge.executeSignatures(message, oneSignature, {
from: recipientAccount,
gas: GSNGasLimit
}).should.be.rejected
err.message.should.include(paymasterError('not allowed target'))
})
it('should reject not GSN calls', async () => {
const recipientAccount = accounts[0]
const transactionHash = '0x1045bfe274b88120a6b1e5d01b5ec00ab5d01098346e90e7c7a3c9b8f0181c80'
const message = createMessage(recipientAccount, REQUESTED_TOKENS, transactionHash, foreignBridge.address)
const signature = await sign(authorities[0], message)
const oneSignature = packSignatures([signatureToVRS(signature)])
await foreignBridge
.executeSignaturesGSN(message, oneSignature, FIVE_ETHER, { from: recipientAccount, useGSN: false })
.should.be.rejectedWith(`${ERROR_MSG} invalid forwarder`)
})
})
})

View File

@ -202,3 +202,18 @@ async function delay(ms) {
}
module.exports.delay = delay
async function evalMetrics(target, ...metrics) {
const before = await Promise.all(metrics.map(metric => metric()))
await target()
const after = await Promise.all(metrics.map(metric => metric()))
return [...before, ...after]
}
module.exports.evalMetrics = evalMetrics
function paymasterError(reason) {
return `paymaster rejected in local view call to 'relayCall()' : ${reason}`
}
module.exports.paymasterError = paymasterError