Add correct fee estimation for relayer withdrawal transaction

This commit is contained in:
Theo 2023-06-30 03:47:30 -07:00
parent e92046a7ec
commit 7243803b21

202
cli.js

@ -28,6 +28,7 @@ const gasSpeedPreferences = ['instant', 'fast', 'standard', 'low'];
let web3, let web3,
torPort, torPort,
tornado, tornado,
tornadoProxyAddress,
tornadoContract, tornadoContract,
tornadoInstance, tornadoInstance,
circuit, circuit,
@ -115,7 +116,7 @@ async function printERC20Balance({ address, name, tokenAddress }) {
* @param {Array} depositEvents Array of deposit event objects * @param {Array} depositEvents Array of deposit event objects
* @returns {Object} treeData * @returns {Object} treeData
* @returns {String[]} treeData.leaves Commitment hashes converted to decimals * @returns {String[]} treeData.leaves Commitment hashes converted to decimals
* @returns {@link MerkleTree} treeData.tree Builded merkle tree * @returns {MerkleTree} treeData.tree Builded merkle tree
* @returns {String} treeData.root Merkle tree root * @returns {String} treeData.root Merkle tree root
*/ */
function computeDepositEventsTree(depositEvents) { function computeDepositEventsTree(depositEvents) {
@ -155,28 +156,39 @@ async function submitTransaction(signedTX) {
}); });
} }
/**
* Estimate gas for created transaction
* @typedef {string} EthAddress Wallet (account) address
* @param {EthAddress} from Transaction sender
* @param {EthAddress} to Transaction recipient
* @param {Number} nonce Account nonce (transactions count on sender account)
* @param {string} encodedData Encoded ABI of transaction
* @param {Number} value Amount of funds to send in this transaction
* @returns {Promise<Number>} current acceptable gas limit to send this transaction
*/
async function estimateGas(from, to, nonce, encodedData, value = 0) {
const fetchedGas = await web3.eth.estimateGas({
from,
to,
value,
nonce,
data: encodedData
});
const bumped = Math.floor(fetchedGas * 1.3);
return bumped;
}
async function generateTransaction(to, encodedData, value = 0) { async function generateTransaction(to, encodedData, value = 0) {
const nonce = await web3.eth.getTransactionCount(senderAccount); const nonce = await web3.eth.getTransactionCount(senderAccount);
let gasPrice = await fetchGasPrice(); let gasPrice = await fetchGasPrice();
let gasLimit; let gasLimit;
async function estimateGas() {
const fetchedGas = await web3.eth.estimateGas({
from: senderAccount,
to: to,
value: value,
nonce: nonce,
data: encodedData
});
const bumped = Math.floor(fetchedGas * 1.3);
return web3.utils.toHex(bumped);
}
if (encodedData) { if (encodedData) {
gasLimit = await estimateGas(); gasLimit = await estimateGas(senderAccount, to, nonce, encodedData, value);
} else { } else {
gasLimit = web3.utils.toHex(23000); gasLimit = 23000;
} }
gasLimit = web3.utils.toHex(gasLimit);
const isNumRString = typeof value == 'string' || typeof value == 'number'; const isNumRString = typeof value == 'string' || typeof value == 'number';
const valueCost = isNumRString ? toBN(value) : value; const valueCost = isNumRString ? toBN(value) : value;
@ -334,7 +346,7 @@ async function deposit({ currency, amount, commitmentNote }) {
await printETHBalance({ address: senderAccount, name: 'Sender account' }); await printETHBalance({ address: senderAccount, name: 'Sender account' });
const value = isTestRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 }); const value = isTestRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 });
console.log('Submitting deposit transaction'); console.log('Submitting deposit transaction');
await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI(), value); await generateTransaction(tornadoProxyAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI(), value);
await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract' }); await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract' });
await printETHBalance({ address: senderAccount, name: 'Sender account' }); await printETHBalance({ address: senderAccount, name: 'Sender account' });
} else { } else {
@ -356,7 +368,7 @@ async function deposit({ currency, amount, commitmentNote }) {
} }
console.log('Submitting deposit transaction'); console.log('Submitting deposit transaction');
await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI()); await generateTransaction(tornadoProxyAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI());
await printERC20Balance({ address: tornadoContract._address, name: 'Tornado contract' }); await printERC20Balance({ address: tornadoContract._address, name: 'Tornado contract' });
await printERC20Balance({ address: senderAccount, name: 'Sender account' }); await printERC20Balance({ address: senderAccount, name: 'Sender account' });
} }
@ -366,11 +378,21 @@ async function deposit({ currency, amount, commitmentNote }) {
} }
} }
/**
* @typedef {Object} MerkleProof Use pregenerated merkle proof
* @property {string} root Merkle tree root
* @property {Array<number|string>} pathElements Number of hashes and hashes
* @property {Array<number>} pathIndices Indicies
*/
/** /**
* Generate merkle tree for a deposit. * Generate merkle tree for a deposit.
* Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf * Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
* in it and generates merkle proof * in it and generates merkle proof
* @param deposit Deposit object * @param {Object} deposit Deposit object
* @param {string} currency Currency ticker, like 'ETH' or 'BNB'
* @param {number} amount Tornado instance amount, like 0.1 (ETH or BNB) or 10
* @return {MerkleProof} Calculated valid merkle tree (proof)
*/ */
async function generateMerkleProof(deposit, currency, amount) { async function generateMerkleProof(deposit, currency, amount) {
// Get all deposit events from smart contract and assemble merkle tree from them // Get all deposit events from smart contract and assemble merkle tree from them
@ -401,16 +423,26 @@ async function generateMerkleProof(deposit, currency, amount) {
} }
/** /**
* Generate SNARK proof for withdrawal * @typedef {Object} ProofData
* @param deposit Deposit object * @property {string} proof - ZK-SNARK proof
* @param recipient Funds recipient * @property {Array<string>} args - Withdrawal transaction proofed arguments
* @param relayer Relayer address
* @param fee Relayer fee
* @param refund Receive ether for exchanged tokens
*/ */
async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
/**
* Generate SNARK proof for withdrawal
* @param {Object} args Arguments
* @param {Object} args.deposit Deposit object
* @param {string} args.recipient Funds recipient
* @param {string | 0 } args.relayer Relayer address
* @param {number} args.fee Relayer fee
* @param {number} args.refund Receive ether for exchanged tokens
* @param {MerkleProof} [args.merkleProof] Valid merkle tree proof
* @returns {Promise<ProofData>} Proof data
*/
async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0, merkleProof }) {
// Compute merkle proof of our commitment // Compute merkle proof of our commitment
const { root, pathElements, pathIndices } = await generateMerkleProof(deposit, currency, amount); if (merkleProof === undefined) merkleProof = await generateMerkleProof(deposit, currency, amount);
const { root, pathElements, pathIndices } = merkleProof;
// Prepare circuit input // Prepare circuit input
const input = { const input = {
@ -484,38 +516,47 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
const gasPrice = await fetchGasPrice(); const gasPrice = await fetchGasPrice();
const decimals = isTestRPC ? 18 : config.deployments[`netId${netId}`]['tokens'][currency].decimals; const decimals = isTestRPC ? 18 : config.deployments[`netId${netId}`]['tokens'][currency].decimals;
const fee = calculateFee({
currency, const merkleWithdrawalProof = await generateMerkleProof(deposit, currency, amount);
gasPrice,
amount, async function calculateDataForRelayer(gasLimit) {
refund, const { desiredFee: totalFee, feePercent: relayerFee } = calculateRelayerWithdrawFee({
ethPrices, currency,
relayerServiceFee: tornadoServiceFee, gasPrice,
decimals amount,
}); refund,
if (fee.gt(fromDecimals({ amount, decimals }))) { ethPrices,
throw new Error('Too high refund'); relayerServiceFee: tornadoServiceFee,
decimals,
gasLimit
});
const { proof, args } = await generateProof({
deposit,
currency,
amount,
recipient,
relayerAddress: rewardAccount,
fee: totalFee,
refund,
merkleProof: merkleWithdrawalProof
});
return { proof, args, totalFee, relayerFee };
} }
const { proof, args } = await generateProof({ const { proof: dummyProof, args: dummyArgs } = await calculateDataForRelayer();
deposit, const realGasLimit = await estimateWithdrawGasLimit({ relayer: rewardAccount, proof: dummyProof, callArgs: dummyArgs });
currency, const { proof, args, totalFee, relayerFee } = await calculateDataForRelayer(realGasLimit);
amount,
recipient,
relayerAddress: rewardAccount,
fee,
refund
});
console.log('Sending withdraw transaction through relay'); console.log('Sending withdraw transaction through relay');
const gasCosts = toBN(gasPrice).mul(toBN(340000)); const gasCosts = toBN(gasPrice).mul(toBN(realGasLimit));
const totalCosts = fee.add(gasCosts);
/** Relayer fee details **/ /** Relayer fee details **/
console.log('Transaction fee: ', rmDecimalBN(fromWei(gasCosts), 12), `${netSymbol}`); console.log('Transaction fee: ', rmDecimalBN(fromWei(gasCosts), 12), `${netSymbol}`);
console.log('Relayer fee: ', rmDecimalBN(fromWei(fee), 12), `${netSymbol}`); console.log('Relayer fee: ', rmDecimalBN(fromWei(relayerFee), 12), `${netSymbol}`);
console.log('Total fees: ', rmDecimalBN(fromWei(totalCosts), 12), `${netSymbol}`); console.log('Total fees: ', rmDecimalBN(fromWei(totalFee), 12), `${netSymbol}`);
/** -------------------- **/ /** -------------------- **/
if (shouldPromptConfirmation) { if (shouldPromptConfirmation) {
@ -558,7 +599,7 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund }); const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund });
console.log('Submitting withdraw transaction'); console.log('Submitting withdraw transaction');
await generateTransaction(contractAddress, tornado.methods.withdraw(tornadoInstance, proof, ...args).encodeABI()); await generateTransaction(tornadoProxyAddress, tornado.methods.withdraw(tornadoInstance, proof, ...args).encodeABI());
} }
if (currency === netSymbol.toLowerCase()) { if (currency === netSymbol.toLowerCase()) {
await printETHBalance({ address: recipient, name: 'Recipient' }); await printETHBalance({ address: recipient, name: 'Recipient' });
@ -860,13 +901,48 @@ async function fetchGasPrice() {
} }
} }
function calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerServiceFee, decimals }) { /**
* Get default gas limit depending on chain ID
* Available chain ID's on Tornado
* @typedef {1 | 5 | 10 | 56 | 100 | 137 | 42161 | 43114 } NetID
* @param {NetID}} netId
* @returns {Number} acceptable withdraw gas limit for selected chain
*/
function getHardcodedWithdrawGasLimit(netId) {
switch (netId) {
case 10:
return 440000;
case 42161:
return 1900000;
default:
return 390000;
}
}
/**
* Estimate gas limit for withdraw transaction via relayer with pregenerated proof
* @typedef {string} EthAddress Wallet (account) address
* @param {object} args Function arguments
* @param {EthAddress} args.relayer Relayer address
* @param {string} args.proof Calculated SNARK proof for withdrawal
* @param {Array<string>} args.callArgs Arguments to call 'withdraw' function from Tornado Proxy contract
* @returns {Promise<Number>} Acceptable gas limit to send withdraw transaction via relayer for selected chain
*/
async function estimateWithdrawGasLimit({ relayer, proof, callArgs }) {
const tx = tornado.methods.withdraw(tornadoInstance, proof, ...callArgs).encodeABI();
const nonce = await web3.eth.getTransactionCount(relayer);
const gasLimit = await estimateGas(relayer, tornadoProxyAddress, nonce, tx, callArgs[5]);
return gasLimit;
}
function calculateRelayerWithdrawFee({ currency, gasPrice, amount, refund, ethPrices, relayerServiceFee, decimals, gasLimit }) {
const decimalsPoint = const decimalsPoint =
Math.floor(relayerServiceFee) === Number(relayerServiceFee) ? 0 : relayerServiceFee.toString().split('.')[1].length; Math.floor(relayerServiceFee) === Number(relayerServiceFee) ? 0 : relayerServiceFee.toString().split('.')[1].length;
const roundDecimal = 10 ** decimalsPoint; const roundDecimal = 10 ** decimalsPoint;
const total = toBN(fromDecimals({ amount, decimals })); const total = toBN(fromDecimals({ amount, decimals }));
const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100)); const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100));
const expense = toBN(gasPrice).mul(toBN(5e5)); const expense = toBN(gasPrice).mul(toBN(gasLimit || getHardcodedWithdrawGasLimit(netId)));
let desiredFee; let desiredFee;
switch (currency) { switch (currency) {
case netSymbol.toLowerCase(): { case netSymbol.toLowerCase(): {
@ -882,7 +958,7 @@ function calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerSe
break; break;
} }
} }
return desiredFee; return { desiredFee, feePercent };
} }
/** /**
@ -1117,7 +1193,8 @@ async function fetchEvents({ type, currency, amount, filterEvents }) {
return Number(result); return Number(result);
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch latest event from thegraph'); console.log(error);
console.error('Failed to fetch latest event from subgraph');
} }
} }
@ -1189,7 +1266,7 @@ async function fetchEvents({ type, currency, amount, filterEvents }) {
} }
async function fetchGraphEvents() { async function fetchGraphEvents() {
console.log('Querying latest events from TheGraph'); console.log('Querying latest events from subgraph');
const latestTimestamp = await queryLatestTimestamp(); const latestTimestamp = await queryLatestTimestamp();
if (latestTimestamp) { if (latestTimestamp) {
const getCachedBlock = await web3.eth.getBlock(startBlock); const getCachedBlock = await web3.eth.getBlock(startBlock);
@ -1222,7 +1299,7 @@ async function fetchEvents({ type, currency, amount, filterEvents }) {
} }
await fetchGraphEvents(); await fetchGraphEvents();
} }
if (!privateRpc && !subgraph && !isTestRPC) { if (!privateRpc && subgraph && !isTestRPC) {
await syncGraphEvents(); await syncGraphEvents();
} else { } else {
await syncEvents(); await syncEvents();
@ -1376,7 +1453,7 @@ async function promptConfirmation() {
* Init web3, contracts, and snark * Init web3, contracts, and snark
*/ */
async function init({ rpc, noteNetId, currency = 'dai', amount = '100', balanceCheck, localMode, privateKey }) { async function init({ rpc, noteNetId, currency = 'dai', amount = '100', balanceCheck, localMode, privateKey }) {
let contractJson, instanceJson, erc20ContractJson, erc20tornadoJson, tornadoAddress, tokenAddress; let contractJson, instanceJson, erc20ContractJson, erc20tornadoJson, tokenAddress;
let ipOptions = {}; let ipOptions = {};
if (noteNetId && !rpc) rpc = config.deployments[`netId${noteNetId}`].defaultRpc; if (noteNetId && !rpc) rpc = config.deployments[`netId${noteNetId}`].defaultRpc;
@ -1473,7 +1550,7 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100', balanceC
} }
if (isTestRPC) { if (isTestRPC) {
tornadoAddress = tornadoProxyAddress =
currency === netSymbol.toLowerCase() ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address; currency === netSymbol.toLowerCase() ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address;
tokenAddress = currency !== netSymbol.toLowerCase() ? erc20ContractJson.networks[netId].address : null; tokenAddress = currency !== netSymbol.toLowerCase() ? erc20ContractJson.networks[netId].address : null;
deployedBlockNumber = 0; deployedBlockNumber = 0;
@ -1484,13 +1561,13 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100', balanceC
currency = netSymbol.toLowerCase(); currency = netSymbol.toLowerCase();
amount = Object.keys(config.deployments[`netId${netId}`][currency].instanceAddress)[0]; amount = Object.keys(config.deployments[`netId${netId}`][currency].instanceAddress)[0];
} }
tornadoAddress = config.deployments[`netId${netId}`].proxy; tornadoProxyAddress = config.deployments[`netId${netId}`].proxy;
multiCall = config.deployments[`netId${netId}`].multicall; multiCall = config.deployments[`netId${netId}`].multicall;
subgraph = config.deployments[`netId${netId}`].subgraph; subgraph = config.deployments[`netId${netId}`].subgraph;
tornadoInstance = config.deployments[`netId${netId}`]['tokens'][currency].instanceAddress[amount]; tornadoInstance = config.deployments[`netId${netId}`]['tokens'][currency].instanceAddress[amount];
deployedBlockNumber = config.deployments[`netId${netId}`]['tokens'][currency].deployedBlockNumber[amount]; deployedBlockNumber = config.deployments[`netId${netId}`]['tokens'][currency].deployedBlockNumber[amount];
if (!tornadoAddress) { if (!tornadoProxyAddress) {
throw new Error(); throw new Error();
} }
tokenAddress = tokenAddress =
@ -1500,9 +1577,8 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100', balanceC
process.exit(1); process.exit(1);
} }
} }
tornado = new web3.eth.Contract(contractJson, tornadoAddress); tornado = new web3.eth.Contract(contractJson, tornadoProxyAddress);
tornadoContract = new web3.eth.Contract(instanceJson, tornadoInstance); tornadoContract = new web3.eth.Contract(instanceJson, tornadoInstance);
contractAddress = tornadoAddress;
erc20 = currency !== netSymbol.toLowerCase() ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {}; erc20 = currency !== netSymbol.toLowerCase() ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {};
erc20Address = tokenAddress; erc20Address = tokenAddress;
} }