Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions contracts/test/mocks/relayer/RelayerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@ import "../../../test/mocks/common/TimeHelpersMock.sol";


contract RelayerMock is Relayer, TimeHelpersMock {
uint256 public chainId;

function initializeWithChainId(uint256 _monthlyRefundQuota, uint256 _chainId) external onlyInit {
initialized();
startDate = getTimestamp();
monthlyRefundQuota = _monthlyRefundQuota;
setDepositable(true);

chainId = _chainId;
}

function messageHash(address to, uint256 nonce, bytes data, uint256 gasRefund, uint256 gasPrice) public view returns (bytes32) {
return _messageHash(to, nonce, data, gasRefund, gasPrice);
}

function domainSeparator() public view returns (bytes32) {
return _domainSeparator();
function _domainChainId() internal view returns (uint256) {
return chainId == 0 ? 1 : chainId;
}
}
25 changes: 25 additions & 0 deletions lib/relayer/GasPriceOracle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const GAS_STATION_API_URL = 'https://ethgasstation.info/json/ethgasAPI.json'

const DEFAULT_DEVNET_GAS_PRICE = 1e5
const DEFAULT_TESTNET_GAS_PRICE = 1e6

const MAINNET_ID = 1
const TESTNET_IDS = [2, 3, 42] // ropsten, rinkeby and kovan

module.exports = {
async fetch(networkId) {
if (MAINNET_ID === networkId) return this._fetchMainnetGasPrice()
if (TESTNET_IDS.includes(networkId)) return DEFAULT_TESTNET_GAS_PRICE
return DEFAULT_DEVNET_GAS_PRICE
},

async _fetchMainnetGasPrice() {
try {
const axios = require('axios')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well lift this up as a top-level import

const { data: responseData } = await axios.get(GAS_STATION_API_URL)
return (responseData.average / 10) * 1e9
} catch (error) {
throw new Error(`Could not fetch gas price from ETH gas station: ${error}`)
}
}
}
118 changes: 118 additions & 0 deletions lib/relayer/RelayTransactionSigner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const GasPriceOracle = require('./GasPriceOracle')

const DEFAULT_DOMAIN_NAME = 'Aragon Relayer'
const DEFAULT_DOMAIN_VERSION = '1'

const GAS_MULTIPLIER = 1.18

const FIRST_TX_GAS_OVERLOAD = 83500
const NORMAL_TX_GAS_OVERLOAD = 53500

const DATA_TYPES = {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Transaction: [
{ name: 'to', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'data', type: 'bytes' },
{ name: 'gasRefund', type: 'uint256' },
{ name: 'gasPrice', type: 'uint256' }
],
}

module.exports = web3 => class RelayTransactionSigner {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about either placing the web3 instance inside these classes, or extracting the web3 instance used into its own util (e.g. getWeb3()).

Generally we don't run services in the context of a truffle environment (maybe we should? I'm a bit scared at needing to depend on them for the service to stay alive), so there's no global web3 instance available.


It'd also avoid defining a class in a function, which is a bit awkward, since you can have different definitions if you invoke the function more than once. This then starts to violate the instanceof checks and prototype chains, and gets very awkward if you ever need to compare / manipulate more than one object derived from what should be same class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely agree, it should be part of the constructor o at least a known object that resolves that dependency

constructor(relayer, domainName = DEFAULT_DOMAIN_NAME, domainVersion = DEFAULT_DOMAIN_VERSION) {
this.relayer = relayer
this.domainName = domainName
this.domainVersion = domainVersion
}

async signTransaction({ from, to, data, nonce = undefined, gasRefund = undefined, gasPrice = undefined }, service = undefined) {
if (!nonce) nonce = await this._fetchNextNonce(from)
if (!gasPrice) gasPrice = await this._estimateGasPrice()
if (!gasRefund) gasRefund = service
? await this._estimateRelayGasWithService(service, { from, to, data, nonce, gasPrice })
: await this._estimateRelayGasConservatively({ from, to, data, nonce, gasPrice })

const message = { from, to, nonce, data, gasRefund, gasPrice }
const signature = await this.signMessage(message)
return { ...message, signature }
}

async signMessage({ from, to, nonce, data, gasRefund, gasPrice }) {
const message = { to, nonce, data, gasRefund, gasPrice }
const params = { method: 'eth_signTypedData', params: [from, this._typeData(message)], from }
return new Promise((resolve, reject) => {
web3.currentProvider.sendAsync(params, (error, tx) => {
return error ? reject(error) : resolve(tx.result)
})
})
}

async _fetchNextNonce(sender) {
const lastUsedNonce = (await this.relayer.getSender(sender))[1]
return parseInt(lastUsedNonce.toString()) + 1
}

async _estimateRelayGasWithService(service, { from, to, data, nonce, gasPrice }) {
// simulate signature using gas limit as worst case scenario
const gasLimit = await this._gasLimit()
const signature = await this.signMessage({ from, to, nonce, data, gasRefund: gasLimit, gasPrice })
const estimatedGas = await service.estimateGas({ from, to, nonce, data, gasRefund: gasLimit, gasPrice, signature })
return Math.ceil(estimatedGas * GAS_MULTIPLIER)
}

async _estimateRelayGasConservatively({ from, to, data, nonce }) {
const estimatedGas = await this._estimateCallGas({ from, to, data })
const relayOverload = nonce === 1 ? FIRST_TX_GAS_OVERLOAD : NORMAL_TX_GAS_OVERLOAD
return Math.ceil((estimatedGas + relayOverload) * GAS_MULTIPLIER)
}

async _estimateCallGas({ from, to, data }) {
const call = { from, to, data }
return new Promise((resolve, reject) => {
web3.eth.estimateGas(call, (error, response) => {
return error ? reject(error) : resolve(response)
})
})
}

async _estimateGasPrice() {
return GasPriceOracle.fetch(this._networkId())
}

async _gasLimit() {
const block = await this._latestBlock()
return block.gasLimit
}

async _latestBlock() {
return new Promise((resolve, reject) => {
web3.eth.getBlock('latest', (error, response) => {
return error ? reject(error) : resolve(response)
})
})
}

_networkId() {
return parseInt(this.relayer.constructor.network_id)
}

_typeData(message) {
return {
types: DATA_TYPES,
primaryType: 'Transaction',
domain: {
name: this.domainName,
version: this.domainVersion,
chainId: this._networkId(),
verifyingContract: this.relayer.address
},
message: message
}
}
}
85 changes: 85 additions & 0 deletions lib/relayer/RelayerService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const GasPriceOracle = require('./GasPriceOracle')

module.exports = (artifacts, web3) => class RelayerService {
constructor(wallet, relayer) {
this.wallet = wallet
this.relayer = relayer
}

async relay(transaction) {
await this._assertTargetIsAragonApp(transaction)
await this._assertTransactionWontRevert(transaction)
await this._assertTransactionGasCostIsCovered(transaction)
await this._assertTransactionReasonableGasPrice(transaction)

return this._relay(transaction)
}

async estimateGas(transaction) {
return this._estimateGas(transaction)
}

async _relay(transaction) {
const { from, to, nonce, data, gasRefund, gasPrice, signature } = transaction
const txParams = { from: this.wallet, gas: gasRefund, gasPrice }

// console.log(`\nRelaying transaction ${JSON.stringify(transaction)} with params ${JSON.stringify(txParams)}`)
return this.relayer.relay(from, to, nonce, data, gasRefund, gasPrice, signature, txParams)
}

async _assertTargetIsAragonApp({ to }) {
let relayerKernel, aragonAppKernel
try {
relayerKernel = await this.relayer.kernel()
aragonAppKernel = await artifacts.require('AragonApp').at(to).kernel()
} catch (error) {
throw Error(`Could not ensure target address is actually an AragonApp from the same Kernel: ${error}`)
}
if (relayerKernel === aragonAppKernel) return;
throw Error(`The Kernel of the target app ${aragonAppKernel} does not match with the Kernel of the current realyer ${relayerKernel}`)
}

async _assertTransactionWontRevert(transaction) {
const error = await this._transactionWillFail(transaction)
if (!error) return

throw Error(error.message.search(/(revert|invalid opcode|invalid jump)/) > -1
? `Will not relay failing transaction: ${error.message}`
: `Could not estimate gas: ${error.message}`)
}

async _assertTransactionGasCostIsCovered(transaction) {
const { gasRefund } = transaction
const estimatedGas = await this._estimateGas(transaction)
if (gasRefund < estimatedGas) throw Error(`Given gas refund amount ${gasRefund} does not cover transaction gas cost ${estimatedGas}`)
}

async _assertTransactionReasonableGasPrice(transaction) {
const averageGasPrice = await GasPriceOracle.fetch(this._networkId())
if (transaction.gasPrice < averageGasPrice) throw Error(`Given gas price is below the average ${averageGasPrice}`)
}

async _transactionWillFail(transaction) {
try {
await this._estimateGas(transaction)
return false
} catch (error) {
return error
}
}

async _estimateGas({ from, to, nonce, data, gasRefund, gasPrice, signature }) {
const calldata = this.relayer.contract.relay.getData(from, to, nonce, data, gasRefund, gasPrice, signature)
const call = { from: this.wallet, to: this.relayer.address, data: calldata }

return new Promise((resolve, reject) => {
web3.eth.estimateGas(call, (error, response) => {
return error ? reject(error) : resolve(response)
})
})
}

_networkId() {
return this.relayer.constructor.network_id
}
}
35 changes: 0 additions & 35 deletions lib/signTypedData.js

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"eth-ens-namehash": "^2.0.8",
"eth-gas-reporter": "^0.1.1",
"ethereumjs-abi": "^0.6.5",
"ganache-cli": "^6.4.2",
"ganache-cli": "~6.2.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, was the latest version failing due to the gas estimation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, I was facing this issue

"mocha-lcov-reporter": "^1.3.0",
"solidity-coverage": "0.5.8",
"solium": "^1.2.3",
Expand Down
Loading