Add GSN support for erc20-to-native bridge mode (#571)
This commit is contained in:
parent
07afe27eb4
commit
f6758222d5
|
@ -11,3 +11,4 @@ deploy/*.env*
|
|||
!deploy/.env.example
|
||||
upgrade/*.env*
|
||||
!upgrade/.env.example
|
||||
.vscode
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue