diff --git a/.env.example b/.env.example index db8d53d..3bd63c5 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,8 @@ REDIS_URL=redis://127.0.0.1:6379 # without 0x prefix PRIVATE_KEY= # 2.5 means 2.5% -RELAYER_FEE=2.5 +REGULAR_TORNADO_WITHDRAW_FEE=2.5 +MINING_SERVICE_FEE=2.5 APP_PORT=8000 # Resubmitter params: diff --git a/config.js b/config.js index abe01cb..89069c6 100644 --- a/config.js +++ b/config.js @@ -1,5 +1,7 @@ require('dotenv').config() +const jobType = require('./src/jobTypes') + function updateConfig(options) { config = Object.assign(config, options) } @@ -151,14 +153,15 @@ let config = { }, }, }, - defaultGasPrice: 20, port: process.env.APP_PORT || 8000, - relayerServiceFee: Number(process.env.RELAYER_FEE), - maxGasPrice: process.env.MAX_GAS_PRICE || 200, - watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000, - pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000, - gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20, - rewardAccount: '0x0000000000000000000000000000000000000000', + tornadoServiceFee: Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE), + miningServiceFee: Number(process.env.MINING_SERVICE_FEE), + rewardAccount: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5', + gasLimits: { + [jobType.TORNADO_WITHDRAW]: 350000, + [jobType.MINING_REWARD]: 800000, + [jobType.MINING_WITHDRAW]: 800000, + }, } module.exports = config diff --git a/package.json b/package.json index aec01de..a2cea8a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "Relayer for Tornado.cash privacy solution. https://tornado.cash", "scripts": { "server": "node src/server.js", - "treeUpdater": "node src/treeWatcher", + "worker": "node src/worker", + "treeWatcher": "node src/treeWatcher", + "priceWatcher": "node src/priceWatcher", "eslint": "eslint --ext .js --ignore-path .gitignore .", "prettier:check": "npx prettier --check . --config .prettierrc", "prettier:fix": "npx prettier --write . --config .prettierrc", diff --git a/src/controller.js b/src/controller.js index b789c15..d1fbdc2 100644 --- a/src/controller.js +++ b/src/controller.js @@ -4,7 +4,7 @@ const { getMiningWithdrawInputError, } = require('./validator') const { postJob } = require('./queue') -const { jobType } = require('./utils') +const jobType = require('./jobTypes') async function tornadoWithdraw(req, res) { const inputError = getTornadoWithdrawInputError(req.body) diff --git a/src/jobTypes.js b/src/jobTypes.js new file mode 100644 index 0000000..4ba40f0 --- /dev/null +++ b/src/jobTypes.js @@ -0,0 +1,5 @@ +module.exports = Object.freeze({ + TORNADO_WITHDRAW: 'TORNADO_WITHDRAW', + MINING_REWARD: 'MINING_REWARD', + MINING_WITHDRAW: 'MINING_WITHDRAW', +}) diff --git a/src/priceWatcher.js b/src/priceWatcher.js new file mode 100644 index 0000000..e52ac9e --- /dev/null +++ b/src/priceWatcher.js @@ -0,0 +1,22 @@ +const Redis = require('ioredis') +const { redisUrl, oracleAddress, oracleRpcUrl } = require('../config') +const { getArgsForOracle, setSafeInterval } = require('./utils') +const redis = new Redis(redisUrl) +const Web3 = require('web3') +const web3 = new Web3(oracleRpcUrl) + +const priceOracleABI = require('../abis/PriceOracle.abi.json') +const oracle = new web3.eth.Contract(priceOracleABI, oracleAddress) +const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle() + +async function main() { + const prices = await oracle.methods.getPricesInETH(tokenAddresses, oneUintAmount).call() + const ethPrices = prices.reduce((acc, price, i) => { + acc[currencyLookup[tokenAddresses[i]]] = price + return acc + }, {}) + await redis.hmset('prices', ethPrices) + console.log('Wrote following prices to redis', ethPrices) +} + +setSafeInterval(main, 30 * 1000) diff --git a/src/server.js b/src/server.js index 2a66f05..2e99007 100644 --- a/src/server.js +++ b/src/server.js @@ -3,7 +3,6 @@ const status = require('./status') const controller = require('./controller') const { port } = require('../config') const { version } = require('../package.json') -const worker = require('./worker') const app = express() app.use(express.json()) @@ -33,6 +32,5 @@ app.post('/relay', controller.tornadoWithdraw) app.post('/v1/miningReward', controller.miningReward) app.post('/v1/miningWithdraw', controller.miningWithdraw) -worker.start() app.listen(port) console.log(`Relayer ${version} started on port ${port}`) diff --git a/src/utils.js b/src/utils.js index bb682fc..becbee8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,12 +1,6 @@ const { instances, netId } = require('../config') const { poseidon } = require('circomlib') -const { toBN, toChecksumAddress } = require('web3-utils') - -const jobType = Object.freeze({ - TORNADO_WITHDRAW: 'TORNADO_WITHDRAW', - MINING_REWARD: 'MINING_REWARD', - MINING_WITHDRAW: 'MINING_WITHDRAW', -}) +const { toBN, toChecksumAddress, BN } = require('web3-utils') const sleep = ms => new Promise(res => setTimeout(res, ms)) @@ -49,11 +43,76 @@ function when(source, event) { }) } +function getArgsForOracle() { + const tokens = instances.netId1 + const tokenAddresses = [] + const oneUintAmount = [] + const currencyLookup = {} + Object.entries(tokens).map(([currency, data]) => { + if (currency !== 'eth') { + tokenAddresses.push(data.tokenAddress) + oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString()) + currencyLookup[data.tokenAddress] = currency + } + }) + return { tokenAddresses, oneUintAmount, currencyLookup } +} + +function fromDecimals(value, decimals) { + value = value.toString() + let ether = value.toString() + const base = new BN('10').pow(new BN(decimals)) + const baseLength = base.toString(10).length - 1 || 1 + + const negative = ether.substring(0, 1) === '-' + if (negative) { + ether = ether.substring(1) + } + + if (ether === '.') { + throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value') + } + + // Split it into a whole and fractional part + const comps = ether.split('.') + if (comps.length > 2) { + throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points') + } + + let whole = comps[0] + let fraction = comps[1] + + if (!whole) { + whole = '0' + } + if (!fraction) { + fraction = '0' + } + if (fraction.length > baseLength) { + throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places') + } + + while (fraction.length < baseLength) { + fraction += '0' + } + + whole = new BN(whole) + fraction = new BN(fraction) + let wei = whole.mul(base).add(fraction) + + if (negative) { + wei = wei.mul(negative) + } + + return new BN(wei.toString(10), 10) +} + module.exports = { getInstance, setSafeInterval, poseidonHash2, sleep, when, - jobType, + getArgsForOracle, + fromDecimals, } diff --git a/src/worker.js b/src/worker.js index 3cecd77..9e5d41d 100644 --- a/src/worker.js +++ b/src/worker.js @@ -1,6 +1,6 @@ const fs = require('fs') const Web3 = require('web3') -const { toBN } = require('web3-utils') +const { toBN, toWei, fromWei } = require('web3-utils') const MerkleTree = require('fixed-merkle-tree') const Redis = require('ioredis') const { GasPriceOracle } = require('gas-price-oracle') @@ -9,8 +9,21 @@ const tornadoABI = require('../abis/tornadoABI.json') const miningABI = require('../abis/mining.abi.json') const swapABI = require('../abis/swap.abi.json') const { queue } = require('./queue') -const { poseidonHash2, jobType } = require('./utils') -const { rpcUrl, redisUrl, privateKey, updateConfig, swapAddress, minerAddress } = require('../config') +const { poseidonHash2, getInstance, fromDecimals } = require('./utils') +const jobType = require('./jobTypes') +const { + netId, + rpcUrl, + redisUrl, + privateKey, + updateConfig, + swapAddress, + minerAddress, + gasLimits, + instances, + tornadoServiceFee, + miningServiceFee, +} = require('../config') const { TxManager } = require('tx-manager') const { Controller } = require('tornado-cash-anonymity-mining') @@ -74,17 +87,48 @@ async function start() { console.log('Worker started') } -function checkFee({ data, type }) { - if (type === jobType.TORNADO_WITHDRAW) { +function checkFee({ data }) { + if (data.type === jobType.TORNADO_WITHDRAW) { return checkTornadoFee(data) } return checkMiningFee(data) } async function checkTornadoFee({ args, contract }) { - console.log('args, contract', args, contract) + const { currency, amount } = getInstance(contract) + const { decimals } = instances[`netId${netId}`][currency] + const [fee, refund] = [args[4], args[5]].map(toBN) const { fast } = await gasPriceOracle.gasPrices() - console.log('fast', fast) + + const ethPrice = await redis.hget('prices', currency) + const expense = toBN(toWei(fast.toString(), 'gwei')).mul(toBN(gasLimits.TORNADO_WITHDRAW)) + const feePercent = toBN(fromDecimals(amount, decimals)) + .mul(toBN(tornadoServiceFee * 1e10)) + .div(toBN(1e10 * 100)) + let desiredFee + switch (currency) { + case 'eth': { + desiredFee = expense.add(feePercent) + break + } + default: { + desiredFee = expense + .add(refund) + .mul(toBN(10 ** decimals)) + .div(toBN(ethPrice)) + desiredFee = desiredFee.add(feePercent) + break + } + } + console.log( + 'sent fee, desired fee, feePercent', + fromWei(fee.toString()), + fromWei(desiredFee.toString()), + fromWei(feePercent.toString()), + ) + if (fee.lt(desiredFee)) { + throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.') + } } async function checkMiningFee({ args }) { @@ -95,61 +139,59 @@ async function checkMiningFee({ args }) { // todo: use desired torn/eth rate and compute the same way as in tornado } -// may be this looks better -// const isTornadoWithdraw = type === jobType.TORNADO_WITHDRAW -// const ABI = isTornadoWithdraw ? tornadoABI : miningABI -// const contractAddress = isTornadoWithdraw ? data.contract : minerAddress -// const value = isTornadoWithdraw ? data.args[5] : 0 // refund -function getTxObject({ data, type }) { - let ABI, - contractAddress, - value = - type === jobType.TORNADO_WITHDRAW - ? [tornadoABI, data.contract, data.args[5]] - : [miningABI, minerAddress, 0] - const method = type !== jobType.MINING_REWARD ? 'withdraw' : 'reward' +function getTxObject({ data }) { + let [ABI, contractAddress, value] = + data.type === jobType.TORNADO_WITHDRAW + ? [tornadoABI, data.contract, data.args[5]] + : [miningABI, minerAddress, 0] + const method = data.type !== jobType.MINING_REWARD ? 'withdraw' : 'reward' const contract = new web3.eth.Contract(ABI, contractAddress) const calldata = contract.methods[method](data.proof, ...data.args).encodeABI() return { value, - to: contract, + to: contractAddress, data: calldata, } } async function process(job) { - if (!jobType[job.data.type]) { - throw new Error(`Unknown job type: ${job.data.type}`) - } - await updateStatus(status.ACCEPTED) - currentJob = job - console.log(`Start processing a new ${job.data.type} job #${job.id}`) - await checkFee(job) - if (job.data.type !== jobType.TORNADO_WITHDRAW) { - // precheck if root is up to date - } - - currentTx = await txManager.createTx(getTxObject(job)) - try { - await currentTx - .send() - .on('transactionHash', txHash => { - updateTxHash(txHash) - updateStatus(status.SENT) - }) - .on('mined', receipt => { - console.log('Mined in block', receipt.blockNumber) - updateStatus(status.MINED) - }) - .on('confirmations', updateConfirmations) + if (!jobType[job.data.type]) { + throw new Error(`Unknown job type: ${job.data.type}`) + } + currentJob = job + await updateStatus(status.ACCEPTED) + console.log(`Start processing a new ${job.data.type} job #${job.id}`) + await checkFee(job) + if (job.data.type !== jobType.TORNADO_WITHDRAW) { + // precheck if root is up to date + } - await updateStatus(status.CONFIRMED) + currentTx = await txManager.createTx(getTxObject(job)) + + try { + await currentTx + .send() + .on('transactionHash', txHash => { + updateTxHash(txHash) + updateStatus(status.SENT) + }) + .on('mined', receipt => { + console.log('Mined in block', receipt.blockNumber) + updateStatus(status.MINED) + }) + .on('confirmations', updateConfirmations) + + await updateStatus(status.CONFIRMED) + } catch (e) { + console.error('Revert', e) + throw new Error(`Revert by smart contract ${e.message}`) + } } catch (e) { - console.error('Revert', e) - throw new Error(`Revert by smart contract ${e.message}`) + console.error(e) + throw e } } @@ -171,4 +213,6 @@ async function updateStatus(status) { await currentJob.update(currentJob.data) } +start() + module.exports = { start, process }