diff --git a/data/relayerLosses.txt b/data/relayerLosses.txt new file mode 100644 index 0000000..8dd45bb --- /dev/null +++ b/data/relayerLosses.txt @@ -0,0 +1,4 @@ +0x864DF9CD806D58341f13602103Bf853066ff962a = 625894225496155734516 // ~ 625 TORN +0x5555555731006f71f121144534Ca7C8799F66AA3 = 43970301082908267318 // ~ 43 TORN +0x2Ee39Ff05643bC7cc9ed31B71e142429044A425C = 189640345451160934437 // ~ 189 TORN +0x03392600086874456E08D2bAc104380BCdEBCfC0 = 129757783603193410350 // ~ 129 TORN \ No newline at end of file diff --git a/package.json b/package.json index 40c7245..371e4ec 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,26 @@ { - "name": "forge-proposal-template", + "name": "proposal-33-compensate-relayer-losses", "version": "1.0.0", - "repository": "https://git.tornado.ws/Theo/forge-proposal-template", + "repository": "https://git.tornado.ws/Theo/proposal-33-compensate-relayer-losses", "author": "Theo", "license": "MIT", - "type": "module", "private": false, "scripts": { "init": "cd lib && git clone --recurse-submodules https://github.com/foundry-rs/forge-std", "test:windows": ".\\.env.bat && forge test", "test:linux": ". .env && forge test", "test:gas:windows": ".\\.env.bat && forge test --gas-report", - "test:gas:linux": ". .env && forge test --gas-report" + "test:gas:linux": ". .env && forge test --gas-report", + "calculate": "npx ts-node scripts/calculateLosses.ts" }, "dependencies": { "@ensdomains/ens-contracts": "^0.0.21", "@openzeppelin/contracts": "^4.9.0", - "@openzeppelin/upgrades-core": "^1.26.2" + "@openzeppelin/upgrades-core": "^1.26.2", + "axios": "^1.5.1", + "dotenv": "^16.3.1", + "web3": "^4.2.0", + "web3-utils": "^4.0.7" }, "optionalDependencies": { "@gnosis.pm/ido-contracts": "^0.5.0", diff --git a/scripts/calculateLosses.ts b/scripts/calculateLosses.ts new file mode 100644 index 0000000..2c9de97 --- /dev/null +++ b/scripts/calculateLosses.ts @@ -0,0 +1,67 @@ +import * as dotenv from "dotenv"; +import { Web3 } from "web3"; +import fs from "fs"; +import path from "path"; +import axios from "axios"; +import { RelayerLosses, TxReceipt } from "./types"; +dotenv.config(); + +const web3 = new Web3(process.env.MAINNET_RPC_URL); +const routerAddress = "0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b"; + +async function fetchFailedWithdrawals() { + const attackStartBlock = 18363087; + const attackEndBlock = 18366622; + + let failedWithdrawalTransactions = []; + for (let blockNumber = attackStartBlock; blockNumber <= attackEndBlock; blockNumber++) { + let block = null; + while (block == null) + try { + block = await web3.eth.getBlock(blockNumber); + } catch (e) {} + const results = await Promise.allSettled(block.transactions.map((tx) => web3.eth.getTransactionReceipt(tx as string))); + const transactions = results.filter((r) => r.status === "fulfilled").map((r) => (r as PromiseFulfilledResult).value); + failedWithdrawalTransactions.push(...transactions.filter((tx) => tx.to === routerAddress.toLowerCase() && tx.status == 0)); + } + + return failedWithdrawalTransactions; +} + +async function calculateLosses(failedTransactions: Array): Promise { + return failedTransactions.reduce((acc, tx) => { + const gasExpenses = BigInt(tx.gasUsed) * BigInt(tx.effectiveGasPrice!); + acc[tx.from] = acc[tx.from] ? acc[tx.from] + gasExpenses : gasExpenses; + return acc; + }, {} as RelayerLosses); +} + +async function getEthRateInTorn(): Promise { + const api = "https://api.binance.com/api/v3/ticker/price?symbol="; + const fetchPrice = async (symbol: string) => Number((await axios.get(api + symbol)).data.price); + const tornInUsd = await fetchPrice("TORNBUSD"); + const ethInUsd = await fetchPrice("ETHUSDT"); + + return BigInt(Math.floor(ethInUsd / tornInUsd)); +} + +function writeData(relayerLossesInTorn: RelayerLosses) { + const data = Object.entries(relayerLossesInTorn) + .map(([relayer, amount]) => `${web3.utils.toChecksumAddress(relayer)} = ${amount.toString()} // ~ ${amount / 10n ** 18n} TORN`) + .join("\n"); + fs.writeFileSync(path.join(".", "data", "relayerLosses.txt"), data); +} + +async function main() { + const failedWithdrawals = await fetchFailedWithdrawals(); + const relayerLosses = await calculateLosses(failedWithdrawals); + console.log(relayerLosses); + const ethRateInTorn = await getEthRateInTorn(); + const relayerLossesInTorn = Object.fromEntries( + Object.entries(relayerLosses).map(([relayer, amounthInEth]) => [relayer, amounthInEth * ethRateInTorn]) + ); + writeData(relayerLossesInTorn); + console.log("All data about relayer losses written successfully."); +} + +main(); diff --git a/scripts/types.ts b/scripts/types.ts new file mode 100644 index 0000000..8701e86 --- /dev/null +++ b/scripts/types.ts @@ -0,0 +1,4 @@ +import { Web3Eth } from "web3"; + +export type TxReceipt = Awaited>; +export type RelayerLosses = Record; diff --git a/scripts/utils.ts b/scripts/utils.ts index dd1477d..ff27200 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,34 +1,31 @@ -type NodeVarObject = { [key: string]: string | number }; +type NodeVarObject = { [key: string]: string }; +type NodeVarArray = [string, string]; const solidityCodePadding = " ".repeat(8); const pad = (decl: string, padding: string = solidityCodePadding) => padding + decl + "\n"; class DeclCalculator { - declType!: string | number; - padding!: string; - transformator!: Function; - public constructor( - declType: string | number, - padding: string = solidityCodePadding, - transformator: Function = ( + private declType: string, + private padding: string = solidityCodePadding, + private transformator: Function = ( + () => (x: any) => + x + )(), + private variableNameChanger: Function = ( () => (x: any) => x )() - ) { - this.declType = declType; - this.padding = padding || solidityCodePadding; - this.transformator = transformator; - } + ) {} private displayVariableName(varObj: NodeVarObject) { return Object.keys(varObj)[0]; } - public calculateDecl = (varObj: NodeVarObject, type: string = "bytes32") => { - const solidityVariableName = this.displayVariableName(varObj); - const solidityVariableValue = this.transformator(Object.values(varObj)[0]); - const solidityDeclaration = `${this.declType || type} ${solidityVariableName} = ${solidityVariableValue.toString()};`; + public calculateDecl = (varInfo: NodeVarObject | NodeVarArray, type: string = "bytes32") => { + const solidityVariableName = this.variableNameChanger(Array.isArray(varInfo) ? varInfo[0] : this.displayVariableName(varInfo)); + const solidityVariableValue = this.transformator(Array.isArray(varInfo) ? varInfo[1] : Object.values(varInfo)[0]); + const solidityDeclaration = `${this.declType || type} ${solidityVariableName} = ${solidityVariableValue};`; return pad(solidityDeclaration, this.padding); }; diff --git a/tsconfig.json b/tsconfig.json index 2eb3a35..5569225 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ /* Modules */ "module": "NodeNext" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */