Compare commits

..

4 Commits

8 changed files with 229 additions and 51 deletions

View File

@@ -1,3 +1,34 @@
# proposal-23-restore-rewards
# Proposal 25: restore old rewards
Proposal to restore old rewards value (as before hack) after redeploying Staking contract
Proposal to restore old rewards value (as before hack) after redeploying Staking contract to stakers, who did not have time to withdraw their rewards due to a bug
### Changes:
- Redeploying staking implementation contract to add new function `setReward`
- Restore rewards to 357 stakers (all stakers, who had more than 1 TORN in rewards at the time of the hack and didn't have time to withdraw them)
- Transfer 42 754 TORN from Governance to new Staking contract (so that stakers can withdraw their restored rewards)
### Requirements
- Rust ([Need only for Windows](https://doc.rust-lang.org/cargo/getting-started/installation.html))
- Foundryup ([Windows](https://github.com/altugbakan/foundryup-windows), [Linux](https://book.getfoundry.sh/getting-started/installation))
- Node 14 or higher ([Windows](https://github.com/coreybutler/nvm-windows), [Linux](https://github.com/nvm-sh/nvm))
### Installation
```text
git clone --recurse-submodules https://git.tornado.ws/Theo/proposal-25-restore-rewards
cd proposal-25-restore-rewards
npm install
```
### Testing
```text
npm run test
```
### Contracts
1. New staking implementation contract: [etherscan link](https://etherscan.io/address/0x9c97be37840f0e754bb7adb1b16fd0954a2ba248#code)
2. Proposal contract: [etherscan link](https://etherscan.io/address/0xe3ea2661908d7ebdae01582ad77ab31eebffd365#code)

86
abi/MultiCallABI.json Normal file
View File

@@ -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"
}
]

View File

@@ -0,0 +1 @@
Locked balance of stakers to who we want restore rewards: 392252226884126520706939 (~ 392252.23 TORN)

View File

@@ -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"
}
}

View File

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

View File

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

View File

@@ -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<Array<IStaker>>
const stakingContract = new web3.eth.Contract(StakingAbi as AbiItem[], oldStakingAddress);
const stakersAddresses = await getAddressesStakersWithProssibleRewards();
let stakersWithRewardsBeforeHack: Array<IStaker> = [];
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<IStaker> = stakersWithRewardsBeforeHackRawData.map((reward, index) => ({
address: stakersAddresses[index],
rewardBalance: BigNumber(reward),
}));
return stakersWithRewardsBeforeHack;
// let stakersWithRewardsBeforeHack: Array<IStaker> = [];
// 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<Array<EthAddress>> {
@@ -61,3 +71,30 @@ export async function getStakersWithdrawedAfterHack(): Promise<Array<EthAddress>
return stakersWithdrawedAfterHack;
}
async function useMultiCall(contractAddress: EthAddress, queryArray: Array<Object>, callBlock?: number): Promise<any[]> {
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<BigNumber> {
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<string> = await useMultiCall(
governanceAddress,
stakersToRestoreRewards.map((staker) => governanceContract.methods.lockedBalance(staker.address).encodeABI()),
block
);
return lockedBalances.reduce((acc, cur) => acc.plus(BigNumber(cur)), BigNumber(0));
}