Compare commits

...

11 Commits

Author SHA1 Message Date
Alexander Drygin 6464973b10
Merge pull request #7 from tornadocash/TC-74-proposal-architecture-docs
TC-74 architecture docs + tests coverage
2022-08-04 14:50:56 +03:00
Drygin 5091c0998d TC-93-proposal-repo-example 2022-08-04 14:40:40 +03:00
Drygin 0b92bdf73f TC-95-coverage-to-CI 2022-08-03 14:33:46 +03:00
Drygin 68b42a2c52 TC-95-tests-coverage 2022-08-03 14:27:08 +03:00
Drygin 1904c9f9c5 TC-93-proposal-creation-manual 2022-08-03 14:10:32 +03:00
Drygin 9083416f44 add correct upgrade link 2022-03-15 11:21:22 +01:00
Drygin debbd60010 add V3 tests 2022-03-15 11:21:22 +01:00
Drygin 74f38d68b0 v1/v2 tests 2022-03-15 11:21:22 +01:00
Drygin 74b6021aa7 add gov v3 2022-03-15 11:21:22 +01:00
Drygin 1b50b703c2 clean v2 changes doc 2022-03-15 11:21:22 +01:00
Drygin 1bee8e29f6 gov v2 refactored 2022-03-15 11:21:22 +01:00
42 changed files with 1866 additions and 7025 deletions

View File

@ -21,6 +21,11 @@ jobs:
- run: yarn install
- run: yarn hardhat compile
- run: yarn lint
- run: yarn coverage
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: yarn hardhat test
- name: Telegram Failure Notification
uses: appleboy/telegram-action@0.0.7

115
.gitignore vendored
View File

@ -1,6 +1,111 @@
node_modules
cache
artifacts
build
dist
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
artifacts
cache
coverage
coverage.json
.vscode

View File

@ -5,4 +5,6 @@ artifacts
build
dist
README.md
contracts/v2-vault-and-gas/libraries/EtherSend.sol
contracts/v2-vault-and-gas/libraries/EtherSend.sol
coverage
coverage.json

3
.solcover.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
skipFiles: [],
}

View File

@ -8,7 +8,8 @@
}
],
"quotes": ["error", "double"],
"indent": ["error", 2]
"indent": ["error", 2],
"compiler-version": ["error", "^0.6.0"]
},
"plugins": ["prettier"]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"solidity.compileUsingRemoteVersion": "v0.6.12+commit.27d51765"
}

View File

@ -1,14 +1,49 @@
# tornado-governance
# Tornado governance [![build status](https://github.com/tornadocash/tornado-governance/actions/workflows/build.yml/badge.svg)](https://github.com/tornadocash/tornado-governance/actions/workflows/build.yml) [![Coverage Status](https://coveralls.io/repos/github/tornadocash/tornado-governance/badge.svg?branch=master)](https://coveralls.io/github/tornadocash/tornado-governance?branch=master)
## Description
This repository holds all the tornado.cash governance upgrade and original governance contracts.
The highest governance version right now is version 2, which includes a gas and vault upgrade.
This repository holds all the tornado.cash governance upgrades and original governance contracts.
## Setup
## Documentation
All high-level documentation can be find [here](https://docs.tornado.cash/general/governance).
## Code architecture
Tornado governance infrastructure consists of two types of repository:
1. **Governance repository** (this one) - contains the original governance contracts and parts of proposals that upgrade governance itself via loopback proxy. So here you can compile the actual version of the governance contract.
2. **Proposal repository** - a separate repository for each governance proposal. It contains the full codebase of a proposal.
### Loopback proxy
[Loopback proxy](https://github.com/tornadocash/tornado-governance/blob/master/contracts/v1/LoopbackProxy.sol) is a special type of proxy contract that is used to add the ability to upgrade the proxy itself. This way governance proposals can upgrade governance implementation.
### Proposal creation manual
To create your custom governance proposal you need to:
1. Create a proposal repository (for [example](https://github.com/Rezan-vm/tornado-relayer-registry)):
- a proposal is executed from the governance contract using delegatecall of __executeProposal()__ method
- as a proposal is executed using delegatecall, it should not store any storage variables - use constants and immutable variables instead
2. If your proposal is upgrading governance itself, you need to create a pull request to the governance repository. PR should add folder with governance contract upgrade (separate folder in contracts folder - for [example](https://github.com/tornadocash/tornado-governance/pull/6/commits/5f36d5744a9f279a58e9ba1f0e0cd9d493af41c7)).
3. Deploy proposal. The proposal must be smart contracts with verified code.
4. Go to Tornado governance [UI](https://tornadocash.eth.limo/governance) to start the proposal voting process.
## Tests/Coverage
Setting up the repository:
```bash
git clone https://github.com/tornadocash/tornado-governance.git
yarn
cp .env.example .env # you must enter your details into .env
yarn test
cp .env.example .env
```
Please fill out .env according to the template provided in it. Please ensure that all of the example values are set to the correct addresses.
To run test scripts:
```bash
yarn test
```
To run tests coverage:
```bash
yarn coverage
```

6
config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
governance: '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
TORN: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
tornWhale: '0xF977814e90dA44bFA03b6295A0616a897441aceC',
forkBlockNumber: 14352372,
}

View File

@ -3,7 +3,7 @@
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import { GovernanceVaultUpgrade } from "../vault/GovernanceVaultUpgrade.sol";
import { GovernanceVaultUpgrade } from "./GovernanceVaultUpgrade.sol";
import { GasCompensator } from "./GasCompensator.sol";
import { Math } from "@openzeppelin/contracts/math/Math.sol";

View File

@ -3,9 +3,9 @@
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import { Governance } from "../../v1/Governance.sol";
import { Governance } from "../v1/Governance.sol";
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
import { ITornadoVault } from "../interfaces/ITornadoVault.sol";
import { ITornadoVault } from "./interfaces/ITornadoVault.sol";
/// @title Version 2 Governance contract of the tornado.cash governance
contract GovernanceVaultUpgrade is Governance {

View File

@ -1,39 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
interface IGovernanceMultisigAddress {
function returnMultisigAddress() external pure returns (address);
}
/**
* @notice Contract which hold governance information. Useful for avoiding code duplication.
* */
contract ImmutableGovernanceInformation {
address internal constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
address internal constant TornTokenAddress = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C;
modifier onlyGovernance() {
require(msg.sender == GovernanceAddress, "only governance");
_;
}
/**
* @dev this modifier calls the pure governance returnMultisigAddress() function,
* if governance version is not -> vault-and-gas upgrade <= version
* then this will not work!
*/
modifier onlyMultisig() {
require(msg.sender == IGovernanceMultisigAddress(GovernanceAddress).returnMultisigAddress(), "only multisig");
_;
}
/**
* @notice Function to return a payable version of the governance address.
* @return payable version of the address
* */
function returnPayableGovernance() internal pure returns (address payable) {
return payable(GovernanceAddress);
}
}

View File

@ -1,14 +1,6 @@
# Tornado Governance Changes Documentation
# Tornado Governance Changes
`LotteryAndVaultProposal.sol`, if executed, modifies the tornado.cash governance contract in multiple ways.
This serves as documentation for all functions which are being added or modified and which have relevant functionality:
## Governance (Governance.sol)
The `Governance` contract available in the `tornado-governance` repository modifies the original Governance source code to enable overriding functions via inheritance. This has been submitted via PR.
This is a non issue, as logic and memory slots are left unaffected and properly referenced to. `tornado-governance` is being imported as a package.
Governance proposal [repo](https://github.com/peppersec/tornado-vault-and-gas-proposal).
## Governance Vault Upgrade (GovernanceVaultUpgrade.sol)

View File

@ -1,89 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { LoopbackProxy } from "tornado-governance/contracts/LoopbackProxy.sol";
import { TornadoVault } from "./vault/TornadoVault.sol";
import { TornadoAuctionHandler } from "./auction/TornadoAuctionHandler.sol";
import { GovernanceGasUpgrade } from "./gas/GovernanceGasUpgrade.sol";
import { IGovernanceVesting } from "./interfaces/IGovernanceVesting.sol";
import { ImmutableGovernanceInformation } from "./ImmutableGovernanceInformation.sol";
/**
* @notice This proposal should upgrade governance to the vault and gas version without breaking any logic.
* */
contract VaultAndGasProposal is ImmutableGovernanceInformation {
using SafeMath for uint256;
IGovernanceVesting public constant GovernanceVesting = IGovernanceVesting(0x179f48C78f57A3A78f0608cC9197B8972921d1D2);
address public immutable gasCompLogic;
/// @notice the new voting period we would like to include
uint256 public immutable votingPeriod;
event TornadoAuctionHandlerCreated(address indexed handler);
constructor(address _gasCompLogic, uint256 _votingPeriod) public {
gasCompLogic = _gasCompLogic;
votingPeriod = _votingPeriod;
}
/// @notice the entry point for the governance upgrade logic execution
/// @dev this function bundles all of the initialization logic for all of the contracts of the project
function executeProposal() external {
address vault = address(new TornadoVault());
LoopbackProxy(returnPayableGovernance()).upgradeTo(address(new GovernanceGasUpgrade(gasCompLogic, vault)));
GovernanceGasUpgrade newGovernance = GovernanceGasUpgrade(returnPayableGovernance());
IERC20 tornToken = IERC20(TornTokenAddress);
newGovernance.setVotingPeriod(votingPeriod);
/**
The below variable holds the total amount of TORN outflows from all of the proposal executions,
which will be used to calculate the proper amount of TORN for transfer to Governance.
For an explanation as to how this variable has been calculated with these fix values, please look at:
https://github.com/h-ivor/tornado-lottery-period/blob/production/scripts/balance_estimation.md
*/
uint256 totalOutflowsOfProposalExecutions = 120000000000000000000000 +
22916666666666666666666 +
54999999999999969408000 -
27e18;
require(
tornToken.transfer(
address(newGovernance.userVault()),
(tornToken.balanceOf(address(this))).sub(GovernanceVesting.released().sub(totalOutflowsOfProposalExecutions))
),
"TORN: transfer failed"
);
uint96 amountOfTornToAuctionOff = 787 ether;
uint96 minBuyAmount = 11 ether;
uint256 minBidInTorn = 10 ether;
uint256 fundingThreshold = 9 ether;
TornadoAuctionHandler auctionHandler = new TornadoAuctionHandler();
emit TornadoAuctionHandlerCreated(address(auctionHandler));
tornToken.transfer(address(auctionHandler), amountOfTornToAuctionOff);
/**
As with above, please see:
https://github.com/h-ivor/tornado-lottery-period/blob/production/contracts/auction/TornadoAuctionHandler.sol
*/
auctionHandler.initializeAuction(
block.timestamp + 5 days,
amountOfTornToAuctionOff,
minBuyAmount,
minBidInTorn,
fundingThreshold
);
}
}

View File

@ -1,59 +0,0 @@
# Auctioning some Tornado for compensations ETH
To boost voting activity, one of our ideas is to compensate gas used for voting on proposals.
Both for the castVote and castDelegatedVote functionality.
To make this as smooth as possible, we will compensate users directly in **ETH** (non-wrapped) for voting.
The priority fee is not compensated for, as to make exploiting the compensations unnecessary and unprofitable.
In order to receive ETH, TORN will be auctioned off by the governance contract with the help of a auction helper
(see contracts/auction/TornadoAuctionHandler.sol).
This contract has two functionalities:
- Initiate an auction.
- Convert all WETH it holds into ETH and send to Governance (callable by anyone).
This way, Governance does not need to handle WETH swap logic (would require extra logic) and ETH will be directly sent to the governance contract.
The initializeAuction function takes a couple of parameters:
```
function initializeAuction(
uint256 _auctionEndDate,
uint96 _auctionedSellAmount,
uint96 _minBuyAmount,
uint256 _minBidPerOrder,
uint256 _minFundingThreshold
) external onlyGovernance {
```
- \_auctionEndDate -> the auction end date expressed in UNIX format.
- \_auctionedSellAmount -> the amount of TORN to be sold in the auction.
- \_minBuyAmount -> this variable helps to define the minimum price via the following formula: \_auctionedSellAmount/\_minBuyAmount, in other words the minimum amount of TORN per ETH.
- \_minBidPerOrder -> minimum buy amount per a single order (of tokens being auctioned), is also used to prevent users from buying too low amounts and hurting themselves.
- \_minFundingThreshold -> minimum amount of buy tokens (ETH) for the ENTIRE auction. If this is not reached, the auction reverts and all tokens are sent back to their original owners.
This function does not take all the parameters for initializing the auction, the entire function may be seen below, some were left out of convenience:
```
IEasyAuction(EasyAuctionAddress).initiateAuction(
IERC20(TornTokenAddress),
IERC20(WETHAddress),
0, // orderCancellationEndDate
_auctionEndDate,
_auctionedSellAmount,
_minBuyAmount,
_minBidPerOrder,
_minFundingThreshold,
true, // isAtomicClosureAllowed
address(0x0000000000000000000000000000000000000000), // access
new bytes(0) // access
);
```
- Addresses of the tokens being bought/sold (ETH/TORN).
- orderCancellationEndDate -> date until order can be cancelled. For us, this is 0, meaning orders can't be cancelled once set.
- isAtomicClosureAllowed -> when auction end date is reached, a participant may set a last order in exchange for closing the auction, meaning it incentivizes the user to end the auction (gas payments, time saving) by giving him a risk-free action at the end. For us, false, due to tests showing that dust collection might not work if this is used.
- Last two fields are for access management, we have no whitelist for the auction, thus redundant and set to 0 for us.

View File

@ -1,58 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import { IWETH } from "./interfaces/IWETH.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { EtherSend } from "../libraries/EtherSend.sol";
import { IEasyAuction } from "./interfaces/IEasyAuction.sol";
import { ImmutableGovernanceInformation } from "../ImmutableGovernanceInformation.sol";
/// @notice Handler which should help governance start an auction and transfer results of an auction to governance.
/// @dev The reasoning behind this contract is to not bloat governance with unnecessary logic.
contract TornadoAuctionHandler is ImmutableGovernanceInformation {
using EtherSend for address;
address public constant EasyAuctionAddress = 0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101;
address public constant WETHAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
/// @notice main auction initialization function, please see: https://github.com/h-ivor/tornado-lottery-period/blob/only-vault-and-gas/contracts/auction/Auction.md
/// @dev calls easy auction deployed on eth mainnet
function initializeAuction(
uint256 _auctionEndDate,
uint96 _auctionedSellAmount,
uint96 _minBuyAmount,
uint256 _minBidPerOrder,
uint256 _minFundingThreshold
) external onlyGovernance {
require(IERC20(TornTokenAddress).balanceOf(address(this)) >= _auctionedSellAmount, "torn balance not enough");
IERC20(TornTokenAddress).approve(EasyAuctionAddress, _auctionedSellAmount);
IEasyAuction(EasyAuctionAddress).initiateAuction(
IERC20(TornTokenAddress),
IERC20(WETHAddress),
0,
_auctionEndDate,
_auctionedSellAmount,
_minBuyAmount,
_minBidPerOrder,
_minFundingThreshold,
false,
address(0x0000000000000000000000000000000000000000),
new bytes(0)
);
}
/// @notice function to transfer all eth and TORN dust to governance
function convertAndTransferToGovernance() external {
IWETH(WETHAddress).withdraw(IWETH(WETHAddress).balanceOf(address(this)));
if (address(this).balance > 0) require(GovernanceAddress.sendEther(address(this).balance), "pay fail");
if (IERC20(TornTokenAddress).balanceOf(address(this)) > 0)
IERC20(TornTokenAddress).transfer(GovernanceAddress, IERC20(TornTokenAddress).balanceOf(address(this)));
}
/// @notice receive eth that should only allow mainnet WETH to send eth
receive() external payable {
require(msg.sender == WETHAddress, "only weth");
}
}

View File

@ -1,21 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IEasyAuction {
function initiateAuction(
IERC20 _auctioningToken,
IERC20 _biddingToken,
uint256 orderCancellationEndDate,
uint256 auctionEndDate,
uint96 _auctionedSellAmount,
uint96 _minBuyAmount,
uint256 minimumBiddingAmountPerOrder,
uint256 minFundingThreshold,
bool isAtomicClosureAllowed,
address accessManagerContract,
bytes memory accessManagerContractData
) external returns (uint256);
}

View File

@ -1,7 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
interface IPayableGovernance {
function receiveEther() external payable returns (bool);
}

View File

@ -1,23 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
interface IWETH {
function balanceOf(address account) external view returns (uint256);
function deposit() external payable;
function withdraw(uint256 wad) external;
function totalSupply() external view returns (uint256);
function approve(address guy, uint256 wad) external returns (bool);
function transfer(address dst, uint256 wad) external returns (bool);
function transferFrom(
address src,
address dst,
uint256 wad
) external returns (bool);
}

View File

@ -1,49 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import { EtherSend } from "../libraries/EtherSend.sol";
interface IPayableGovernance {
function receiveEther() external payable returns (bool);
}
/**
* @notice this contract should store ether for gas compensations and also retrieve the basefee
* */
contract GasCompensationVault {
using EtherSend for address;
address private constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
modifier onlyGovernance() {
require(msg.sender == GovernanceAddress, "only gov");
_;
}
/**
* @notice function to compensate gas by sending amount eth to a recipient
* @param recipient address to receive amount eth
* @param gasAmount the amount of gas to be compensated
* */
function compensateGas(address recipient, uint256 gasAmount) external onlyGovernance {
uint256 vaultBalance = address(this).balance;
uint256 toCompensate = gasAmount * block.basefee;
if (vaultBalance == 0) return;
payable(recipient).send((toCompensate > vaultBalance) ? vaultBalance : toCompensate);
}
/**
* @notice function to withdraw compensate eth back to governance
* @param amount the amount of eth to withdraw back to governance
* */
function withdrawToGovernance(uint256 amount) external onlyGovernance {
uint256 vaultBalance = address(this).balance;
require(GovernanceAddress.sendEther((amount > vaultBalance) ? vaultBalance : amount), "pay fail");
}
/**
* @notice receive ether function, does nothing but receive ether
* */
receive() external payable {}
}

View File

@ -1,8 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
interface IGovernanceVesting {
function released() external view returns (uint256);
}

View File

@ -1,17 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12 || ^0.8.7;
/// @notice very short library which implements a method to transfer ether via <address>.call
library EtherSend {
/**
* @notice function to transfer ether via filling the value field of a call
* @dev DICLAIMER: you must handle the possibility of reentrancy when using this function!!!
* @param to address to be transferred to
* @param amount amount to be transferred
* @return success true if transfer successful
* */
function sendEther(address to, uint256 amount) internal returns (bool success) {
(success, ) = payable(to).call{ value: amount }("");
}
}

View File

@ -1,26 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract GasCompensationVault {
address private constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
modifier onlyGovernance() {
require(msg.sender == GovernanceAddress, "only gov");
_;
}
function compensateGas(address recipient, uint256 amount) external onlyGovernance {
if (address(this).balance == 0) return;
require(
(amount > address(this).balance) ? payable(recipient).send(address(this).balance) : payable(recipient).send(amount),
"compensation failed"
);
}
receive() external payable {}
function getBasefee() external view returns (uint256) {
return 5;
}
}

View File

@ -2,9 +2,9 @@
pragma solidity ^0.6.12;
import "tornado-governance/contracts/Governance.sol";
import "../../v1/Governance.sol";
contract MockProposal1 {
contract MockProposal {
address public constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
function executeProposal() external {

View File

@ -1,16 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import { IterableOrderedOrderSet } from "@gnosis.pm/ido-contracts/contracts/libraries/IterableOrderedOrderSet.sol";
contract OrderEncoderHelper {
function encodeOrder(
uint64 userId,
uint96 buyAmount,
uint96 sellAmount
) external pure returns (bytes32) {
return IterableOrderedOrderSet.encodeOrder(userId, buyAmount, sellAmount);
}
}

View File

@ -1,21 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
/// @title Vault which holds user funds
contract TornadoVault {
using SafeERC20 for IERC20;
address internal constant TornTokenAddress = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C;
address internal constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
/// @notice withdraws TORN from the contract
/// @param amount amount to withdraw
function withdrawTorn(address recipient, uint256 amount) external {
require(msg.sender == GovernanceAddress, "only gov");
IERC20(TornTokenAddress).safeTransfer(recipient, amount);
}
}

View File

@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
import { GovernanceGasUpgrade } from "../v2-vault-and-gas/GovernanceGasUpgrade.sol";
import { ITornadoStakingRewards } from "./interfaces/ITornadoStakingRewards.sol";
/**
* @notice The Governance staking upgrade. Adds modifier to any un/lock operation to update rewards
* @dev CONTRACT RISKS:
* - if updateRewards reverts (should not happen due to try/catch) locks/unlocks could be blocked
* - generally inherits risks from former governance upgrades
*/
contract GovernanceStakingUpgrade is GovernanceGasUpgrade {
ITornadoStakingRewards public immutable Staking;
event RewardUpdateSuccessful(address indexed account);
event RewardUpdateFailed(address indexed account, bytes indexed errorData);
constructor(
address stakingRewardsAddress,
address gasCompLogic,
address userVaultAddress
) public GovernanceGasUpgrade(gasCompLogic, userVaultAddress) {
Staking = ITornadoStakingRewards(stakingRewardsAddress);
}
/**
* @notice This modifier should make a call to Staking to update the rewards for account without impacting logic on revert
* @dev try / catch block to handle reverts
* @param account Account to update rewards for.
* */
modifier updateRewards(address account) {
try Staking.updateRewardsOnLockedBalanceChange(account, lockedBalance[account]) {
emit RewardUpdateSuccessful(account);
} catch (bytes memory errorData) {
emit RewardUpdateFailed(account, errorData);
}
_;
}
function lock(
address owner,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override updateRewards(owner) {
super.lock(owner, amount, deadline, v, r, s);
}
function lockWithApproval(uint256 amount) public virtual override updateRewards(msg.sender) {
super.lockWithApproval(amount);
}
function unlock(uint256 amount) public virtual override updateRewards(msg.sender) {
super.unlock(amount);
}
}

View File

@ -0,0 +1,14 @@
# Tornado Relayer Registry
Governance proposal [repo](https://github.com/Rezan-vm/tornado-relayer-registry).
Governance upgrade which includes a registry for relayer registration and staking mechanisms for the TORN token.
## Overview
1. Anyone can become a relayer by staking TORN into Registry contract.
2. Minimum stake is governed by the Governance.
3. Each Pool has its own fee % which is also set by the Governance.
4. On every withdrawal via relayer, the relayer has to pay the Tornado Pool fee in TORN. The fee is deducted from his staked balance.
5. All collected fees are stored into StakingReward contract.
6. Any TORN holder can stake their TORN into Governance contract like they were before, but earning fees proportionately to their stake.

View File

@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
interface ITornadoStakingRewards {
function updateRewardsOnLockedBalanceChange(address account, uint256 amountLockedBeforehand) external;
}

View File

@ -6,8 +6,10 @@ require('hardhat-spdx-license-identifier')
require('hardhat-storage-layout')
require('hardhat-log-remover')
require('hardhat-contract-sizer')
require('solidity-coverage')
const config = require('./config')
require('./tasks/deploy_proposal.js')
/**
* @type import('hardhat/config').HardhatUserConfig
*/
@ -23,31 +25,13 @@ module.exports = {
},
},
},
{
version: '0.8.7',
settings: {
optimizer: {
enabled: true,
runs: 1000,
},
},
},
{
version: '0.7.6',
settings: {
optimizer: {
enabled: true,
runs: 1000,
},
},
},
],
},
networks: {
hardhat: {
forking: {
url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
blockNumber: 13042331,
blockNumber: config.forkBlockNumber,
},
initialBaseFeePerGas: 5,
},

View File

@ -1,6 +1,6 @@
{
"name": "tornado-governance",
"version": "2.0.0",
"version": "3.0.0",
"description": "",
"main": "index.js",
"files": [
@ -16,7 +16,8 @@
"test:all:f": "yarn prettier:fix && yarn test && yarn lint",
"test:f": "yarn prettier:fix && yarn test",
"clean": "yarn prettier:fix && yarn lint",
"compile": "yarn prettier:fix && yarn hardhat compile"
"compile": "yarn prettier:fix && yarn hardhat compile",
"coverage": "yarn hardhat coverage"
},
"author": "Tornado.cash team <hello@tornado.cash>",
"license": "MIT",
@ -49,6 +50,7 @@
"hardhat-storage-layout": "^0.1.6",
"prettier": "^2.3.2",
"prettier-plugin-solidity": "^1.0.0-beta.17",
"solhint-plugin-prettier": "^0.0.5"
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "^0.7.21"
}
}

View File

@ -1,33 +0,0 @@
require('dotenv').config()
const { task } = require('hardhat/config')
const { BigNumber } = require('@ethersproject/bignumber')
task('deploy_proposal', 'deploy the lottery/vault upgrade proposal')
.addParam('votingPeriod', 'the desired new voting period')
.setAction(async (taskArgs, hre) => {
const GasVaultFactory = await hre.ethers.getContractFactory(
'contracts/basefee/GasCompensationVault.sol:GasCompensationVault',
)
const GasVaultContract = await GasVaultFactory.deploy()
await GasVaultContract.deployTransaction.wait(5)
await hre.run('verify:verify', {
address: GasVaultContract.address,
})
const ProposalFactory = await hre.ethers.getContractFactory('VaultAndGasProposal')
const ProposalContract = await ProposalFactory.deploy(
GasVaultContract.address,
BigNumber.from(taskArgs.votingPeriod),
)
await ProposalContract.deployTransaction.wait(5)
await hre.run('verify:verify', {
address: ProposalContract.address,
constructorArguments: [GasVaultContract.address, BigNumber.from(taskArgs.votingPeriod)],
})
console.log('Successfully deployed proposal contract at: ', ProposalContract.address)
})

View File

@ -1,20 +0,0 @@
require('dotenv').config()
const { task } = require('hardhat/config')
task('propose_proposal', 'propose proposal that uses factory')
.addParam('proposalAddress', 'address of proposal')
.setAction(async (taskArgs, hre) => {
const proposalName = 'lottery-and-vault-proposal'
const signerArray = hre.ethers.getSigners()
const GovernanceContract = await hre.ethers.getContractAt(
'Governance',
'0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
)
await GovernanceContract.propose(taskArgs.proposalAddress, proposalName)
const id = await GovernanceContract.latestProposalIds(signerArray[0].address)
const state = await GovernanceContract.state(id)
console.log('Proposal with name: ', proposalName, ' proposed with id: ', id, ', has state: ', state)
})

View File

@ -1,93 +0,0 @@
{
"mainnet": {
"tornado_cash_addresses": {
"governance": "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce",
"multisig": "0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4",
"trees": "0x527653eA119F3E6a1F5BD18fbF4714081D7B31ce",
"tornado_proxy": "0x722122dF12D4e14e13Ac3b6895a86e84145b6967"
},
"token_addresses": {
"torn": "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C",
"weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"dai": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"eth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"cdai": "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643",
"usdt": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"wbtc": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
},
"project_specific": {
"contract_construction": {
"RelayerRegistryData": {
"tornado_pools": [
"0x12d66f87a04a9e220743712ce6d9bb1b5616b8fc",
"0x47ce0c6ed5b0ce3d3a51fdb1c52dc66a7c3c2936",
"0x910cbd523d972eb0a6f4cae4618ad62622b39dbf",
"0xa160cdab225685da1d56aa342ad8841c3b53f291",
"0xd4b88df4d29f5cedd6857912842cff3b20c8cfa3",
"0xfd8610d20aa15b7b2e3be39b396a1bc3516c7144",
"0x07687e702b410fa43f4cb4af7fa097918ffd2730",
"0x23773e65ed146a459791799d01336db287f25334",
"0x22aaA7720ddd5388A3c0A3333430953C68f1849b",
"0x03893a7c7463AE47D46bc7f091665f1893656003",
"0x2717c5e28cf931547B621a5dddb772Ab6A35B701",
"0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af",
"0xd96f2B1c14Db8458374d9Aca76E26c3D18364307",
"0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D",
"0x169AD27A470D064DEDE56a2D3ff727986b15D52B",
"0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f",
"0x178169B423a011fff22B9e3F3abeA13414dDD0F1",
"0x610B717796ad172B316836AC95a2ffad065CeaB4",
"0xbB93e510BbCD0B7beb5A853875f9eC60275CF498"
],
"uniswap_pool_fees": [
3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000, 500, 500, 500, 500, 500,
500, 500
],
"pool_tokens": [
"eth",
"eth",
"eth",
"eth",
"dai",
"dai",
"dai",
"dai",
"cdai",
"cdai",
"cdai",
"cdai",
"usdc",
"usdc",
"usdt",
"usdt",
"wbtc",
"wbtc",
"wbtc"
],
"pool_denominations": [
"0.1",
"1",
"10",
"100",
"100",
"1000",
"10000",
"100000",
"5000",
"50000",
"500000",
"5000000",
"100",
"1000",
"100",
"1000",
"0.1",
"1",
"10"
]
}
}
}
}
}

37
test/utils.js Normal file
View File

@ -0,0 +1,37 @@
/* global ethers, network */
async function setTime(timestamp) {
await ethers.provider.send('evm_setNextBlockTimestamp', [timestamp])
}
async function takeSnapshot() {
return await ethers.provider.send('evm_snapshot', [])
}
async function revertSnapshot(id) {
await ethers.provider.send('evm_revert', [id])
}
async function advanceTime(sec) {
const now = (await ethers.provider.getBlock('latest')).timestamp
await setTime(now + sec)
}
async function getSignerFromAddress(address) {
await network.provider.request({
method: 'hardhat_impersonateAccount',
params: [address],
})
let signer = await ethers.provider.getSigner(address)
signer.address = signer._address
return signer
}
module.exports = {
setTime,
advanceTime,
takeSnapshot,
revertSnapshot,
getSignerFromAddress,
}

View File

@ -3,6 +3,7 @@ const { expect } = require('chai')
const { BigNumber } = require('@ethersproject/bignumber')
const { PermitSigner } = require('../../scripts/v1/Permit.js')
const tornConfig = require('torn-token')
const config = require('../../config')
const ProposalState = {
Pending: 0,
@ -35,7 +36,7 @@ const duration = {
},
}
describe('Governance tests', () => {
describe('V1 governance tests', () => {
/// NETWORK && DOMAIN
let chainId
let domain
@ -934,7 +935,7 @@ describe('Governance tests', () => {
{
forking: {
jsonRpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
blockNumber: process.env.use_latest_block == 'true' ? undefined : 13042331,
blockNumber: process.env.use_latest_block == 'true' ? undefined : config.forkBlockNumber,
},
},
])

View File

@ -1,743 +0,0 @@
const { expect } = require('chai')
const { ethers } = require('hardhat')
const { BigNumber } = require('@ethersproject/bignumber')
const { propose } = require('../../scripts/helper/propose_proposal.js')
const testcases = require('@ethersproject/testcases')
const seedbase = require('../../resources/hdnode.json')
const accountList = require('../../resources/accounts.json')
const EasyAuctionJson = require('@gnosis.pm/ido-contracts/build/artifacts/contracts/EasyAuction.sol/EasyAuction.json')
describe('Start of tests', () => {
///// ON-CHAIN CONSTANTS
let proxy_address = '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce'
let quorumVotes
///////////////////////////// CONTRACTS
let GovernanceContract
let TornToken
let WETH
let TornadoAuctionHandler
let GnosisEasyAuction
let ProposalFactory
let ProposalContract
let GasCompensationFactory
let GasCompensationContract
let OrderHelperFactory
let OrderHelper
//////////////////// IMPERSONATED
let tornadoMultisig
//////////////////////////////// MOCK
let MockProposalFactory
/////// GOV PARAMS
const ProposalState = {
Pending: 0,
Active: 1,
Defeated: 2,
Timelocked: 3,
AwaitingExecution: 4,
Executed: 5,
Expired: 6,
}
///// ACCOUNTS
let dore
let whale
let signerArray = []
let whales = []
//////////////////////////////////// TESTING & UTILITY
let randN = Math.floor(Math.random() * 1023)
let testseed = seedbase[randN].seed
let minewait = async (time) => {
await ethers.provider.send('evm_increaseTime', [time])
await ethers.provider.send('evm_mine', [])
}
let timestamp = async () => {
return (await ethers.provider.getBlock('latest')).timestamp
}
let sendr = async (method, params) => {
return await ethers.provider.send(method, params)
}
let clog = (...x) => {
console.log(x)
}
let pE = (x) => {
return ethers.utils.parseEther(`${x}`)
}
let rand = (l, u) => {
return testcases.randomNumber(testseed, l, u)
}
let snapshotIdArray = []
///////////////////////////////////////////////////////////////////////////7
before(async function () {
signerArray = await ethers.getSigners()
dore = signerArray[0]
GasCompensationFactory = await ethers.getContractFactory(
'contracts/v2-vault-and-gas/testing/GasCompensationVault.sol:GasCompensationVault',
)
GasCompensationContract = await GasCompensationFactory.deploy()
MockProposalFactory = await ethers.getContractFactory('MockProposal1')
ProposalFactory = await ethers.getContractFactory('VaultAndGasProposal')
ProposalContract = await ProposalFactory.deploy(GasCompensationContract.address, 260000)
OrderHelperFactory = await ethers.getContractFactory('OrderEncoderHelper')
OrderHelper = await OrderHelperFactory.deploy()
GovernanceContract = await ethers.getContractAt('contracts/v1/Governance.sol:Governance', proxy_address)
GnosisEasyAuction = await ethers.getContractAt(
EasyAuctionJson.abi,
'0x0b7fFc1f4AD541A4Ed16b40D8c37f0929158D101',
)
TornToken = await ethers.getContractAt(
'@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20',
'0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
)
WETH = await ethers.getContractAt('IWETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
quorumVotes = await GovernanceContract.QUORUM_VOTES()
})
describe('Test complete functionality', () => {
describe('Imitation block', () => {
it('Should successfully imitate tornado multisig', async function () {
await sendr('hardhat_impersonateAccount', ['0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4'])
tornadoMultisig = await ethers.getSigner('0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4')
})
it('Should successfully imitate whale', async function () {
await sendr('hardhat_impersonateAccount', ['0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3'])
whale = await ethers.getSigner('0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3')
GovernanceContract = await GovernanceContract.connect(whale)
let balance = await TornToken.balanceOf(whale.address)
TornToken = await TornToken.connect(whale)
await TornToken.approve(GovernanceContract.address, ethers.utils.parseEther('8000000000'))
await expect(GovernanceContract.lockWithApproval(balance)).to.not.be.reverted
expect((await GovernanceContract.lockedBalance(whale.address)).toString()).to.equal(
balance.toString(),
)
snapshotIdArray[0] = await sendr('evm_snapshot', [])
})
})
describe('Proposal passing block', () => {
it('Should successfully pass the proposal', async function () {
let response, id, state
;[response, id, state] = await propose([whale, ProposalContract, 'Gas Upgrade'])
const { events } = await response.wait()
const args = events.find(({ event }) => event == 'ProposalCreated').args
expect(args.id).to.be.equal(id)
expect(args.proposer).to.be.equal(whale.address)
expect(args.target).to.be.equal(ProposalContract.address)
expect(args.description).to.be.equal('Gas Upgrade')
expect(state).to.be.equal(ProposalState.Pending)
await minewait((await GovernanceContract.VOTING_DELAY()).add(1).toNumber())
await expect(GovernanceContract.castVote(id, true)).to.not.be.reverted
state = await GovernanceContract.state(id)
expect(state).to.be.equal(ProposalState.Active)
await minewait(
(
await GovernanceContract.VOTING_PERIOD()
)
.add(await GovernanceContract.EXECUTION_DELAY())
.add(86400)
.toNumber(),
)
await dore.sendTransaction({ to: whale.address, value: pE(10) })
const executeResponse = await GovernanceContract.execute(id)
const executeReceipt = await executeResponse.wait()
console.log(
'______________________\n',
'Gas used for execution: ',
executeReceipt.cumulativeGasUsed.toString(),
'\n-------------------------\n',
)
const topic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
let handlerAddress
for (let i = 0; i < executeReceipt.logs.length; i++) {
if (executeReceipt.logs[i].topics[0] == topic) {
handlerAddress = executeReceipt.logs[i].topics[1]
}
}
TornadoAuctionHandler = await ethers.getContractAt(
'TornadoAuctionHandler',
'0x' + handlerAddress.slice(26),
)
GovernanceContract = await ethers.getContractAt('GovernanceGasUpgrade', GovernanceContract.address)
clog(await GovernanceContract.version())
const auctionCounter = 38
const auctionData = await GnosisEasyAuction.auctionData(auctionCounter)
expect(auctionData.auctioningToken).to.equal(TornToken.address)
console.log(
'////////////////AUCTION/////////////////\n',
'Started at: ',
await timestamp(),
', Will end at: ',
auctionData.auctionEndDate.toString(),
'\n////////////////////////////////',
)
snapshotIdArray[1] = await sendr('evm_snapshot', [])
})
})
describe('Mock rewards + proposal distribution with multiple accounts', () => {
let addrArray = []
let signerArmy = []
let delegatedSignerArmy = []
let votingAddressArray = []
const numberOfVoters = 80
const numberOfDelegators = 30
it('Should create empty address array', () => {
for (let i = 0; i < 10; i++) {
votingAddressArray[i] = new Array(numberOfDelegators / 10 + 1)
}
})
it('Should impersonate and fund 80 accounts', async function () {
////////// WRITE WHALE ADDRESSES AND PREPARE FOR TRANSFERS
addrArray = [
'0x6cC5F688a315f3dC28A7781717a9A798a59fDA7b',
'0xF977814e90dA44bFA03b6295A0616a897441aceC',
'0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3',
'0x055AD5E56c11c0eF55818155c69ed9BA2f4b3e90',
]
for (let i = 0; i < 4; i++) {
await sendr('hardhat_impersonateAccount', [addrArray[i]])
whales[i] = await ethers.getSigner(addrArray[i])
}
for (let i = 1; i < 4; i++) {
//last test really unnecessary
const torn = await TornToken.connect(whales[i])
const whaleBalance = await torn.balanceOf(whales[i].address)
await torn.approve(addrArray[0], whaleBalance)
await expect(() => torn.transfer(addrArray[0], whaleBalance)).to.changeTokenBalance(
torn,
whales[0],
whaleBalance,
)
}
const whale0Balance = await TornToken.balanceOf(whales[0].address)
const toTransfer = whale0Balance.sub(pE(10000)).div(numberOfVoters * 3)
let torn0 = await TornToken.connect(whales[0])
const oldBalance = await TornToken.balanceOf(await GovernanceContract.userVault())
let lockedSum = BigNumber.from(0)
////////// TRANSFER TO 50 ACCOUNTS + DELEGATION TO 10
for (let i = 0; i < numberOfVoters; i++) {
/// PREPARE ACCOUNTS
const accAddress = accountList[i + 7].checksumAddress
await sendr('hardhat_impersonateAccount', [accAddress])
signerArmy[i] = await ethers.getSigner(accAddress)
const tx = { to: signerArmy[i].address, value: pE(1) }
await signerArray[0].sendTransaction(tx)
/// FILL WITH GAS FOR LATER
await expect(() => torn0.transfer(signerArmy[i].address, toTransfer)).to.changeTokenBalance(
torn0,
signerArmy[i],
toTransfer,
)
let torn = await torn0.connect(signerArmy[i])
/// APPROVE TO GOVERNANCE FOR LOCK
await expect(torn.approve(GovernanceContract.address, toTransfer)).to.not.be.reverted
const gov = await GovernanceContract.connect(signerArmy[i])
///// LOCK
if (i > numberOfVoters / 2) {
await expect(() => gov.lockWithApproval(toTransfer.div(i))).to.changeTokenBalance(
torn,
signerArmy[i],
BigNumber.from(0).sub(toTransfer.div(i)),
)
lockedSum = lockedSum.add(toTransfer.div(i))
} else {
await expect(() => gov.lockWithApproval(toTransfer)).to.changeTokenBalance(
torn,
signerArmy[i],
BigNumber.from(0).sub(toTransfer),
)
lockedSum = lockedSum.add(toTransfer)
}
if (i > numberOfVoters - numberOfDelegators - 1) {
delegatedSignerArmy[i - (numberOfVoters - numberOfDelegators)] = signerArmy[i]
}
if (i < 10) {
votingAddressArray[i][0] = signerArmy[i].address
}
const restBalance = await torn.balanceOf(signerArmy[i].address)
await torn.transfer(whale.address, restBalance)
}
for (let i = 0; i < numberOfDelegators; i++) {
const gov = await GovernanceContract.connect(delegatedSignerArmy[i])
/// DELEGATE TO 10 FIRST SIGNERS
await expect(gov.delegate(signerArmy[i % 10].address)).to.emit(gov, 'Delegated')
votingAddressArray[i % 10][Math.floor(i / 10) + 1] = delegatedSignerArmy[i].address
}
const TornVault = await GovernanceContract.userVault()
expect(await TornToken.balanceOf(TornVault)).to.equal(lockedSum.add(oldBalance))
const gov = await GovernanceContract.connect(whales[0])
await expect(torn0.approve(GovernanceContract.address, pE(10000))).to.not.be.reverted
await expect(() => gov.lockWithApproval(toTransfer)).to.changeTokenBalance(
torn0,
whales[0],
BigNumber.from(0).sub(toTransfer),
)
snapshotIdArray[2] = await sendr('evm_snapshot', [])
})
it('Should test if auction handler can convert ETH to gov', async function () {
WETH = await WETH.connect(signerArray[4])
await WETH.deposit({ value: pE(100) })
await WETH.transfer(TornadoAuctionHandler.address, pE(100))
await expect(() => TornadoAuctionHandler.convertAndTransferToGovernance()).to.changeEtherBalance(
GovernanceContract,
pE(100),
)
})
it('Should test if auction will behave properly', async function () {
snapshotIdArray[2] = await sendr('evm_snapshot', [])
let orderArray = []
const initialHandlerBalance = await WETH.balanceOf(TornadoAuctionHandler.address)
/**
* 100 TORN in to total
* Price as of time of writing 1 ETH == 51.66 TORN
* First test is an overbought auction, 20 buyers compete for 40 TORN
*/
for (let i = 0; i < signerArray.length; i++) {
const bidder = signerArray[i]
WETH = await WETH.connect(bidder)
await expect(() => WETH.deposit({ value: pE(100) })).to.changeEtherBalance(
bidder,
BigNumber.from(0).sub(pE(100)),
)
const buyAmount = pE(240)
const sellAmount = pE(23.73 + i / 100)
await WETH.approve(GnosisEasyAuction.address, sellAmount)
GnosisEasyAuction = await GnosisEasyAuction.connect(bidder)
await GnosisEasyAuction.placeSellOrders(
38,
[buyAmount],
[sellAmount],
['0x0000000000000000000000000000000000000000000000000000000000000001'],
'0x',
)
orderArray[i] = await OrderHelper.encodeOrder(
await GnosisEasyAuction.numUsers(),
buyAmount,
sellAmount,
)
}
let auctionEndDt = (await GnosisEasyAuction.auctionData(38))[3].sub(BigNumber.from(await timestamp()))
await minewait(auctionEndDt.toNumber())
await GnosisEasyAuction.settleAuction(38)
expect(await WETH.balanceOf(TornadoAuctionHandler.address)).to.be.gt(initialHandlerBalance)
for (let i = 0; i < signerArray.length; i++) {
await GnosisEasyAuction.claimFromParticipantOrder(38, [orderArray[i]])
const balance = await TornToken.balanceOf(signerArray[i].address)
if (balance.toString() != '0') console.log(`Signer ${i} claimed:`, balance.toString(), ' torn')
}
console.log('All other signers got nothing!')
let claimedSum = BigNumber.from(0)
for (let i = 0; i < signerArray.length; i++) {
const claimed = await TornToken.balanceOf(signerArray[i].address)
claimedSum = claimedSum.add(claimed)
}
expect(claimedSum).to.closeTo(ethers.utils.parseEther('787'), ethers.utils.parseUnits('1', 'szabo'))
/// Now revert and test with lower
await sendr('evm_revert', [snapshotIdArray[2]])
snapshotIdArray[2] = await sendr('evm_snapshot', [])
for (let i = 0; i < signerArray.length; i++) {
const bidder = signerArray[i]
WETH = await WETH.connect(bidder)
await expect(() => WETH.deposit({ value: pE(100) })).to.changeEtherBalance(
bidder,
BigNumber.from(0).sub(pE(100)),
)
const buyAmount = pE(0.5)
const sellAmount = pE(22 + i / 100)
await WETH.approve(GnosisEasyAuction.address, sellAmount)
GnosisEasyAuction = await GnosisEasyAuction.connect(bidder)
await GnosisEasyAuction.placeSellOrders(
38,
[buyAmount],
[sellAmount],
['0x0000000000000000000000000000000000000000000000000000000000000001'],
'0x',
)
orderArray[i] = await OrderHelper.encodeOrder(
await GnosisEasyAuction.numUsers(),
buyAmount,
sellAmount,
)
}
auctionEndDt = (await GnosisEasyAuction.auctionData(38))[3].sub(BigNumber.from(await timestamp()))
await minewait(auctionEndDt.toNumber())
await GnosisEasyAuction.settleAuction(38)
expect(await WETH.balanceOf(TornadoAuctionHandler.address)).to.be.gt(initialHandlerBalance)
for (let i = 0; i < signerArray.length; i++) {
await GnosisEasyAuction.claimFromParticipantOrder(38, [orderArray[i]])
console.log(
`Signer ${i} claimed: `,
(await TornToken.balanceOf(signerArray[i].address)).toString(),
' torn',
)
}
claimedSum = BigNumber.from(0)
for (let i = 0; i < signerArray.length; i++) {
const claimed = await TornToken.balanceOf(signerArray[i].address)
claimedSum = claimedSum.add(claimed)
}
expect(claimedSum).to.be.closeTo(
ethers.utils.parseEther('787'),
ethers.utils.parseUnits('1', 'szabo'),
)
})
it('Test multiple accounts proposal', async function () {
let checkIfQuorumFulfilled = async function (proposalId) {
const proposalData = await GovernanceContract.proposals(proposalId)
const allVotes = proposalData[4].add(proposalData[5])
return allVotes.gte(quorumVotes)
}
ProposalContract = await MockProposalFactory.deploy()
clog(
'Torn balance of governance contract: ',
(await TornToken.balanceOf(GovernanceContract.address)).toString(),
)
////////////// STANDARD PROPOSAL ARGS TEST //////////////////////
let response, id, state
;[response, id, state] = await propose([whales[0], ProposalContract, 'LotteryUpgrade'])
const { events } = await response.wait()
const args = events.find(({ event }) => event == 'ProposalCreated').args
expect(args.id).to.be.equal(id)
expect(args.target).to.be.equal(ProposalContract.address)
expect(args.description).to.be.equal('LotteryUpgrade')
expect(state).to.be.equal(ProposalState.Pending)
////////////////////////INCREMENT TO VOTING TIME////////////////////////
await minewait((await GovernanceContract.VOTING_DELAY()).add(1).toNumber())
/////////////////// PREPARE MULTISIG AND COMPENSATIONS
let multiGov = await GovernanceContract.connect(tornadoMultisig)
await dore.sendTransaction({ to: tornadoMultisig.address, value: pE(1) })
await expect(multiGov.setGasCompensations(pE(500))).to.not.be.reverted
///////////////////////////// VOTE ////////////////////////////
const overrides = {
gasPrice: BigNumber.from(5),
}
let signerArmyBalanceInitial = []
let signerArmyBalanceDiff = []
let gasUsedArray = []
snapshotIdArray[3] = await sendr('evm_snapshot', [])
for (let i = 0; i < 10; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
signerArmyBalanceInitial[i] = await signerArmy[i].getBalance()
if (randN > 0) {
response = await gov.castDelegatedVote(votingAddressArray[i], id, true, overrides)
} else {
response = await gov.castDelegatedVote(votingAddressArray[i], id, false, overrides)
}
signerArmyBalanceDiff[i] = !(await checkIfQuorumFulfilled(id))
? signerArmyBalanceInitial[i].sub(await signerArmy[i].getBalance())
: signerArmyBalanceDiff[i - 1]
const receipt = await response.wait()
gasUsedArray[i] = receipt.cumulativeGasUsed
}
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
signerArmyBalanceInitial[i] = await signerArmy[i].getBalance()
if (randN > 0) {
response = await gov.castVote(id, true, overrides)
} else {
response = await gov.castVote(id, false, overrides)
}
signerArmyBalanceDiff[i] = !(await checkIfQuorumFulfilled(id))
? signerArmyBalanceInitial[i].sub(await signerArmy[i].getBalance())
: signerArmyBalanceDiff[i - 1]
const receipt = await response.wait()
gasUsedArray[i] = receipt.cumulativeGasUsed
}
//////////////////////////////// GET STATE ///////////////////////////////
state = await GovernanceContract.state(id)
expect(state).to.be.equal(ProposalState.Active)
///////////////////////////// VOTER INFO ///////////////////////////////////
// (uncomment for more data)
/*
for (i = 0; i < numberOfVoters; i+=5) {
const j = BigNumber.from(i);
console.log(
`Voter ${i} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j))[0]).toString(),
`Voter ${i+1} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(1)))[0]).toString(),
`Voter ${i+2} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(2)))[0]).toString(),
`Voter ${i+3} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(3)))[0]).toString(),
`Voter ${i+4} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(4)))[0]).toString(),
"\n",
)
}
for (i = 0; i < numberOfVoters; i+=5) {
console.log(
`Voter ${i} ether used: `,
gasUsedArray[i],
`Voter ${i+1} ether used: `,
gasUsedArray[i+1],
`Voter ${i+2} ether used: `,
gasUsedArray[i+2],
`Voter ${i+3} ether used: `,
gasUsedArray[i+3],
`Voter ${i+4} ether used: `,
gasUsedArray[i+4],
"\n",
)
}
*/
await sendr('evm_revert', [snapshotIdArray[3]])
///////////////////////////////// VOTE WITHOUT COMPENSATION //////////////////////////////////////
let gasUsedWithoutCompensation = []
await multiGov.setGasCompensations(pE(100000))
for (let i = 0; i < 10; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
if (randN > 0) {
response = await gov.castDelegatedVote(votingAddressArray[i], id, true, overrides)
} else {
response = await gov.castDelegatedVote(votingAddressArray[i], id, false, overrides)
}
const receipt = await response.wait()
gasUsedWithoutCompensation[i] = receipt.cumulativeGasUsed
}
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
if (randN > 0) {
response = await gov.castVote(id, true, overrides)
} else {
response = await gov.castVote(id, false, overrides)
}
const receipt = await response.wait()
gasUsedWithoutCompensation[i] = receipt.cumulativeGasUsed
}
await multiGov.setGasCompensations(pE(100))
//////////////////////////////// GET STATE ///////////////////////////////
state = await GovernanceContract.state(id)
expect(state).to.be.equal(ProposalState.Active)
///////////////////////////// VOTING GAS INFO ///////////////////////////////////
let gasUsedSumNoComp = BigNumber.from(0)
let gasUsedSum = BigNumber.from(0)
let gasSumDiff = BigNumber.from(0)
let gasUsedSumNoCompDel = BigNumber.from(0)
let gasUsedSumDel = BigNumber.from(0)
let gasSumDiffDel = BigNumber.from(0)
for (let i = 0; i < 10; i++) {
gasUsedSumDel = gasUsedSumDel.add(gasUsedArray[i])
gasUsedSumNoCompDel = gasUsedSumNoCompDel.add(gasUsedWithoutCompensation[i])
gasSumDiffDel = gasSumDiffDel.add(signerArmyBalanceDiff[i])
}
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
gasUsedSum = gasUsedSum.add(gasUsedArray[i])
gasUsedSumNoComp = gasUsedSumNoComp.add(gasUsedWithoutCompensation[i])
gasSumDiff = gasSumDiff.add(signerArmyBalanceDiff[i])
}
const gasUsedAverageNoCompDel = gasUsedSumNoCompDel.div(10)
const gasUsedAverageDel = gasUsedSumDel.div(10)
const gasSumAverageDiffDel = gasSumDiffDel.div(10)
const gasUsedAverageNoComp = gasUsedSumNoComp.div(numberOfVoters - 10)
const gasUsedAverage = gasUsedSum.div(numberOfVoters - 10)
const gasSumAverageDiff = gasSumDiff.div(numberOfVoters - 10)
console.log(
'\n',
'----------------------------CAST VOTE INFO------------------------',
'\n',
'Gas use average: ',
gasUsedAverage.toString(),
'\n',
'Gas use without compensation average: ',
gasUsedAverageNoComp.toString(),
'\n',
'Gas diff average: ',
gasSumAverageDiff.toString(),
'\n',
'Gas compensated in average: ',
gasUsedAverage.sub(gasSumAverageDiff).toString(),
'\n',
'--------------------------------------------------------------------',
'\n',
)
console.log(
'\n',
'----------------------------CAST DELEGATED VOTE INFO------------------------',
'\n',
'Gas use average: ',
gasUsedAverageDel.toString(),
'\n',
'Gas use without compensation average: ',
gasUsedAverageNoCompDel.toString(),
'\n',
'Gas diff average: ',
gasSumAverageDiffDel.toString(),
'\n',
'Gas compensated in average: ',
gasUsedAverageDel.sub(gasSumAverageDiffDel).toString(),
'\n',
'--------------------------------------------------------------------',
'\n',
)
/////////////////////////////// INCREMENT AGAIN //////////////////////////////////
await minewait(
(
await GovernanceContract.VOTING_PERIOD()
)
.add(await GovernanceContract.EXECUTION_DELAY())
.add(10000)
.toNumber(),
)
////////////// EXECUTE
if (BigNumber.from(await GovernanceContract.state(id)).eq(ProposalState.Defeated)) {
await expect(GovernanceContract.execute(id)).to.be.reverted
} else {
await expect(GovernanceContract.execute(id)).to.not.be.reverted
}
})
})
})
after(async function () {
await ethers.provider.send('hardhat_reset', [
{
forking: {
jsonRpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
blockNumber: process.env.use_latest_block == 'true' ? undefined : 13211966,
},
},
])
})
})

View File

@ -0,0 +1,496 @@
const { expect } = require('chai')
const { ethers } = require('hardhat')
const { BigNumber } = require('@ethersproject/bignumber')
const { propose } = require('../../scripts/helper/propose_proposal.js')
const testcases = require('@ethersproject/testcases')
const seedbase = require('../../resources/hdnode.json')
const accountList = require('../../resources/accounts.json')
const config = require('../../config')
describe('V2 governance tests', () => {
///// ON-CHAIN CONSTANTS
let proxy_address = '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce'
let quorumVotes
///////////////////////////// CONTRACTS
let GovernanceContract
let TornToken
//////////////////// IMPERSONATED
let tornadoMultisig
//////////////////////////////// MOCK
let MockProposalFactory
/////// GOV PARAMS
const ProposalState = {
Pending: 0,
Active: 1,
Defeated: 2,
Timelocked: 3,
AwaitingExecution: 4,
Executed: 5,
Expired: 6,
}
///// ACCOUNTS
let dore
let whale
let signerArray = []
let whales = []
//////////////////////////////////// TESTING & UTILITY
let randN = Math.floor(Math.random() * 1023)
let testseed = seedbase[randN].seed
let minewait = async (time) => {
await ethers.provider.send('evm_increaseTime', [time])
await ethers.provider.send('evm_mine', [])
}
let sendr = async (method, params) => {
return await ethers.provider.send(method, params)
}
let clog = (...x) => {
console.log(x)
}
let pE = (x) => {
return ethers.utils.parseEther(`${x}`)
}
let rand = (l, u) => {
return testcases.randomNumber(testseed, l, u)
}
let snapshotIdArray = []
///////////////////////////////////////////////////////////////////////////7
before(async function () {
signerArray = await ethers.getSigners()
dore = signerArray[0]
MockProposalFactory = await ethers.getContractFactory('MockProposal')
GovernanceContract = await ethers.getContractAt('GovernanceGasUpgrade', proxy_address)
TornToken = await ethers.getContractAt(
'@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20',
'0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
)
quorumVotes = await GovernanceContract.QUORUM_VOTES()
})
describe('#imitation block', () => {
it('Should successfully imitate tornado multisig', async function () {
await sendr('hardhat_impersonateAccount', ['0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4'])
tornadoMultisig = await ethers.getSigner('0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4')
})
it('Should successfully imitate whale', async function () {
await sendr('hardhat_impersonateAccount', ['0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3'])
whale = await ethers.getSigner('0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3')
GovernanceContract = await GovernanceContract.connect(whale)
let balance = await TornToken.balanceOf(whale.address)
TornToken = await TornToken.connect(whale)
await TornToken.approve(GovernanceContract.address, ethers.utils.parseEther('8000000000'))
await expect(GovernanceContract.lockWithApproval(balance)).to.not.be.reverted
expect((await GovernanceContract.lockedBalance(whale.address)).toString()).to.equal(balance.toString())
snapshotIdArray[0] = await sendr('evm_snapshot', [])
})
})
describe('#mock rewards + proposal distribution with multiple accounts', () => {
let addrArray = []
let signerArmy = []
let delegatedSignerArmy = []
let votingAddressArray = []
const numberOfVoters = 80
const numberOfDelegators = 30
it('Should create empty address array', () => {
for (let i = 0; i < 10; i++) {
votingAddressArray[i] = new Array(numberOfDelegators / 10 + 1)
}
})
it('Should impersonate and fund 80 accounts', async function () {
////////// WRITE WHALE ADDRESSES AND PREPARE FOR TRANSFERS
addrArray = [
'0x6cC5F688a315f3dC28A7781717a9A798a59fDA7b',
'0xF977814e90dA44bFA03b6295A0616a897441aceC',
'0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3',
'0x055AD5E56c11c0eF55818155c69ed9BA2f4b3e90',
]
for (let i = 0; i < 4; i++) {
await sendr('hardhat_impersonateAccount', [addrArray[i]])
whales[i] = await ethers.getSigner(addrArray[i])
}
for (let i = 1; i < 4; i++) {
//last test really unnecessary
const torn = await TornToken.connect(whales[i])
const whaleBalance = await torn.balanceOf(whales[i].address)
await torn.approve(addrArray[0], whaleBalance)
await expect(() => torn.transfer(addrArray[0], whaleBalance)).to.changeTokenBalance(
torn,
whales[0],
whaleBalance,
)
}
const whale0Balance = await TornToken.balanceOf(whales[0].address)
const toTransfer = whale0Balance.sub(pE(10000)).div(numberOfVoters * 3)
let torn0 = await TornToken.connect(whales[0])
const oldBalance = await TornToken.balanceOf(await GovernanceContract.userVault())
let lockedSum = BigNumber.from(0)
////////// TRANSFER TO 50 ACCOUNTS + DELEGATION TO 10
for (let i = 0; i < numberOfVoters; i++) {
/// PREPARE ACCOUNTS
const accAddress = accountList[i + 7].checksumAddress
await sendr('hardhat_impersonateAccount', [accAddress])
signerArmy[i] = await ethers.getSigner(accAddress)
const tx = { to: signerArmy[i].address, value: pE(1) }
await signerArray[0].sendTransaction(tx)
/// FILL WITH GAS FOR LATER
await expect(() => torn0.transfer(signerArmy[i].address, toTransfer)).to.changeTokenBalance(
torn0,
signerArmy[i],
toTransfer,
)
let torn = await torn0.connect(signerArmy[i])
/// APPROVE TO GOVERNANCE FOR LOCK
await expect(torn.approve(GovernanceContract.address, toTransfer)).to.not.be.reverted
const gov = await GovernanceContract.connect(signerArmy[i])
///// LOCK
if (i > numberOfVoters / 2) {
await expect(() => gov.lockWithApproval(toTransfer.div(i))).to.changeTokenBalance(
torn,
signerArmy[i],
BigNumber.from(0).sub(toTransfer.div(i)),
)
lockedSum = lockedSum.add(toTransfer.div(i))
} else {
await expect(() => gov.lockWithApproval(toTransfer)).to.changeTokenBalance(
torn,
signerArmy[i],
BigNumber.from(0).sub(toTransfer),
)
lockedSum = lockedSum.add(toTransfer)
}
if (i > numberOfVoters - numberOfDelegators - 1) {
delegatedSignerArmy[i - (numberOfVoters - numberOfDelegators)] = signerArmy[i]
}
if (i < 10) {
votingAddressArray[i][0] = signerArmy[i].address
}
const restBalance = await torn.balanceOf(signerArmy[i].address)
await torn.transfer(whale.address, restBalance)
}
for (let i = 0; i < numberOfDelegators; i++) {
const gov = await GovernanceContract.connect(delegatedSignerArmy[i])
/// DELEGATE TO 10 FIRST SIGNERS
await expect(gov.delegate(signerArmy[i % 10].address)).to.emit(gov, 'Delegated')
votingAddressArray[i % 10][Math.floor(i / 10) + 1] = delegatedSignerArmy[i].address
}
const TornVault = await GovernanceContract.userVault()
expect(await TornToken.balanceOf(TornVault)).to.equal(lockedSum.add(oldBalance))
const gov = await GovernanceContract.connect(whales[0])
await expect(torn0.approve(GovernanceContract.address, pE(10000))).to.not.be.reverted
await expect(() => gov.lockWithApproval(toTransfer)).to.changeTokenBalance(
torn0,
whales[0],
BigNumber.from(0).sub(toTransfer),
)
snapshotIdArray[1] = await sendr('evm_snapshot', [])
})
it('Test multiple accounts proposal', async function () {
let checkIfQuorumFulfilled = async function (proposalId) {
const proposalData = await GovernanceContract.proposals(proposalId)
const allVotes = proposalData[4].add(proposalData[5])
return allVotes.gte(quorumVotes)
}
const ProposalContract = await MockProposalFactory.deploy()
clog(
'Torn balance of governance contract: ',
(await TornToken.balanceOf(GovernanceContract.address)).toString(),
)
////////////// STANDARD PROPOSAL ARGS TEST //////////////////////
let response, id, state
;[response, id, state] = await propose([whales[0], ProposalContract, 'LotteryUpgrade'])
const { events } = await response.wait()
const args = events.find(({ event }) => event == 'ProposalCreated').args
expect(args.id).to.be.equal(id)
expect(args.target).to.be.equal(ProposalContract.address)
expect(args.description).to.be.equal('LotteryUpgrade')
expect(state).to.be.equal(ProposalState.Pending)
////////////////////////INCREMENT TO VOTING TIME////////////////////////
await minewait((await GovernanceContract.VOTING_DELAY()).add(1).toNumber())
/////////////////// PREPARE MULTISIG AND COMPENSATIONS
let multiGov = await GovernanceContract.connect(tornadoMultisig)
await dore.sendTransaction({ to: tornadoMultisig.address, value: pE(1) })
await expect(multiGov.setGasCompensations(pE(500))).to.not.be.reverted
///////////////////////////// VOTE ////////////////////////////
const overrides = {
gasPrice: BigNumber.from(5),
}
let signerArmyBalanceInitial = []
let signerArmyBalanceDiff = []
let gasUsedArray = []
snapshotIdArray[2] = await sendr('evm_snapshot', [])
for (let i = 0; i < 10; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
signerArmyBalanceInitial[i] = await signerArmy[i].getBalance()
if (randN > 0) {
response = await gov.castDelegatedVote(votingAddressArray[i], id, true, overrides)
} else {
response = await gov.castDelegatedVote(votingAddressArray[i], id, false, overrides)
}
signerArmyBalanceDiff[i] = !(await checkIfQuorumFulfilled(id))
? signerArmyBalanceInitial[i].sub(await signerArmy[i].getBalance())
: signerArmyBalanceDiff[i - 1]
const receipt = await response.wait()
gasUsedArray[i] = receipt.cumulativeGasUsed
}
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
signerArmyBalanceInitial[i] = await signerArmy[i].getBalance()
if (randN > 0) {
response = await gov.castVote(id, true, overrides)
} else {
response = await gov.castVote(id, false, overrides)
}
signerArmyBalanceDiff[i] = !(await checkIfQuorumFulfilled(id))
? signerArmyBalanceInitial[i].sub(await signerArmy[i].getBalance())
: signerArmyBalanceDiff[i - 1]
const receipt = await response.wait()
gasUsedArray[i] = receipt.cumulativeGasUsed
}
//////////////////////////////// GET STATE ///////////////////////////////
state = await GovernanceContract.state(id)
expect(state).to.be.equal(ProposalState.Active)
///////////////////////////// VOTER INFO ///////////////////////////////////
// (uncomment for more data)
/*
for (i = 0; i < numberOfVoters; i+=5) {
const j = BigNumber.from(i);
console.log(
`Voter ${i} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j))[0]).toString(),
`Voter ${i+1} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(1)))[0]).toString(),
`Voter ${i+2} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(2)))[0]).toString(),
`Voter ${i+3} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(3)))[0]).toString(),
`Voter ${i+4} sqrt: `,
((await GovernanceLottery.lotteryUserData(id,j.add(4)))[0]).toString(),
"\n",
)
}
for (i = 0; i < numberOfVoters; i+=5) {
console.log(
`Voter ${i} ether used: `,
gasUsedArray[i],
`Voter ${i+1} ether used: `,
gasUsedArray[i+1],
`Voter ${i+2} ether used: `,
gasUsedArray[i+2],
`Voter ${i+3} ether used: `,
gasUsedArray[i+3],
`Voter ${i+4} ether used: `,
gasUsedArray[i+4],
"\n",
)
}
*/
await sendr('evm_revert', [snapshotIdArray[2]])
///////////////////////////////// VOTE WITHOUT COMPENSATION //////////////////////////////////////
let gasUsedWithoutCompensation = []
await multiGov.setGasCompensations(pE(100000))
for (let i = 0; i < 10; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
if (randN > 0) {
response = await gov.castDelegatedVote(votingAddressArray[i], id, true, overrides)
} else {
response = await gov.castDelegatedVote(votingAddressArray[i], id, false, overrides)
}
const receipt = await response.wait()
gasUsedWithoutCompensation[i] = receipt.cumulativeGasUsed
}
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
let gov = await GovernanceContract.connect(signerArmy[i])
let randN = rand(i * 5, i * 6)
randN = randN % 2
let response
if (randN > 0) {
response = await gov.castVote(id, true, overrides)
} else {
response = await gov.castVote(id, false, overrides)
}
const receipt = await response.wait()
gasUsedWithoutCompensation[i] = receipt.cumulativeGasUsed
}
await multiGov.setGasCompensations(pE(100))
//////////////////////////////// GET STATE ///////////////////////////////
state = await GovernanceContract.state(id)
expect(state).to.be.equal(ProposalState.Active)
///////////////////////////// VOTING GAS INFO ///////////////////////////////////
let gasUsedSumNoComp = BigNumber.from(0)
let gasUsedSum = BigNumber.from(0)
let gasSumDiff = BigNumber.from(0)
let gasUsedSumNoCompDel = BigNumber.from(0)
let gasUsedSumDel = BigNumber.from(0)
let gasSumDiffDel = BigNumber.from(0)
for (let i = 0; i < 10; i++) {
gasUsedSumDel = gasUsedSumDel.add(gasUsedArray[i])
gasUsedSumNoCompDel = gasUsedSumNoCompDel.add(gasUsedWithoutCompensation[i])
gasSumDiffDel = gasSumDiffDel.add(signerArmyBalanceDiff[i])
}
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
gasUsedSum = gasUsedSum.add(gasUsedArray[i])
gasUsedSumNoComp = gasUsedSumNoComp.add(gasUsedWithoutCompensation[i])
gasSumDiff = gasSumDiff.add(signerArmyBalanceDiff[i])
}
const gasUsedAverageNoCompDel = gasUsedSumNoCompDel.div(10)
const gasUsedAverageDel = gasUsedSumDel.div(10)
const gasSumAverageDiffDel = gasSumDiffDel.div(10)
const gasUsedAverageNoComp = gasUsedSumNoComp.div(numberOfVoters - 10)
const gasUsedAverage = gasUsedSum.div(numberOfVoters - 10)
const gasSumAverageDiff = gasSumDiff.div(numberOfVoters - 10)
console.log(
'\n',
'----------------------------CAST VOTE INFO------------------------',
'\n',
'Gas use average: ',
gasUsedAverage.toString(),
'\n',
'Gas use without compensation average: ',
gasUsedAverageNoComp.toString(),
'\n',
'Gas diff average: ',
gasSumAverageDiff.toString(),
'\n',
'Gas compensated in average: ',
gasUsedAverage.sub(gasSumAverageDiff).toString(),
'\n',
'--------------------------------------------------------------------',
'\n',
)
console.log(
'\n',
'----------------------------CAST DELEGATED VOTE INFO------------------------',
'\n',
'Gas use average: ',
gasUsedAverageDel.toString(),
'\n',
'Gas use without compensation average: ',
gasUsedAverageNoCompDel.toString(),
'\n',
'Gas diff average: ',
gasSumAverageDiffDel.toString(),
'\n',
'Gas compensated in average: ',
gasUsedAverageDel.sub(gasSumAverageDiffDel).toString(),
'\n',
'--------------------------------------------------------------------',
'\n',
)
/////////////////////////////// INCREMENT AGAIN //////////////////////////////////
await minewait(
(
await GovernanceContract.VOTING_PERIOD()
)
.add(await GovernanceContract.EXECUTION_DELAY())
.add(10000)
.toNumber(),
)
////////////// EXECUTE
if (BigNumber.from(await GovernanceContract.state(id)).eq(ProposalState.Defeated)) {
await expect(GovernanceContract.execute(id)).to.be.reverted
} else {
await expect(GovernanceContract.execute(id)).to.not.be.reverted
}
})
})
after(async function () {
await ethers.provider.send('hardhat_reset', [
{
forking: {
jsonRpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
blockNumber: process.env.use_latest_block == 'true' ? undefined : config.forkBlockNumber,
},
},
])
})
})

View File

@ -0,0 +1,71 @@
const { ethers } = require('hardhat')
const { expect } = require('chai')
const config = require('../../config')
const { getSignerFromAddress, takeSnapshot, revertSnapshot } = require('../utils')
describe('V3 governance tests', () => {
let snapshotId
//// CONTRACTS
let torn = config.TORN
let gov
//// IMPERSONATED ACCOUNTS
let tornWhale
//// HELPER FN
let getToken = async (tokenAddress) => {
return await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', tokenAddress)
}
before(async function () {
tornWhale = await getSignerFromAddress(config.tornWhale)
gov = (await ethers.getContractAt('GovernanceStakingUpgrade', config.governance)).connect(tornWhale)
snapshotId = await takeSnapshot()
})
describe('#lock functionality', () => {
it('should be able to lock/unlock torn in governance', async () => {
const [sender] = await ethers.getSigners()
const value = ethers.utils.parseEther('1000')
const tornToken = await (await getToken(torn)).connect(tornWhale)
await tornToken.transfer(sender.address, value)
await tornToken.connect(sender).approve(gov.address, value)
const ethBalanceBeforeLock = await ethers.provider.getBalance(sender.address)
const tokenBalanceBeforeLock = await tornToken.balanceOf(sender.address)
let tx = await gov.connect(sender).lockWithApproval(value)
let receipt = await tx.wait()
let txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice)
const ethBalanceAfterLock = await ethers.provider.getBalance(sender.address)
const tokenBalanceAfterLock = await tornToken.balanceOf(sender.address)
expect(ethBalanceAfterLock).to.be.equal(ethBalanceBeforeLock.sub(txFee))
expect(tokenBalanceAfterLock).to.be.equal(tokenBalanceBeforeLock.sub(value))
const lockedBalanceAfterLock = await gov.lockedBalance(sender.address)
expect(lockedBalanceAfterLock).to.be.equal(value)
tx = await gov.connect(sender).unlock(value)
receipt = await tx.wait()
txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice)
const ethBalanceAfterUnlock = await ethers.provider.getBalance(sender.address)
const tokenBalanceAfterUnlock = await tornToken.balanceOf(sender.address)
expect(ethBalanceAfterUnlock).to.be.equal(ethBalanceAfterLock.sub(txFee))
expect(tokenBalanceAfterUnlock).to.be.equal(tokenBalanceBeforeLock)
const lockedBalanceAfterUnlock = await gov.lockedBalance(sender.address)
expect(lockedBalanceAfterUnlock).to.be.equal(0)
})
})
afterEach(async () => {
await revertSnapshot(snapshotId)
snapshotId = await takeSnapshot()
})
})

View File

@ -1,35 +0,0 @@
module.exports = {
networks: {
// development: {
// // host: '127.0.0.1', // Localhost (default: none)
// // port: 8545, // Standard Ethereum port (default: none)
// network_id: '*', // Any network (default: none)
// accounts: 20,
// },
coverage: {
host: 'localhost',
network_id: '*',
port: 8554, // <-- If you change this, also set the port option in .solcover.js.
gas: 0xfffffffffff, // <-- Use this high gas value
gasPrice: 0x01, // <-- Use this low gas price
},
},
mocha: {
// timeout: 100000
},
compilers: {
solc: {
version: '0.6.12',
docker: false,
settings: {
optimizer: {
enabled: true,
runs: 200,
},
// evmVersion: "byzantium"
},
},
},
}

File diff suppressed because it is too large Load Diff

1004
yarn.lock

File diff suppressed because it is too large Load Diff