check tornado withdraw fee

This commit is contained in:
Alexey 2020-10-05 17:22:52 +03:00
parent 5328178d65
commit 0c3c5d1407
9 changed files with 202 additions and 68 deletions

View File

@ -7,7 +7,8 @@ REDIS_URL=redis://127.0.0.1:6379
# without 0x prefix # without 0x prefix
PRIVATE_KEY= PRIVATE_KEY=
# 2.5 means 2.5% # 2.5 means 2.5%
RELAYER_FEE=2.5 REGULAR_TORNADO_WITHDRAW_FEE=2.5
MINING_SERVICE_FEE=2.5
APP_PORT=8000 APP_PORT=8000
# Resubmitter params: # Resubmitter params:

View File

@ -1,5 +1,7 @@
require('dotenv').config() require('dotenv').config()
const jobType = require('./src/jobTypes')
function updateConfig(options) { function updateConfig(options) {
config = Object.assign(config, options) config = Object.assign(config, options)
} }
@ -151,14 +153,15 @@ let config = {
}, },
}, },
}, },
defaultGasPrice: 20,
port: process.env.APP_PORT || 8000, port: process.env.APP_PORT || 8000,
relayerServiceFee: Number(process.env.RELAYER_FEE), tornadoServiceFee: Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE),
maxGasPrice: process.env.MAX_GAS_PRICE || 200, miningServiceFee: Number(process.env.MINING_SERVICE_FEE),
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000, rewardAccount: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000, gasLimits: {
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20, [jobType.TORNADO_WITHDRAW]: 350000,
rewardAccount: '0x0000000000000000000000000000000000000000', [jobType.MINING_REWARD]: 800000,
[jobType.MINING_WITHDRAW]: 800000,
},
} }
module.exports = config module.exports = config

View File

@ -4,7 +4,9 @@
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash", "description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"scripts": { "scripts": {
"server": "node src/server.js", "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 .", "eslint": "eslint --ext .js --ignore-path .gitignore .",
"prettier:check": "npx prettier --check . --config .prettierrc", "prettier:check": "npx prettier --check . --config .prettierrc",
"prettier:fix": "npx prettier --write . --config .prettierrc", "prettier:fix": "npx prettier --write . --config .prettierrc",

View File

@ -4,7 +4,7 @@ const {
getMiningWithdrawInputError, getMiningWithdrawInputError,
} = require('./validator') } = require('./validator')
const { postJob } = require('./queue') const { postJob } = require('./queue')
const { jobType } = require('./utils') const jobType = require('./jobTypes')
async function tornadoWithdraw(req, res) { async function tornadoWithdraw(req, res) {
const inputError = getTornadoWithdrawInputError(req.body) const inputError = getTornadoWithdrawInputError(req.body)

5
src/jobTypes.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = Object.freeze({
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
MINING_REWARD: 'MINING_REWARD',
MINING_WITHDRAW: 'MINING_WITHDRAW',
})

22
src/priceWatcher.js Normal file
View File

@ -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)

View File

@ -3,7 +3,6 @@ const status = require('./status')
const controller = require('./controller') const controller = require('./controller')
const { port } = require('../config') const { port } = require('../config')
const { version } = require('../package.json') const { version } = require('../package.json')
const worker = require('./worker')
const app = express() const app = express()
app.use(express.json()) app.use(express.json())
@ -33,6 +32,5 @@ app.post('/relay', controller.tornadoWithdraw)
app.post('/v1/miningReward', controller.miningReward) app.post('/v1/miningReward', controller.miningReward)
app.post('/v1/miningWithdraw', controller.miningWithdraw) app.post('/v1/miningWithdraw', controller.miningWithdraw)
worker.start()
app.listen(port) app.listen(port)
console.log(`Relayer ${version} started on port ${port}`) console.log(`Relayer ${version} started on port ${port}`)

View File

@ -1,12 +1,6 @@
const { instances, netId } = require('../config') const { instances, netId } = require('../config')
const { poseidon } = require('circomlib') const { poseidon } = require('circomlib')
const { toBN, toChecksumAddress } = require('web3-utils') const { toBN, toChecksumAddress, BN } = require('web3-utils')
const jobType = Object.freeze({
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
MINING_REWARD: 'MINING_REWARD',
MINING_WITHDRAW: 'MINING_WITHDRAW',
})
const sleep = ms => new Promise(res => setTimeout(res, ms)) 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 = { module.exports = {
getInstance, getInstance,
setSafeInterval, setSafeInterval,
poseidonHash2, poseidonHash2,
sleep, sleep,
when, when,
jobType, getArgsForOracle,
fromDecimals,
} }

View File

@ -1,6 +1,6 @@
const fs = require('fs') const fs = require('fs')
const Web3 = require('web3') const Web3 = require('web3')
const { toBN } = require('web3-utils') const { toBN, toWei, fromWei } = require('web3-utils')
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('fixed-merkle-tree')
const Redis = require('ioredis') const Redis = require('ioredis')
const { GasPriceOracle } = require('gas-price-oracle') const { GasPriceOracle } = require('gas-price-oracle')
@ -9,8 +9,21 @@ const tornadoABI = require('../abis/tornadoABI.json')
const miningABI = require('../abis/mining.abi.json') const miningABI = require('../abis/mining.abi.json')
const swapABI = require('../abis/swap.abi.json') const swapABI = require('../abis/swap.abi.json')
const { queue } = require('./queue') const { queue } = require('./queue')
const { poseidonHash2, jobType } = require('./utils') const { poseidonHash2, getInstance, fromDecimals } = require('./utils')
const { rpcUrl, redisUrl, privateKey, updateConfig, swapAddress, minerAddress } = require('../config') const jobType = require('./jobTypes')
const {
netId,
rpcUrl,
redisUrl,
privateKey,
updateConfig,
swapAddress,
minerAddress,
gasLimits,
instances,
tornadoServiceFee,
miningServiceFee,
} = require('../config')
const { TxManager } = require('tx-manager') const { TxManager } = require('tx-manager')
const { Controller } = require('tornado-cash-anonymity-mining') const { Controller } = require('tornado-cash-anonymity-mining')
@ -74,17 +87,48 @@ async function start() {
console.log('Worker started') console.log('Worker started')
} }
function checkFee({ data, type }) { function checkFee({ data }) {
if (type === jobType.TORNADO_WITHDRAW) { if (data.type === jobType.TORNADO_WITHDRAW) {
return checkTornadoFee(data) return checkTornadoFee(data)
} }
return checkMiningFee(data) return checkMiningFee(data)
} }
async function checkTornadoFee({ args, contract }) { 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() 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 }) { 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 // todo: use desired torn/eth rate and compute the same way as in tornado
} }
// may be this looks better function getTxObject({ data }) {
// const isTornadoWithdraw = type === jobType.TORNADO_WITHDRAW let [ABI, contractAddress, value] =
// const ABI = isTornadoWithdraw ? tornadoABI : miningABI data.type === jobType.TORNADO_WITHDRAW
// const contractAddress = isTornadoWithdraw ? data.contract : minerAddress ? [tornadoABI, data.contract, data.args[5]]
// const value = isTornadoWithdraw ? data.args[5] : 0 // refund : [miningABI, minerAddress, 0]
function getTxObject({ data, type }) { const method = data.type !== jobType.MINING_REWARD ? 'withdraw' : 'reward'
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'
const contract = new web3.eth.Contract(ABI, contractAddress) const contract = new web3.eth.Contract(ABI, contractAddress)
const calldata = contract.methods[method](data.proof, ...data.args).encodeABI() const calldata = contract.methods[method](data.proof, ...data.args).encodeABI()
return { return {
value, value,
to: contract, to: contractAddress,
data: calldata, data: calldata,
} }
} }
async function process(job) { 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 { try {
await currentTx if (!jobType[job.data.type]) {
.send() throw new Error(`Unknown job type: ${job.data.type}`)
.on('transactionHash', txHash => { }
updateTxHash(txHash) currentJob = job
updateStatus(status.SENT) await updateStatus(status.ACCEPTED)
}) console.log(`Start processing a new ${job.data.type} job #${job.id}`)
.on('mined', receipt => { await checkFee(job)
console.log('Mined in block', receipt.blockNumber) if (job.data.type !== jobType.TORNADO_WITHDRAW) {
updateStatus(status.MINED) // precheck if root is up to date
}) }
.on('confirmations', updateConfirmations)
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) { } catch (e) {
console.error('Revert', e) console.error(e)
throw new Error(`Revert by smart contract ${e.message}`) throw e
} }
} }
@ -171,4 +213,6 @@ async function updateStatus(status) {
await currentJob.update(currentJob.data) await currentJob.update(currentJob.data)
} }
start()
module.exports = { start, process } module.exports = { start, process }