Compare commits

...

3 Commits

9 changed files with 5109 additions and 100 deletions

View File

@@ -1,38 +1,46 @@
# Governance upgrade to patch exploit # Governance upgrade to patch exploit
### Major changes ### Major changes
1. Adding protection from metamorphic contracts to Governance voting process; 1. Adding protection from metamorphic contracts to Governance voting process;
2. Redeploying Governance Staking proxy contract to nullify bugged rewards; 2. Redeploying Governance Staking proxy contract to nullify bugged rewards;
3. Return of tokens lost due to a bug in Governance Staking; 3. Return of tokens lost due to a bug in Governance Staking;
4. Redeploying Governance Staking logic contract and Relayer Registry logic contract to change the staking address to the current one. 4. Redeploying Governance Staking logic contract and Relayer Registry logic contract to change the staking address to the current one.
### Requirements ### Requirements
- Foundryup ([Windows](https://github.com/altugbakan/foundryup-windows), [Linux](https://book.getfoundry.sh/getting-started/installation)) - Rust ([Any system](https://doc.rust-lang.org/cargo/getting-started/installation.html))
- Node 14 or higher ([Windows](https://github.com/coreybutler/nvm-windows), [Linux](https://github.com/nvm-sh/nvm)) - 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
### Installation
```text
git clone --recurse-submodules https://git.tornado.ws/Theo/proposal-22-governance-and-rewards-patch ```text
cd proposal-22-governance-and-rewards-patch git clone --recurse-submodules https://git.tornado.ws/Theo/proposal-22-governance-and-rewards-patch
npm install cd proposal-22-governance-and-rewards-patch
``` npm install
```
### Testing
### Testing
```text
npm run test ```text
``` npm run test
```
### Contracts info
### Contracts info
The contracts can be currently found here:
The contracts can be currently found here:
- [GovernancePatchUpgrade](https://git.tornado.ws/Theo/proposal-22-governance-and-rewards-patch/src/branch/main/src/v4-patch/GovernancePatchUpgrade.sol)
- [PatchProposal](https://git.tornado.ws/Theo/proposal-22-governance-and-rewards-patch/src/branch/main/src/v4-patch/PatchProposal.sol) - [GovernancePatchUpgrade](https://git.tornado.ws/Theo/proposal-22-governance-and-rewards-patch/src/branch/main/src/v4-patch/GovernancePatchUpgrade.sol)
- [PatchProposal](https://git.tornado.ws/Theo/proposal-22-governance-and-rewards-patch/src/branch/main/src/v4-patch/PatchProposal.sol)
Inlined version of the `RelayerRegistry` and `TornadoStakingRewards` are also used. Check the `diffs` folder to see how much they deviate from the deployed contract implementations.
Inlined version of the `RelayerRegistry` and `TornadoStakingRewards` are also used. Check the `diffs` folder to see how much they deviate from the deployed contract implementations.
For testing resistance against metamorphic contracts, we use the contracts provided by: https://github.com/0age/metamorphic.git
For testing resistance against metamorphic contracts, we use the contracts provided by: https://github.com/0age/metamorphic.git
##### Deployed contracts
- [Governance logic (implementation) contract](https://etherscan.io/address/0xba178126c28f50ee60322a82f5ebcd6b3711e101#code)
- [Staking proxy contract](https://etherscan.io/address/0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29#code)
- [Staking logic (implementation) contract](https://etherscan.io/address/0xefbea4ec481c2467a1a94d94bc54f111f6a7345f#code)
- [Relayer Registry logic (implementation) contract](https://etherscan.io/address/0xe27b91724c55e950f68b394f33fa3b86693179c0#code)

4963
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,11 @@
"author": "Theo", "author": "Theo",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17336117 --no-match-contract TestRelayerBalance", "test": "npm run test:all",
"testWithGas": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17336117 --no-match-contract TestRelayerBalance --gas-report", "test:all": "npm run test:beforeProposed && npm run test:afterProposed",
"test:beforeProposed": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17336117 --no-match-contract TestRelayerBalance",
"test:afterProposed": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17387853 --no-match-contract TestRelayerBalance",
"test:gas": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17336117 --no-match-contract TestRelayerBalance --gas-report",
"relayerBalancesSum": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17304722 --match-contract TestRelayerBalance --gas-report" "relayerBalancesSum": "forge test -vvv --fork-url https://rpc.mevblocker.io --fork-block-number 17304722 --match-contract TestRelayerBalance --gas-report"
}, },
"dependencies": { "dependencies": {

View File

@@ -7,6 +7,11 @@ contract Mock {
address public constant TEST_REAL_ADDRESS_WITH_BALANCE = 0x9Ff3C1Bea9ffB56a78824FE29f457F066257DD58; address public constant TEST_REAL_ADDRESS_WITH_BALANCE = 0x9Ff3C1Bea9ffB56a78824FE29f457F066257DD58;
address public constant TEST_RELAYER_ADDRESS = 0x30F96AEF199B399B722F8819c9b0723016CEAe6C; // moon-relayer.eth (just for testing) address public constant TEST_RELAYER_ADDRESS = 0x30F96AEF199B399B722F8819c9b0723016CEAe6C; // moon-relayer.eth (just for testing)
// Address and private key to test staking, Governance lock and rewards accruals
address public constant TEST_STAKER_ADDRESS = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
uint256 public constant TEST_STAKER_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
// Two test accounts to create proposal and vote for it
uint256 public constant TEST_PRIVATE_KEY_ONE = uint256 public constant TEST_PRIVATE_KEY_ONE =
0x66ddbd7cbe4a566df405f6ded0b908c669f88cdb1656380c050e3a457bd21df0; 0x66ddbd7cbe4a566df405f6ded0b908c669f88cdb1656380c050e3a457bd21df0;
uint256 public constant TEST_PRIVATE_KEY_TWO = uint256 public constant TEST_PRIVATE_KEY_TWO =
@@ -14,6 +19,8 @@ contract Mock {
address public constant TEST_ADDRESS_ONE = 0x118251976c65AFAf291f5255450ddb5b6A4d8B88; address public constant TEST_ADDRESS_ONE = 0x118251976c65AFAf291f5255450ddb5b6A4d8B88;
address public constant TEST_ADDRESS_TWO = 0x63aE7d90Eb37ca39FC62dD9991DbEfeE70673a20; address public constant TEST_ADDRESS_TWO = 0x63aE7d90Eb37ca39FC62dD9991DbEfeE70673a20;
uint256 public constant STAKING_FIX_PROPOSAL_ID = 22;
uint256 public constant ATTACKER_PROPOSAL_ID = 21; // Last attacker proposal (to restore Governance Vault balance) id uint256 public constant ATTACKER_PROPOSAL_ID = 21; // Last attacker proposal (to restore Governance Vault balance) id
uint256 public constant PROPOSAL_VOTING_DURATION = 5 days; uint256 public constant PROPOSAL_VOTING_DURATION = 5 days;

View File

@@ -8,6 +8,7 @@ import { TornadoStakingRewards } from "@root/v4-patch/TornadoStakingRewards.sol"
import { RelayerRegistry } from "@root/v4-patch/RelayerRegistry.sol"; import { RelayerRegistry } from "@root/v4-patch/RelayerRegistry.sol";
import { AdminUpgradeableProxy } from "@root/v4-patch/AdminUpgradeableProxy.sol"; import { AdminUpgradeableProxy } from "@root/v4-patch/AdminUpgradeableProxy.sol";
import { ProposalUtils } from "./ProposalUtils.sol"; import { ProposalUtils } from "./ProposalUtils.sol";
import { Proposal, IGovernance } from "@interfaces/IGovernance.sol";
import { Test } from "@forge-std/Test.sol"; import { Test } from "@forge-std/Test.sol";
@@ -18,12 +19,21 @@ contract MockProposal is Test, ProposalUtils {
} }
modifier executeAttackerProposalBefore() { modifier executeAttackerProposalBefore() {
waitUntilExecutable(ATTACKER_PROPOSAL_ID); if(!getProposal(ATTACKER_PROPOSAL_ID).executed){
governance.execute(ATTACKER_PROPOSAL_ID); waitUntilExecutable(ATTACKER_PROPOSAL_ID);
governance.execute(ATTACKER_PROPOSAL_ID);
}
_; _;
} }
function createAndExecuteProposal() public { function createAndExecuteProposal() public {
// If current proposal already proposed, just wait until executable and execute it
if(hasProposal(STAKING_FIX_PROPOSAL_ID)) {
waitUntilExecutable(STAKING_FIX_PROPOSAL_ID);
governance.execute(STAKING_FIX_PROPOSAL_ID);
return;
}
TornadoStakingRewards governanceStakingImplementation = TornadoStakingRewards governanceStakingImplementation =
new TornadoStakingRewards(_governanceAddress, _tokenAddress, _relayerRegistryAddress); new TornadoStakingRewards(_governanceAddress, _tokenAddress, _relayerRegistryAddress);

View File

@@ -2,26 +2,29 @@
pragma solidity ^0.6.12; pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import { Test } from "@forge-std/Test.sol"; import { Utils } from "./Utils.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC20Permit } from "torn-token/contracts/ERC20Permit.sol";
import { Mock } from "./Mock.sol";
import { Proposal, IGovernance } from "@interfaces/IGovernance.sol"; import { Proposal, IGovernance } from "@interfaces/IGovernance.sol";
import { Parameters } from "@proprietary/Parameters.sol";
import { GovernancePatchUpgrade } from "@root/v4-patch/GovernancePatchUpgrade.sol"; import { GovernancePatchUpgrade } from "@root/v4-patch/GovernancePatchUpgrade.sol";
contract ProposalUtils is Mock, Parameters, Test { contract ProposalUtils is Utils {
GovernancePatchUpgrade internal governance = GovernancePatchUpgrade(payable(_governanceAddress)); GovernancePatchUpgrade internal governance = GovernancePatchUpgrade(payable(_governanceAddress));
function getProposalExecutableTime(uint256 proposalId) internal view returns (uint256) { function getProposalExecutableTime(uint256 proposalId) internal view returns (uint256) {
Proposal memory proposal = IGovernance(_governanceAddress).proposals(proposalId); Proposal memory proposal = getProposal(proposalId);
return proposal.endTime + PROPOSAL_LOCKED_DURATION + 1 hours; return proposal.endTime + PROPOSAL_LOCKED_DURATION + 1 seconds;
}
function getProposal(uint256 proposalId) internal view returns (Proposal memory){
return IGovernance(_governanceAddress).proposals(proposalId);
}
function hasProposal(uint256 proposalId) internal view returns (bool){
return governance.proposalCount() >= proposalId;
} }
function waitUntilExecutable(uint256 proposalId) internal { function waitUntilExecutable(uint256 proposalId) internal {
uint256 proposalExecutableTime = getProposalExecutableTime(proposalId); uint256 proposalExecutableTime = getProposalExecutableTime(proposalId);
require(block.timestamp < proposalExecutableTime, "Too late to execute proposal"); require(block.timestamp < proposalExecutableTime + PROPOSAL_EXECUTION_MAX_DURATION, "Too late to execute proposal");
vm.warp(proposalExecutableTime); vm.warp(proposalExecutableTime);
} }
@@ -52,41 +55,10 @@ contract ProposalUtils is Mock, Parameters, Test {
return proposalId; return proposalId;
} }
function retrieveAndLockBalance(uint256 privateKey, address voter, uint256 amount) internal {
uint256 lockTimestamp = block.timestamp + PROPOSAL_DURATION;
uint256 accountNonce = ERC20Permit(_tokenAddress).nonces(voter);
bytes32 messageHash = keccak256(
abi.encodePacked(
PERMIT_FUNC_SELECTOR,
EIP712_DOMAIN,
keccak256(
abi.encode(
PERMIT_TYPEHASH, voter, _governanceAddress, amount, accountNonce, lockTimestamp
)
)
)
);
/* ----------GOVERNANCE------- */
vm.startPrank(_governanceAddress);
IERC20(_tokenAddress).transfer(voter, amount);
vm.stopPrank();
/* ----------------------------*/
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash);
/* ----------VOTER------------ */
vm.startPrank(voter);
governance.lock(voter, amount, lockTimestamp, v, r, s);
vm.stopPrank();
/* ----------------------------*/
}
function proposeAndExecute(address proposalAddress) public { function proposeAndExecute(address proposalAddress) public {
uint256 proposalId = proposeAndVote(proposalAddress); uint256 proposalId = proposeAndVote(proposalAddress);
waitUntilExecutable(proposalId); waitUntilExecutable(proposalId);
governance.execute(proposalId); IGovernance(_governanceAddress).execute(proposalId);
} }
} }

View File

@@ -36,7 +36,7 @@ contract TestGovernanceStakingRewards is MockProposal {
console2.log( console2.log(
"Accumulated reward per TORN right after proposal execution: %s TORN", "Accumulated reward per TORN right after proposal execution: %s TORN",
accumulatedRewardPerTornBeforeBurning / 10e17 accumulatedRewardPerTornBeforeBurning / _tornDecimals
); );
burnTokens(_governanceAddress, 10_000_000 ether, staking); burnTokens(_governanceAddress, 10_000_000 ether, staking);
@@ -45,7 +45,7 @@ contract TestGovernanceStakingRewards is MockProposal {
console2.log( console2.log(
"Accumulated reward per TORN after burning 10 000 000 TORN: ~ %s TORN", "Accumulated reward per TORN after burning 10 000 000 TORN: ~ %s TORN",
accumulatedRewardPerTornAfterBurning / 10e17 accumulatedRewardPerTornAfterBurning / _tornDecimals
); );
require( require(
@@ -65,18 +65,18 @@ contract TestGovernanceStakingRewards is MockProposal {
uint256 toBurn = 10_000 ether; uint256 toBurn = 10_000 ether;
// Remind that we have locked in Governance 25 000 TORN for TEST_ADDRESS_ONE while voting retrieveAndLockBalance(TEST_STAKER_PRIVATE_KEY, TEST_STAKER_ADDRESS, PROPOSAL_THRESHOLD);
uint256 stakerLockedBalance = governance.lockedBalance(TEST_ADDRESS_ONE); uint256 stakerLockedBalance = governance.lockedBalance(TEST_STAKER_ADDRESS);
require(stakerLockedBalance == 25_000 ether, "Invalid test staker locked balance"); require(stakerLockedBalance == PROPOSAL_THRESHOLD, "Invalid test staker locked balance");
uint256 stakerRewardsBeforeBurning = staking.checkReward(TEST_ADDRESS_ONE); uint256 stakerRewardsBeforeBurning = staking.checkReward(TEST_STAKER_ADDRESS);
console2.log("Staking rewards before burning: %s TORN", stakerRewardsBeforeBurning / 10e17); console2.log("Staking rewards before burning: %s TORN", stakerRewardsBeforeBurning / _tornDecimals);
burnTokens(_governanceAddress, toBurn, staking); burnTokens(_governanceAddress, toBurn, staking);
uint256 stakerRewardsAfterBurning = staking.checkReward(TEST_ADDRESS_ONE); uint256 stakerRewardsAfterBurning = staking.checkReward(TEST_STAKER_ADDRESS);
console2.log( console2.log(
"Staking rewards after burning 10 000 TORN: %s TORN\n", stakerRewardsAfterBurning / 10e17 "Staking rewards after burning 10 000 TORN: %s TORN\n", stakerRewardsAfterBurning / _tornDecimals
); );
require(stakerRewardsAfterBurning > stakerRewardsBeforeBurning, "Rewards isn't changed after burning"); require(stakerRewardsAfterBurning > stakerRewardsBeforeBurning, "Rewards isn't changed after burning");
@@ -85,25 +85,25 @@ contract TestGovernanceStakingRewards is MockProposal {
uint256 receivedReward = stakerRewardsAfterBurning - stakerRewardsBeforeBurning; uint256 receivedReward = stakerRewardsAfterBurning - stakerRewardsBeforeBurning;
uint256 expectedRewards = stakerLockedBalance * toBurn / governanceLockedAmount; uint256 expectedRewards = stakerLockedBalance * toBurn / governanceLockedAmount;
console2.log("Expected staking rewards: %s TORN", expectedRewards / 10e17); console2.log("Expected staking rewards: %s TORN", expectedRewards / _tornDecimals);
console2.log("Staker received rewards: %s TORN\n", receivedReward / 10e17); console2.log("Staker received rewards: %s TORN\n", receivedReward / _tornDecimals);
require(receivedReward == expectedRewards, "Expected and received rewards don't match"); require(receivedReward == expectedRewards, "Expected and received rewards don't match");
uint256 stakerTORNbalanceBeforeGettingRewards = TORN.balanceOf(TEST_ADDRESS_ONE); uint256 stakerTORNbalanceBeforeGettingRewards = TORN.balanceOf(TEST_STAKER_ADDRESS);
console2.log( console2.log(
"Staker balance before getting (withdrawal) collected rewards: %s TORN", "Staker balance before getting (withdrawal) collected rewards: %s TORN",
stakerTORNbalanceBeforeGettingRewards / 10e17 stakerTORNbalanceBeforeGettingRewards / _tornDecimals
); );
vm.startPrank(TEST_ADDRESS_ONE); vm.startPrank(TEST_STAKER_ADDRESS);
staking.getReward(); staking.getReward();
vm.stopPrank(); vm.stopPrank();
uint256 stakerTORNbalanceAfterGettingRewards = TORN.balanceOf(TEST_ADDRESS_ONE); uint256 stakerTORNbalanceAfterGettingRewards = TORN.balanceOf(TEST_STAKER_ADDRESS);
console2.log( console2.log(
"Staker balance after getting (withdrawal) collected rewards: %s TORN", "Staker balance after getting (withdrawal) collected rewards: %s TORN",
stakerTORNbalanceAfterGettingRewards / 10e17 stakerTORNbalanceAfterGettingRewards / _tornDecimals
); );
require( require(

44
test/forge/Utils.sol Normal file
View File

@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC20Permit } from "torn-token/contracts/ERC20Permit.sol";
import { Test } from "@forge-std/Test.sol";
import { Parameters } from "@proprietary/Parameters.sol";
import { Mock } from "./Mock.sol";
import { IGovernance } from "@interfaces/IGovernance.sol";
contract Utils is Parameters, Mock, Test {
function retrieveAndLockBalance(uint256 privateKey, address voter, uint256 amount) public {
uint256 lockTimestamp = block.timestamp + PROPOSAL_DURATION;
uint256 accountNonce = ERC20Permit(_tokenAddress).nonces(voter);
bytes32 messageHash = keccak256(
abi.encodePacked(
PERMIT_FUNC_SELECTOR,
EIP712_DOMAIN,
keccak256(
abi.encode(
PERMIT_TYPEHASH, voter, _governanceAddress, amount, accountNonce, lockTimestamp
)
)
)
);
/* ----------GOVERNANCE------- */
vm.startPrank(_governanceAddress);
IERC20(_tokenAddress).transfer(voter, amount);
vm.stopPrank();
/* ----------------------------*/
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash);
/* ----------VOTER------------ */
vm.startPrank(voter);
IGovernance(_governanceAddress).lock(voter, amount, lockTimestamp, v, r, s);
vm.stopPrank();
/* ----------------------------*/
}
}

View File

@@ -24,6 +24,8 @@ struct Proposal {
interface IGovernance { interface IGovernance {
function proposals(uint256 index) external view returns (Proposal memory); function proposals(uint256 index) external view returns (Proposal memory);
function proposalCount() external view returns (uint256);
function lockedBalance(address account) external view returns (uint256); function lockedBalance(address account) external view returns (uint256);
function propose(address target, string memory description) external returns (uint256); function propose(address target, string memory description) external returns (uint256);