From 55a6540a13ea23309f1ae8ed94bd351bca442785 Mon Sep 17 00:00:00 2001 From: Theo Date: Mon, 19 Jun 2023 09:05:39 -0700 Subject: [PATCH] Add calculation of stakers (to who we propose to restore rewards) locked balances & use multicall, speed up scripts execution more than 10 times --- abi/MultiCallABI.json | 86 ++++++++++++++++++++++++++++++++ data/stakersLockedBalanceSum.txt | 1 + package.json | 73 +++++++++++++-------------- test/stakersLockedBalanceSum.ts | 21 ++++++++ utils/config.ts | 5 +- utils/stakers.ts | 57 +++++++++++++++++---- 6 files changed, 195 insertions(+), 48 deletions(-) create mode 100644 abi/MultiCallABI.json create mode 100644 data/stakersLockedBalanceSum.txt create mode 100644 test/stakersLockedBalanceSum.ts diff --git a/abi/MultiCallABI.json b/abi/MultiCallABI.json new file mode 100644 index 0000000..d74a0b9 --- /dev/null +++ b/abi/MultiCallABI.json @@ -0,0 +1,86 @@ +[ + { + "constant": true, + "inputs": [], + "name": "getCurrentBlockTimestamp", + "outputs": [{ "name": "timestamp", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "components": [ + { "name": "target", "type": "address" }, + { "name": "callData", "type": "bytes" } + ], + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate", + "outputs": [ + { "name": "blockNumber", "type": "uint256" }, + { "name": "returnData", "type": "bytes[]" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getLastBlockHash", + "outputs": [{ "name": "blockHash", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "addr", "type": "address" }], + "name": "getEthBalance", + "outputs": [{ "name": "balance", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getCurrentBlockDifficulty", + "outputs": [{ "name": "difficulty", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getCurrentBlockGasLimit", + "outputs": [{ "name": "gaslimit", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getCurrentBlockCoinbase", + "outputs": [{ "name": "coinbase", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "blockNumber", "type": "uint256" }], + "name": "getBlockHash", + "outputs": [{ "name": "blockHash", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/data/stakersLockedBalanceSum.txt b/data/stakersLockedBalanceSum.txt new file mode 100644 index 0000000..74565f3 --- /dev/null +++ b/data/stakersLockedBalanceSum.txt @@ -0,0 +1 @@ +Locked balance of stakers to who we want restore rewards: 392252226884126520706939 (~ 392252.23 TORN) \ No newline at end of file diff --git a/package.json b/package.json index ff96b36..a6f628e 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,38 @@ { - "name": "proposal-25-restore-rewards", - "version": "1.0.0", - "description": "Proposal to restore old rewards value (as before hack) after redeploying Staking contract", - "scripts": { - "computeRewards": "npx ts-node scripts/writeStakersData.ts", - "test": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17466009", - "testGas": "forge test -vvv --fork-url https://rpc.mevblocker.io --gas-report" - }, - "repository": { - "type": "git", - "url": "https://git.tornado.ws/Theo/proposal-25-restore-rewards" - }, - "author": "Theo", - "license": "MIT", - "dependencies": { - "@openzeppelin/contracts": "^3.2.0-rc.0", - "@openzeppelin/upgrades-core": "^1.0.1", - "bignumber.js": "^9.1.1", - "dotenv": "^16.1.4", - "torn-token": "^1.0.4", - "web3": "^1.10.0", - "web3-utils": "^1.10.0" - }, - "devDependencies": { - "@nomiclabs/hardhat-ethers": "^2.2.3", - "@nomiclabs/hardhat-waffle": "^2.0.6", - "chai": "^4.3.7", - "ethereum-waffle": "^4.0.10", - "ethers": "^6.5.1", - "@nomicfoundation/hardhat-foundry": "^1.0.1", - "@nomicfoundation/hardhat-verify": "^1.0.1", - "@nomiclabs/hardhat-ethers": "^2.2.3", - "ethers": "^5", - "hardhat": "^2.15.0", - "ts-node": "^10.9.1", - "typescript": "^5.1.3" - } + "name": "proposal-25-restore-rewards", + "version": "1.0.0", + "description": "Proposal to restore old rewards value (as before hack) after redeploying Staking contract", + "scripts": { + "computeRewards": "npx ts-node scripts/writeStakersData.ts", + "test": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17466009", + "testGas": "forge test -vvv --fork-url https://rpc.mevblocker.io --gas-report", + "testStakersLocked": "npx ts-node test/stakersLockedBalanceSum.ts" + }, + "repository": { + "type": "git", + "url": "https://git.tornado.ws/Theo/proposal-25-restore-rewards" + }, + "author": "Theo", + "license": "MIT", + "dependencies": { + "@openzeppelin/contracts": "^3.2.0-rc.0", + "@openzeppelin/upgrades-core": "^1.0.1", + "bignumber.js": "^9.1.1", + "dotenv": "^16.1.4", + "torn-token": "^1.0.4", + "web3": "^1.10.0", + "web3-utils": "^1.10.0" + }, + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-waffle": "^2.0.6", + "chai": "^4.3.7", + "ethereum-waffle": "^4.0.10", + "ethers": "^5", + "@nomicfoundation/hardhat-foundry": "^1.0.1", + "@nomicfoundation/hardhat-verify": "^1.0.1", + "hardhat": "^2.15.0", + "ts-node": "^10.9.1", + "typescript": "^5.1.3" + } } diff --git a/test/stakersLockedBalanceSum.ts b/test/stakersLockedBalanceSum.ts new file mode 100644 index 0000000..325e522 --- /dev/null +++ b/test/stakersLockedBalanceSum.ts @@ -0,0 +1,21 @@ +import fs from "fs"; +import path from "path"; + +import { getStakersLockedBalancesSum } from "../utils/stakers"; +import { currentProposalVotingStartBlock } from "../utils/config"; + +async function main() { + const stakersLockedBalanceSum = await getStakersLockedBalancesSum(currentProposalVotingStartBlock); + + console.log("Locked balance of stakers to who we want restore rewards:", stakersLockedBalanceSum.div(1e18).toFixed(2), "TORN"); + + fs.writeFileSync( + path.join("data", "stakersLockedBalanceSum.txt"), + `Locked balance of stakers to who we want restore rewards: ${stakersLockedBalanceSum.toString(10)} (~ ${stakersLockedBalanceSum + .div(1e18) + .toFixed(2)} TORN)`, + { encoding: "utf-8" } + ); +} + +main(); diff --git a/utils/config.ts b/utils/config.ts index 9be6a51..fe785ce 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -1,5 +1,8 @@ export const governanceAddress = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce"; export const oldStakingAddress = "0x2FC93484614a34f26F7970CBB94615bA109BB4bf"; +export const multicallContractAddress = "0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441"; + +export const currentProposalVotingStartBlock = 17492630; export const governanceDeployedBlock = 11474695; export const governanceRewardsProposalBlock = 14173399; -export const hackBlock = 17299139; \ No newline at end of file +export const hackBlock = 17299139; diff --git a/utils/stakers.ts b/utils/stakers.ts index 96f9b60..56a6f8b 100644 --- a/utils/stakers.ts +++ b/utils/stakers.ts @@ -4,10 +4,11 @@ import { AbiItem } from "web3-utils"; import { EthAddress, IStaker } from "./@types/staker"; import * as dotenv from "dotenv"; -import { governanceAddress, governanceRewardsProposalBlock, hackBlock, oldStakingAddress } from "./config"; +import { governanceAddress, governanceRewardsProposalBlock, hackBlock, oldStakingAddress, multicallContractAddress } from "./config"; import GovernanceAbi from "../abi/GovernanceAbi.json"; import StakingAbi from "../abi/StakingABI.json"; +import MulticallABI from "../abi/MultiCallABI.json"; dotenv.config(); @@ -41,16 +42,25 @@ export async function getStakersWithRewardsBeforeHack(): Promise> const stakingContract = new web3.eth.Contract(StakingAbi as AbiItem[], oldStakingAddress); const stakersAddresses = await getAddressesStakersWithProssibleRewards(); - let stakersWithRewardsBeforeHack: Array = []; - for (const address of stakersAddresses) { - // Check stakers rewards on previous block before hack - const rewardsBeforeHack = await stakingContract.methods.checkReward(address).call({}, hackBlock - 1); - // Discard stakers with rewards less than 1 TORN, because accruing each reward requires paying gas - if (BigNumber(rewardsBeforeHack).div(1e18).isGreaterThan(1)) - stakersWithRewardsBeforeHack.push({ address, rewardBalance: BigNumber(rewardsBeforeHack) }); - } + const multiCallQuery: Array<[string, object]> = stakersAddresses.map((stakerAddress) => + stakingContract.methods.checkReward(stakerAddress).encodeABI() + ); + const stakersWithRewardsBeforeHackRawData = await useMultiCall(oldStakingAddress, multiCallQuery, hackBlock - 1); + const stakersWithRewardsBeforeHack: Array = stakersWithRewardsBeforeHackRawData.map((reward, index) => ({ + address: stakersAddresses[index], + rewardBalance: BigNumber(reward), + })); - return stakersWithRewardsBeforeHack; + // let stakersWithRewardsBeforeHack: Array = []; + // for (const address of stakersAddresses) { + // // Check stakers rewards on previous block before hack + // const rewardsBeforeHack = await stakingContract.methods.checkReward(address).call({}, hackBlock - 1); + // // Discard stakers with rewards less than 1 TORN, because accruing each reward requires paying gas + // if (BigNumber(rewardsBeforeHack).div(1e18).isGreaterThan(1)) + // stakersWithRewardsBeforeHack.push({ address, rewardBalance: BigNumber(rewardsBeforeHack) }); + // } + + return stakersWithRewardsBeforeHack.filter((staker) => staker.rewardBalance.div(1e18).isGreaterThan(1)); } export async function getStakersWithdrawedAfterHack(): Promise> { @@ -61,3 +71,30 @@ export async function getStakersWithdrawedAfterHack(): Promise return stakersWithdrawedAfterHack; } + +async function useMultiCall(contractAddress: EthAddress, queryArray: Array, callBlock?: number): Promise { + const multiCallContract = new web3.eth.Contract(MulticallABI as AbiItem[], multicallContractAddress); + const multicallQueryArray = queryArray.map((query) => [contractAddress, query]); + const { returnData } = await multiCallContract.methods.aggregate(multicallQueryArray).call({}, callBlock); + return returnData; +} + +export async function getStakersLockedBalancesSum(block: number): Promise { + const governanceContract = new web3.eth.Contract(GovernanceAbi as AbiItem[], governanceAddress); + + // All stakers who had more than 1 TORN in rewards at the time of the hack + const stakersWithRewardsBeforeHack = await getStakersWithRewardsBeforeHack(); + // Stakers who withdrew rewards from the time of hack until the balance of the old Staking contract was nullified + const stakersWithdrawedAfterHack = await getStakersWithdrawedAfterHack(); + + // It makes no sense to restore awards to those who already withdrew them from the old Staking contract + const stakersToRestoreRewards = stakersWithRewardsBeforeHack.filter((staker) => !stakersWithdrawedAfterHack.includes(staker.address)); + + const lockedBalances: Array = await useMultiCall( + governanceAddress, + stakersToRestoreRewards.map((staker) => governanceContract.methods.lockedBalance(staker.address).encodeABI()), + block + ); + + return lockedBalances.reduce((acc, cur) => acc.plus(BigNumber(cur)), BigNumber(0)); +}