mirror of
https://github.com/tornadocash/tornado-governance
synced 2024-02-02 14:53:55 +01:00
Merge pull request #4 from Tisamenus/master
Upgrade to version 2. Proposal 9. https://tornadocash.eth.link/governance/9
This commit is contained in:
commit
c312c5fb43
@ -7,4 +7,3 @@ end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
© 2020 GitHub, Inc.
|
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
||||
ETHERSCAN_KEY=
|
||||
INFURA_KEY=
|
||||
use_latest_block=false
|
@ -5,7 +5,7 @@
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
@ -21,6 +21,7 @@
|
||||
"semi": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"require-await": "error"
|
||||
"require-await": "error",
|
||||
"prettier/prettier": ["error", { "printWidth": 110 }]
|
||||
}
|
||||
}
|
||||
|
70
.github/workflows/build.yml
vendored
70
.github/workflows/build.yml
vendored
@ -9,74 +9,16 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }}
|
||||
INFURA_KEY: ${{ secrets.INFURA_KEY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 14
|
||||
- run: yarn install
|
||||
- run: yarn test
|
||||
- run: yarn hardhat compile
|
||||
- run: yarn lint
|
||||
- name: Telegram Failure Notification
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
if: failure()
|
||||
with:
|
||||
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
|
||||
format: markdown
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: NPM login
|
||||
# NPM doesn't understand env vars and needs auth file lol
|
||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set vars
|
||||
id: vars
|
||||
run: |
|
||||
echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/v})"
|
||||
echo "::set-output name=repo_name::$(echo ${GITHUB_REPOSITORY#*/})"
|
||||
- name: Check package.json version vs tag
|
||||
run: |
|
||||
[ ${{ steps.vars.outputs.version }} = $(grep '"version":' package.json | grep -o "[0-9.]*") ] || (echo "Git tag doesn't match version in package.json" && false)
|
||||
- name: Publish to npm
|
||||
run: npm publish
|
||||
|
||||
- name: Create GitHub Release Draft
|
||||
uses: actions/create-release@v1
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.vars.outputs.version }}
|
||||
draft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Telegram Notification
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
with:
|
||||
message: 🚀 Published [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}) version [${{ steps.vars.outputs.version }}](https://www.npmjs.com/package/${{ steps.vars.outputs.repo_name }}/v/${{ steps.vars.outputs.version }}) to npm
|
||||
format: markdown
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
|
||||
- name: Telegram Failure Notification
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
if: failure()
|
||||
with:
|
||||
message: ❗ Failed to publish [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
|
||||
format: markdown
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
- run: yarn hardhat test
|
||||
|
101
.gitignore
vendored
101
.gitignore
vendored
@ -1,99 +1,6 @@
|
||||
node_modules
|
||||
cache
|
||||
artifacts
|
||||
build
|
||||
.vscode
|
||||
/index.js
|
||||
flats/*
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# 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
|
||||
|
||||
# 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 output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
ERC20Tornado_flat.sol
|
||||
ETHTornado_flat.sol
|
||||
dist
|
||||
.env
|
||||
|
@ -1,3 +1,8 @@
|
||||
.vscode
|
||||
.idea
|
||||
cache
|
||||
artifacts
|
||||
build
|
||||
scripts
|
||||
dist
|
||||
README.md
|
||||
contracts/v2-vault-and-gas/libraries/EtherSend.sol
|
19
README.md
19
README.md
@ -1,13 +1,14 @@
|
||||
# Tornado.Cash Governance [![Build Status](https://github.com/tornadocash/tornado-governance/workflows/build/badge.svg)](https://github.com/tornadocash/tornado-governance/actions) [![npm](https://img.shields.io/npm/v/tornado-governance)](https://www.npmjs.com/package/tornado-governance)
|
||||
# tornado-governance
|
||||
|
||||
Usage:
|
||||
## 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.
|
||||
|
||||
```
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tornadocash/tornado-governance.git
|
||||
yarn
|
||||
cp .env.example .env
|
||||
cp .env.example .env # you must enter your details into .env
|
||||
yarn test
|
||||
```
|
||||
|
||||
## How to upgrade implementation
|
||||
|
||||
1. Make sure once you deploy new Governance implementation, call `initialize` methods right after it.
|
||||
```
|
@ -1,5 +0,0 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "torn-token/contracts/mocks/TORNMock.sol";
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
contract Configuration {
|
||||
@ -20,7 +21,7 @@ contract Configuration {
|
||||
/// @notice If the outcome of a proposal changes during CLOSING_PERIOD, the vote will be extended by VOTE_EXTEND_TIME (no more than once)
|
||||
uint256 public VOTE_EXTEND_TIME;
|
||||
|
||||
modifier onlySelf {
|
||||
modifier onlySelf() {
|
||||
require(msg.sender == address(this), "Governance: unauthorized");
|
||||
_;
|
||||
}
|
||||
@ -29,7 +30,7 @@ contract Configuration {
|
||||
EXECUTION_DELAY = 2 days;
|
||||
EXECUTION_EXPIRATION = 3 days;
|
||||
QUORUM_VOTES = 25000e18; // 0.25% of TORN
|
||||
PROPOSAL_THRESHOLD = 1000e18; // 0.01% of TORN
|
||||
PROPOSAL_THRESHOLD = 1000e18; // 0.1% of TORN
|
||||
VOTING_DELAY = 75 seconds;
|
||||
VOTING_PERIOD = 3 days;
|
||||
CLOSING_PERIOD = 1 hours;
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
import "./Core.sol";
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
@ -13,7 +14,15 @@ import "./Configuration.sol";
|
||||
contract Governance is Initializable, Configuration, Delegation, EnsResolve {
|
||||
using SafeMath for uint256;
|
||||
/// @notice Possible states that a proposal may be in
|
||||
enum ProposalState { Pending, Active, Defeated, Timelocked, AwaitingExecution, Executed, Expired }
|
||||
enum ProposalState {
|
||||
Pending,
|
||||
Active,
|
||||
Defeated,
|
||||
Timelocked,
|
||||
AwaitingExecution,
|
||||
Executed,
|
||||
Expired
|
||||
}
|
||||
|
||||
struct Proposal {
|
||||
// Creator of the proposal
|
||||
@ -172,7 +181,7 @@ contract Governance is Initializable, Configuration, Delegation, EnsResolve {
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
function execute(uint256 proposalId) external virtual payable {
|
||||
function execute(uint256 proposalId) external payable virtual {
|
||||
require(state(proposalId) == ProposalState.AwaitingExecution, "Governance::execute: invalid proposal state");
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
proposal.executed = true;
|
||||
@ -274,7 +283,7 @@ contract Governance is Initializable, Configuration, Delegation, EnsResolve {
|
||||
return proposals.length - 1;
|
||||
}
|
||||
|
||||
function getBlockTimestamp() internal virtual view returns (uint256) {
|
||||
function getBlockTimestamp() internal view virtual returns (uint256) {
|
||||
// solium-disable-next-line security/no-block-members
|
||||
return block.timestamp;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
import "@openzeppelin/contracts/proxy/TransparentUpgradeableProxy.sol";
|
||||
@ -12,11 +13,7 @@ contract LoopbackProxy is TransparentUpgradeableProxy, EnsResolve {
|
||||
/**
|
||||
* @dev Initializes an upgradeable proxy backed by the implementation at `_logic`.
|
||||
*/
|
||||
constructor(bytes32 _logic, bytes memory _data)
|
||||
public
|
||||
payable
|
||||
TransparentUpgradeableProxy(resolve(_logic), address(this), _data)
|
||||
{}
|
||||
constructor(address _logic, bytes memory _data) public payable TransparentUpgradeableProxy(_logic, address(this), _data) {}
|
||||
|
||||
/**
|
||||
* @dev Override to allow admin (itself) access the fallback function.
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
contract Dummy {
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
@ -11,12 +12,16 @@ contract MockGovernance is Governance {
|
||||
time = time_;
|
||||
}
|
||||
|
||||
function getBlockTimestamp() internal override view returns (uint256) {
|
||||
function getBlockTimestamp() internal view override returns (uint256) {
|
||||
// solium-disable-next-line security/no-block-members
|
||||
return time;
|
||||
}
|
||||
|
||||
function resolve(bytes32 addr) public override view returns (address) {
|
||||
function setTorn(address torna) external {
|
||||
torn = TORN(torna);
|
||||
}
|
||||
|
||||
function resolve(bytes32 addr) public view override returns (address) {
|
||||
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||
}
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
import "../LoopbackProxy.sol";
|
||||
|
||||
contract MockProxy is LoopbackProxy {
|
||||
constructor(bytes32 _logic, bytes memory _data) public payable LoopbackProxy(_logic, _data) {}
|
||||
constructor(address _logic, bytes memory _data) public payable LoopbackProxy(_logic, _data) {}
|
||||
|
||||
function resolve(bytes32 addr) public override view returns (address) {
|
||||
function resolve(bytes32 addr) public view override returns (address) {
|
||||
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
import "./Dummy.sol";
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
interface IGovernance {
|
@ -1,4 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
@ -12,14 +13,20 @@ contract NewImplementation is MockGovernance {
|
||||
uint256 public newVariable;
|
||||
event Overriden(uint256 x);
|
||||
|
||||
function execute(uint256 proposalId) public override payable {
|
||||
function execute(uint256 proposalId) public payable override {
|
||||
newVariable = 999;
|
||||
emit Overriden(proposalId);
|
||||
}
|
||||
}
|
||||
|
||||
contract ProposalUpgrade {
|
||||
address public immutable newLogic;
|
||||
|
||||
constructor(address _newLogic) public {
|
||||
newLogic = _newLogic;
|
||||
}
|
||||
|
||||
function executeProposal() public {
|
||||
IProxy(address(this)).upgradeTo(0xF7E3e47e06F1bDDecb1b2F3a7F60b6b25fd2e233);
|
||||
IProxy(address(this)).upgradeTo(newLogic);
|
||||
}
|
||||
}
|
32
contracts/v1/Mocks/TORNMock.sol
Normal file
32
contracts/v1/Mocks/TORNMock.sol
Normal file
@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "torn-token/contracts/mocks/TORNMock.sol";
|
||||
|
||||
struct Recipient2 {
|
||||
address to;
|
||||
uint256 amount;
|
||||
}
|
||||
|
||||
contract TORNMock2 is TORNMock {
|
||||
constructor(
|
||||
address _governance,
|
||||
uint256 _pausePeriod,
|
||||
Recipient2[] memory vesting
|
||||
) public TORNMock(solve(_governance), _pausePeriod, solve2(vesting)) {}
|
||||
|
||||
function solve(address x) private returns (bytes32) {
|
||||
return bytes32(uint256(x) << 96);
|
||||
}
|
||||
|
||||
function solve2(Recipient2[] memory vesting) private returns (Recipient[] memory) {
|
||||
Recipient[] memory realVesting = new Recipient[](vesting.length);
|
||||
for (uint256 i = 0; i < vesting.length; i++) {
|
||||
realVesting[i].to = solve(vesting[i].to);
|
||||
realVesting[i].amount = vesting[i].amount;
|
||||
}
|
||||
return realVesting;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
37
contracts/v2-vault-and-gas/ProposalChanges.md
Normal file
37
contracts/v2-vault-and-gas/ProposalChanges.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Tornado Governance Changes Documentation
|
||||
|
||||
`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 Vault Upgrade (GovernanceVaultUpgrade.sol)
|
||||
|
||||
`GovernanceVaultUpgrade` is the first major upgrade to tornado governance. This upgrade introduces new logic which is used to communicate with `TornVault` from the governance contract. The motivation behind this upgrade:
|
||||
|
||||
- split DAO member locked TORN from vesting locked TORN.
|
||||
- block Governance from being able to interact with user TORN.
|
||||
|
||||
To solve point 1 of the formerly stated problems, and to reduce the logic bloat of the lock and unlock functionalities, we have opted for calculating the amount of user TORN locked in the governance contract. The calculations and explanations may be found [here](https://github.com/h-ivor/tornado-lottery-period/blob/final_with_auction/scripts/balance_estimation.md).
|
||||
|
||||
### Additions and changes
|
||||
|
||||
| Function/variable signature | is addition or change? | describe significance |
|
||||
| ---------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `_transferTokens(address,uint256)` | change | instead of transferring to the governance contract, funds are now transferred to the torn vault with a `transferFrom` call, this has an effect on both the `lock` and `lockWithApproval` function |
|
||||
| `unlock(uint256)` | change | unlock now triggers `withdrawTorn(address,uint256)` within the vault which reverts on an unsuccessful transfer (safeTransfer) |
|
||||
| `version` | addition | tells current version of governance contract |
|
||||
| `address immutable userVault` | addition | address of the deployed vault |
|
||||
|
||||
### Tornado Vault (TornadoVault.sol)
|
||||
|
||||
The compliment to the above upgrade. Stores user TORN, does not keep records of it. Serves exclusively for deposits and withdrawals. Works in effect as personal store of TORN for a user with the balance being user for voting. Locking mechanisms are still in effect.
|
||||
|
||||
| Function/variable signature | describe significance |
|
||||
| ------------------------------- | --------------------------------------------------- |
|
||||
| `withdrawTorn(address,uint256)` | used for withdrawing TORN balance to users' account |
|
89
contracts/v2-vault-and-gas/VaultAndGasProposal.sol
Normal file
89
contracts/v2-vault-and-gas/VaultAndGasProposal.sol
Normal file
@ -0,0 +1,89 @@
|
||||
// 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 = 100 ether;
|
||||
uint96 minBuyAmount = 1.51 ether;
|
||||
uint256 minBidInTorn = 0.01 ether;
|
||||
uint256 fundingThreshold = 5 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
|
||||
);
|
||||
}
|
||||
}
|
59
contracts/v2-vault-and-gas/auction/Auction.md
Normal file
59
contracts/v2-vault-and-gas/auction/Auction.md
Normal file
@ -0,0 +1,59 @@
|
||||
# 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.
|
58
contracts/v2-vault-and-gas/auction/TornadoAuctionHandler.sol
Normal file
58
contracts/v2-vault-and-gas/auction/TornadoAuctionHandler.sol
Normal file
@ -0,0 +1,58 @@
|
||||
// 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");
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// 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);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.12;
|
||||
|
||||
interface IPayableGovernance {
|
||||
function receiveEther() external payable returns (bool);
|
||||
}
|
23
contracts/v2-vault-and-gas/auction/interfaces/IWETH.sol
Normal file
23
contracts/v2-vault-and-gas/auction/interfaces/IWETH.sol
Normal file
@ -0,0 +1,23 @@
|
||||
// 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);
|
||||
}
|
49
contracts/v2-vault-and-gas/gas/GasCompensationVault.sol
Normal file
49
contracts/v2-vault-and-gas/gas/GasCompensationVault.sol
Normal file
@ -0,0 +1,49 @@
|
||||
// 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 {}
|
||||
}
|
58
contracts/v2-vault-and-gas/gas/GasCompensator.sol
Normal file
58
contracts/v2-vault-and-gas/gas/GasCompensator.sol
Normal file
@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.12;
|
||||
|
||||
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
|
||||
|
||||
interface IGasCompensationVault {
|
||||
function compensateGas(address recipient, uint256 gasAmount) external;
|
||||
|
||||
function withdrawToGovernance(uint256 amount) external;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice This abstract contract is used to add gas compensation functionality to a contract.
|
||||
* */
|
||||
abstract contract GasCompensator {
|
||||
using SafeMath for uint256;
|
||||
|
||||
/// @notice this vault is necessary for the gas compensation functionality to work
|
||||
IGasCompensationVault public immutable gasCompensationVault;
|
||||
|
||||
constructor(address _gasCompensationVault) public {
|
||||
gasCompensationVault = IGasCompensationVault(_gasCompensationVault);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice modifier which should compensate gas to account if eligible
|
||||
* @dev Consider reentrancy, repeated calling of the function being compensated, eligibility.
|
||||
* @param account address to be compensated
|
||||
* @param eligible if the account is eligible for compensations or not
|
||||
* @param extra extra amount in gas to be compensated, will be multiplied by basefee
|
||||
* */
|
||||
modifier gasCompensation(
|
||||
address account,
|
||||
bool eligible,
|
||||
uint256 extra
|
||||
) {
|
||||
if (eligible) {
|
||||
uint256 startGas = gasleft();
|
||||
_;
|
||||
uint256 gasToCompensate = startGas.sub(gasleft()).add(extra).add(10e3);
|
||||
|
||||
gasCompensationVault.compensateGas(account, gasToCompensate);
|
||||
} else {
|
||||
_;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice inheritable unimplemented function to withdraw ether from the vault
|
||||
* */
|
||||
function withdrawFromHelper(uint256 amount) external virtual;
|
||||
|
||||
/**
|
||||
* @notice inheritable unimplemented function to deposit ether into the vault
|
||||
* */
|
||||
function setGasCompensations(uint256 _gasCompensationsLimit) external virtual;
|
||||
}
|
155
contracts/v2-vault-and-gas/gas/GovernanceGasUpgrade.sol
Normal file
155
contracts/v2-vault-and-gas/gas/GovernanceGasUpgrade.sol
Normal file
@ -0,0 +1,155 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.12;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import { GovernanceVaultUpgrade } from "../vault/GovernanceVaultUpgrade.sol";
|
||||
import { GasCompensator } from "./GasCompensator.sol";
|
||||
import { Math } from "@openzeppelin/contracts/math/Math.sol";
|
||||
|
||||
/**
|
||||
* @notice This contract should upgrade governance to be able to compensate gas for certain actions.
|
||||
* These actions are set to castVote, castDelegatedVote in this contract.
|
||||
* */
|
||||
contract GovernanceGasUpgrade is GovernanceVaultUpgrade, GasCompensator {
|
||||
/**
|
||||
* @notice constructor
|
||||
* @param _gasCompLogic gas compensation vault address
|
||||
* @param _userVault tornado vault address
|
||||
* */
|
||||
constructor(address _gasCompLogic, address _userVault)
|
||||
public
|
||||
GovernanceVaultUpgrade(_userVault)
|
||||
GasCompensator(_gasCompLogic)
|
||||
{}
|
||||
|
||||
/// @notice check that msg.sender is multisig
|
||||
modifier onlyMultisig() {
|
||||
require(msg.sender == returnMultisigAddress(), "only multisig");
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice receive ether function, does nothing but receive ether
|
||||
* */
|
||||
receive() external payable {}
|
||||
|
||||
/**
|
||||
* @notice function to add a certain amount of ether for gas compensations
|
||||
* @dev send ether is used in the logic as we don't expect multisig to make a reentrancy attack on governance
|
||||
* @param gasCompensationsLimit the amount of gas to be compensated
|
||||
* */
|
||||
function setGasCompensations(uint256 gasCompensationsLimit) external virtual override onlyMultisig {
|
||||
require(payable(address(gasCompensationVault)).send(Math.min(gasCompensationsLimit, address(this).balance)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice function to withdraw funds from the gas compensator
|
||||
* @dev send ether is used in the logic as we don't expect multisig to make a reentrancy attack on governance
|
||||
* @param amount the amount of ether to withdraw
|
||||
* */
|
||||
function withdrawFromHelper(uint256 amount) external virtual override onlyMultisig {
|
||||
gasCompensationVault.withdrawToGovernance(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice function to cast callers votes on a proposal
|
||||
* @dev IMPORTANT: This function uses the gasCompensation modifier.
|
||||
* as such this function can trigger a payable fallback.
|
||||
It is not possible to vote without revert more than once,
|
||||
without hasAccountVoted being true, eliminating gas refunds in this case.
|
||||
Gas compensation is also using the low level send(), forwarding 23000 gas
|
||||
as to disallow further logic execution above that threshold.
|
||||
* @param proposalId id of proposal account is voting on
|
||||
* @param support true if yes false if no
|
||||
* */
|
||||
function castVote(uint256 proposalId, bool support)
|
||||
external
|
||||
virtual
|
||||
override
|
||||
gasCompensation(
|
||||
msg.sender,
|
||||
!hasAccountVoted(proposalId, msg.sender) && !checkIfQuorumReached(proposalId),
|
||||
(msg.sender == tx.origin ? 21e3 : 0)
|
||||
)
|
||||
{
|
||||
_castVote(msg.sender, proposalId, support);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice function to cast callers votes and votes delegated to the caller
|
||||
* @param from array of addresses that should have delegated to voter
|
||||
* @param proposalId id of proposal account is voting on
|
||||
* @param support true if yes false if no
|
||||
* */
|
||||
function castDelegatedVote(
|
||||
address[] memory from,
|
||||
uint256 proposalId,
|
||||
bool support
|
||||
) external virtual override {
|
||||
require(from.length > 0, "Can not be empty");
|
||||
_castDelegatedVote(from, proposalId, support, !hasAccountVoted(proposalId, msg.sender) && !checkIfQuorumReached(proposalId));
|
||||
}
|
||||
|
||||
/// @notice checker for success on deployment
|
||||
/// @return returns precise version of governance
|
||||
function version() external pure virtual override returns (string memory) {
|
||||
return "2.lottery-and-gas-upgrade";
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice function to check if quorum has been reached on a given proposal
|
||||
* @param proposalId id of proposal
|
||||
* @return true if quorum has been reached
|
||||
* */
|
||||
function checkIfQuorumReached(uint256 proposalId) public view returns (bool) {
|
||||
return (proposals[proposalId].forVotes + proposals[proposalId].againstVotes >= QUORUM_VOTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice function to check if account has voted on a proposal
|
||||
* @param proposalId id of proposal account should have voted on
|
||||
* @param account address of the account
|
||||
* @return true if acc has voted
|
||||
* */
|
||||
function hasAccountVoted(uint256 proposalId, address account) public view returns (bool) {
|
||||
return proposals[proposalId].receipts[account].hasVoted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice function to retrieve the multisig address
|
||||
* @dev reasoning: if multisig changes we need governance to approve the next multisig address,
|
||||
* so simply inherit in a governance upgrade from this function and set the new address
|
||||
* @return the multisig address
|
||||
* */
|
||||
function returnMultisigAddress() public pure virtual returns (address) {
|
||||
return 0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice This should handle the logic of the external function
|
||||
* @dev IMPORTANT: This function uses the gasCompensation modifier.
|
||||
* as such this function can trigger a payable fallback.
|
||||
* It is not possible to vote without revert more than once,
|
||||
* without hasAccountVoted being true, eliminating gas refunds in this case.
|
||||
* Gas compensation is also using the low level send(), forwarding 23000 gas
|
||||
* as to disallow further logic execution above that threshold.
|
||||
* @param from array of addresses that should have delegated to voter
|
||||
* @param proposalId id of proposal account is voting on
|
||||
* @param support true if yes false if no
|
||||
* @param gasCompensated true if gas should be compensated (given all internal checks pass)
|
||||
* */
|
||||
function _castDelegatedVote(
|
||||
address[] memory from,
|
||||
uint256 proposalId,
|
||||
bool support,
|
||||
bool gasCompensated
|
||||
) internal gasCompensation(msg.sender, gasCompensated, (msg.sender == tx.origin ? 21e3 : 0)) {
|
||||
for (uint256 i = 0; i < from.length; i++) {
|
||||
address delegator = from[i];
|
||||
require(delegatedTo[delegator] == msg.sender || delegator == msg.sender, "Governance: not authorized");
|
||||
require(!gasCompensated || !hasAccountVoted(proposalId, delegator), "Governance: voted already");
|
||||
_castVote(delegator, proposalId, support);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.12;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
interface IGovernanceVesting {
|
||||
function released() external view returns (uint256);
|
||||
}
|
8
contracts/v2-vault-and-gas/interfaces/ITornadoVault.sol
Normal file
8
contracts/v2-vault-and-gas/interfaces/ITornadoVault.sol
Normal file
@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.12;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
interface ITornadoVault {
|
||||
function withdrawTorn(address recipient, uint256 amount) external;
|
||||
}
|
17
contracts/v2-vault-and-gas/libraries/EtherSend.sol
Normal file
17
contracts/v2-vault-and-gas/libraries/EtherSend.sol
Normal file
@ -0,0 +1,17 @@
|
||||
// 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 }("");
|
||||
}
|
||||
}
|
26
contracts/v2-vault-and-gas/testing/GasCompensationVault.sol
Normal file
26
contracts/v2-vault-and-gas/testing/GasCompensationVault.sol
Normal file
@ -0,0 +1,26 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
16
contracts/v2-vault-and-gas/testing/MockProposal1.sol
Normal file
16
contracts/v2-vault-and-gas/testing/MockProposal1.sol
Normal file
@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.12;
|
||||
|
||||
import "tornado-governance/contracts/Governance.sol";
|
||||
|
||||
contract MockProposal1 {
|
||||
address public constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
|
||||
|
||||
function executeProposal() external {
|
||||
Governance gov = Governance(GovernanceAddress);
|
||||
|
||||
gov.setVotingPeriod(27000);
|
||||
require(gov.VOTING_PERIOD() == 27000, "Voting period change failed!");
|
||||
}
|
||||
}
|
16
contracts/v2-vault-and-gas/testing/OrderEncoderHelper.sol
Normal file
16
contracts/v2-vault-and-gas/testing/OrderEncoderHelper.sol
Normal file
@ -0,0 +1,16 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
43
contracts/v2-vault-and-gas/vault/GovernanceVaultUpgrade.sol
Normal file
43
contracts/v2-vault-and-gas/vault/GovernanceVaultUpgrade.sol
Normal file
@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.6.12;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import { Governance } from "../../v1/Governance.sol";
|
||||
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
|
||||
import { ITornadoVault } from "../interfaces/ITornadoVault.sol";
|
||||
|
||||
/// @title Version 2 Governance contract of the tornado.cash governance
|
||||
contract GovernanceVaultUpgrade is Governance {
|
||||
using SafeMath for uint256;
|
||||
|
||||
// vault which stores user TORN
|
||||
ITornadoVault public immutable userVault;
|
||||
|
||||
// call Governance v1 constructor
|
||||
constructor(address _userVault) public Governance() {
|
||||
userVault = ITornadoVault(_userVault);
|
||||
}
|
||||
|
||||
/// @notice Withdraws TORN from governance if conditions permit
|
||||
/// @param amount the amount of TORN to withdraw
|
||||
function unlock(uint256 amount) public virtual override {
|
||||
require(getBlockTimestamp() > canWithdrawAfter[msg.sender], "Governance: tokens are locked");
|
||||
lockedBalance[msg.sender] = lockedBalance[msg.sender].sub(amount, "Governance: insufficient balance");
|
||||
userVault.withdrawTorn(msg.sender, amount);
|
||||
}
|
||||
|
||||
/// @notice checker for success on deployment
|
||||
/// @return returns precise version of governance
|
||||
function version() external pure virtual returns (string memory) {
|
||||
return "2.vault-migration";
|
||||
}
|
||||
|
||||
/// @notice transfers tokens from the contract to the vault, withdrawals are unlock()
|
||||
/// @param owner account/contract which (this) spender will send to the user vault
|
||||
/// @param amount amount which spender will send to the user vault
|
||||
function _transferTokens(address owner, uint256 amount) internal virtual override {
|
||||
require(torn.transferFrom(owner, address(userVault), amount), "TORN: transferFrom failed");
|
||||
lockedBalance[owner] = lockedBalance[owner].add(amount);
|
||||
}
|
||||
}
|
21
contracts/v2-vault-and-gas/vault/TornadoVault.sol
Normal file
21
contracts/v2-vault-and-gas/vault/TornadoVault.sol
Normal file
@ -0,0 +1,21 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
3
flat.sh
3
flat.sh
@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
npx truffle-flattener contracts/Governance.sol > flats/Governance_flat.sol
|
||||
npx truffle-flattener contracts/LoopbackProxy.sol > flats/MyProxy_flat.sol
|
72
hardhat.config.js
Normal file
72
hardhat.config.js
Normal file
@ -0,0 +1,72 @@
|
||||
require('dotenv').config()
|
||||
require('@nomiclabs/hardhat-ethers')
|
||||
require('@nomiclabs/hardhat-etherscan')
|
||||
require('@nomiclabs/hardhat-waffle')
|
||||
require('hardhat-spdx-license-identifier')
|
||||
require('hardhat-storage-layout')
|
||||
require('hardhat-log-remover')
|
||||
require('hardhat-contract-sizer')
|
||||
|
||||
require('./tasks/deploy_proposal.js')
|
||||
/**
|
||||
* @type import('hardhat/config').HardhatUserConfig
|
||||
*/
|
||||
module.exports = {
|
||||
solidity: {
|
||||
compilers: [
|
||||
{
|
||||
version: '0.6.12',
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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://mainnet.infura.io/v3/${process.env.INFURA_KEY}`,
|
||||
blockNumber: 13042331,
|
||||
},
|
||||
initialBaseFeePerGas: 5,
|
||||
},
|
||||
localhost: {
|
||||
url: 'http://localhost:8545',
|
||||
timeout: 120000,
|
||||
},
|
||||
mainnet: {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`,
|
||||
accounts: ['900e9f0e8ce24c022026649c48a059fb6ffa0a2523811d797b47d789bf106def'], // random pk off keys.lol
|
||||
timeout: 2147483647,
|
||||
},
|
||||
},
|
||||
mocha: { timeout: 9999999999 },
|
||||
spdxLicenseIdentifier: {
|
||||
overwrite: true,
|
||||
runOnCompile: true,
|
||||
},
|
||||
etherscan: {
|
||||
apiKey: `${process.env.ETHERSCAN_KEY}`,
|
||||
},
|
||||
}
|
53
package.json
53
package.json
@ -1,43 +1,54 @@
|
||||
{
|
||||
"name": "tornado-governance",
|
||||
"version": "1.0.3",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"contracts/*"
|
||||
],
|
||||
"scripts": {
|
||||
"compile": "truffle compile",
|
||||
"test": "truffle test",
|
||||
"test:stacktrace": "yarn test --stacktrace",
|
||||
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||
"prettier:check": "prettier --check . --config .prettierrc",
|
||||
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||
"lint": "yarn eslint && yarn prettier:check"
|
||||
"lint": "yarn eslint && yarn prettier:check",
|
||||
"test:all": "yarn hardhat test",
|
||||
"test": "yarn test:all",
|
||||
"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"
|
||||
},
|
||||
"author": "Tornado.cash team <hello@tornado.cash>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^3.2.0-rc.0",
|
||||
"@ethersproject/bignumber": "^5.5.0",
|
||||
"@gnosis.pm/ido-contracts": "^0.5.0",
|
||||
"@openzeppelin/contracts": "3.2.0-rc.0",
|
||||
"@openzeppelin/upgrades-core": "^1.0.1",
|
||||
"torn-token": "^1.0.0"
|
||||
"torn-token": "^1.0.4",
|
||||
"tornado-governance": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openzeppelin/truffle-upgrades": "^1.0.2",
|
||||
"@ethersproject/testcases": "^5.5.0",
|
||||
"@nomiclabs/hardhat-ethers": "^2.0.2",
|
||||
"@nomiclabs/hardhat-etherscan": "^2.1.6",
|
||||
"@nomiclabs/hardhat-waffle": "^2.0.1",
|
||||
"@ticket721/e712": "^0.4.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bn-chai": "^1.0.1",
|
||||
"bn.js": "^5.1.3",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^7.8.1",
|
||||
"prettier": "^2.1.1",
|
||||
"prettier-plugin-solidity": "^1.0.0-alpha.57",
|
||||
"solhint-plugin-prettier": "^0.0.5",
|
||||
"solidity-coverage": "^0.7.10",
|
||||
"truffle": "^5.1.43",
|
||||
"truffle-flattener": "^1.4.4",
|
||||
"truffle-hdwallet-provider": "^1.0.17"
|
||||
"chai": "^4.3.4",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"ethereum-waffle": "^3.4.0",
|
||||
"ethers": "^5.5.1",
|
||||
"hardhat": "2.6.0",
|
||||
"hardhat-contract-sizer": "^2.0.3",
|
||||
"hardhat-log-remover": "^2.0.2",
|
||||
"hardhat-spdx-license-identifier": "^2.0.3",
|
||||
"hardhat-storage-layout": "^0.1.6",
|
||||
"prettier": "^2.3.2",
|
||||
"prettier-plugin-solidity": "^1.0.0-beta.17",
|
||||
"solhint-plugin-prettier": "^0.0.5"
|
||||
}
|
||||
}
|
||||
|
8299
resources/accounts.json
Normal file
8299
resources/accounts.json
Normal file
File diff suppressed because it is too large
Load Diff
44152
resources/hdnode.json
Normal file
44152
resources/hdnode.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,55 +0,0 @@
|
||||
// This module is used only for tests
|
||||
function send(method, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
web3.currentProvider.send(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method,
|
||||
params,
|
||||
},
|
||||
(err, res) => {
|
||||
return err ? reject(err) : resolve(res)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const takeSnapshot = async () => {
|
||||
return await send('evm_snapshot')
|
||||
}
|
||||
|
||||
const traceTransaction = async (tx) => {
|
||||
return await send('debug_traceTransaction', [tx, {}])
|
||||
}
|
||||
|
||||
const revertSnapshot = async (id) => {
|
||||
await send('evm_revert', [id])
|
||||
}
|
||||
|
||||
const mineBlock = async (timestamp) => {
|
||||
await send('evm_mine', [timestamp])
|
||||
}
|
||||
|
||||
const increaseTime = async (seconds) => {
|
||||
await send('evm_increaseTime', [seconds])
|
||||
}
|
||||
|
||||
const minerStop = async () => {
|
||||
await send('miner_stop', [])
|
||||
}
|
||||
|
||||
const minerStart = async () => {
|
||||
await send('miner_start', [])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
takeSnapshot,
|
||||
revertSnapshot,
|
||||
mineBlock,
|
||||
minerStop,
|
||||
minerStart,
|
||||
increaseTime,
|
||||
traceTransaction,
|
||||
}
|
21
scripts/helper/propose_proposal.js
Normal file
21
scripts/helper/propose_proposal.js
Normal file
@ -0,0 +1,21 @@
|
||||
require('dotenv').config()
|
||||
const { ethers } = require('hardhat')
|
||||
|
||||
async function propose(proposalArgs) {
|
||||
const proposer = proposalArgs[0]
|
||||
const ProposalContract = proposalArgs[1]
|
||||
|
||||
let GovernanceContract = await ethers.getContractAt(
|
||||
'contracts/v1/Governance.sol:Governance',
|
||||
'0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
|
||||
)
|
||||
GovernanceContract = await GovernanceContract.connect(proposer)
|
||||
|
||||
const response = await GovernanceContract.propose(ProposalContract.address, proposalArgs[2])
|
||||
|
||||
const id = await GovernanceContract.latestProposalIds(proposer.address)
|
||||
const state = await GovernanceContract.state(id)
|
||||
|
||||
return [response, id, state]
|
||||
}
|
||||
module.exports.propose = propose
|
@ -24,7 +24,8 @@ class PermitSigner extends EIP712Signer {
|
||||
}
|
||||
|
||||
async getSignature(privateKey) {
|
||||
const payload = this.getPayload()
|
||||
let payload = this.getPayload()
|
||||
payload.message.owner = payload.message.owner.address
|
||||
const { hex, v, r, s } = await this.sign(privateKey, payload)
|
||||
return {
|
||||
hex,
|
33
tasks/deploy_proposal.js
Normal file
33
tasks/deploy_proposal.js
Normal file
@ -0,0 +1,33 @@
|
||||
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)
|
||||
})
|
20
tasks/propose_proposal.js
Normal file
20
tasks/propose_proposal.js
Normal file
@ -0,0 +1,20 @@
|
||||
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)
|
||||
})
|
@ -1,722 +0,0 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
const util = require('ethereumjs-util')
|
||||
|
||||
const Governance = artifacts.require('./MockGovernance.sol')
|
||||
const Dummy = artifacts.require('./Dummy.sol')
|
||||
const Proposal = artifacts.require('./Proposal.sol')
|
||||
const Torn = artifacts.require('./TORNMock.sol')
|
||||
const TransparentUpgradeableProxy = artifacts.require('./MockProxy.sol')
|
||||
const ProposalStateChangeGovernance = artifacts.require('./ProposalStateChangeGovernance.sol')
|
||||
const NewImplementation = artifacts.require('./NewImplementation.sol')
|
||||
const ProposalUpgrade = artifacts.require('./ProposalUpgrade.sol')
|
||||
|
||||
const { PermitSigner } = require('../lib/Permit')
|
||||
|
||||
const { toBN, toChecksumAddress } = require('web3-utils')
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const BN = require('bn.js')
|
||||
const tornConfig = require('torn-token')
|
||||
const RLP = require('rlp')
|
||||
|
||||
const ProposalState = {
|
||||
Pending: 0,
|
||||
Active: 1,
|
||||
Defeated: 2,
|
||||
Timelocked: 3,
|
||||
AwaitingExecution: 4,
|
||||
Executed: 5,
|
||||
Expired: 6,
|
||||
}
|
||||
|
||||
const duration = {
|
||||
seconds: function (val) {
|
||||
return val
|
||||
},
|
||||
minutes: function (val) {
|
||||
return val * this.seconds(60)
|
||||
},
|
||||
hours: function (val) {
|
||||
return val * this.minutes(60)
|
||||
},
|
||||
days: function (val) {
|
||||
return val * this.hours(24)
|
||||
},
|
||||
weeks: function (val) {
|
||||
return val * this.days(7)
|
||||
},
|
||||
years: function (val) {
|
||||
return val * this.days(365)
|
||||
},
|
||||
}
|
||||
|
||||
async function getNextAddr(sender, offset = 0) {
|
||||
const nonce = await web3.eth.getTransactionCount(sender)
|
||||
return (
|
||||
'0x' +
|
||||
web3.utils
|
||||
.sha3(RLP.encode([sender, Number(nonce) + Number(offset)]))
|
||||
.slice(12)
|
||||
.substring(14)
|
||||
)
|
||||
}
|
||||
|
||||
contract('Governance', (accounts) => {
|
||||
let governance, dummy
|
||||
let proposer = accounts[3]
|
||||
let secondProposer = accounts[8]
|
||||
let snapshotId
|
||||
let timestamp = 1577836800 // 01/01/2020 00:00
|
||||
let torn
|
||||
let chainId
|
||||
let domain
|
||||
let votingDelay
|
||||
let votingPeriod
|
||||
let executionExpiration
|
||||
let executionDelay
|
||||
let extendTime
|
||||
let proposalStartTime
|
||||
let proposalEndTime
|
||||
let lockingPeriod
|
||||
let balanceProposer
|
||||
const cap = toBN(tornConfig.torn.cap)
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000))
|
||||
const miningPrivateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'
|
||||
const miningPublicKey = toChecksumAddress(
|
||||
'0x' + util.privateToAddress(Buffer.from(miningPrivateKey.slice(2), 'hex')).toString('hex'),
|
||||
)
|
||||
|
||||
before(async () => {
|
||||
chainId = await web3.eth.net.getId()
|
||||
const governanceExpectedAddr = await getNextAddr(accounts[0], 2)
|
||||
torn = await Torn.new(governanceExpectedAddr, duration.days(30), [
|
||||
{ to: miningPublicKey, amount: cap.toString() },
|
||||
])
|
||||
const governanceImplementation = await Governance.new()
|
||||
const calldata = governanceImplementation.contract.methods.initialize(torn.address).encodeABI()
|
||||
const proxy = await TransparentUpgradeableProxy.new(governanceImplementation.address, calldata)
|
||||
governance = await Governance.at(proxy.address)
|
||||
dummy = await Dummy.new()
|
||||
balanceProposer = cap.div(toBN(4))
|
||||
await torn.transfer(secondProposer, balanceProposer.div(toBN(2)), { from: miningPublicKey })
|
||||
await torn.transfer(proposer, balanceProposer, { from: miningPublicKey })
|
||||
await torn.setChainId(chainId)
|
||||
await governance.setTimestamp(timestamp)
|
||||
votingDelay = await governance.VOTING_DELAY()
|
||||
votingPeriod = await governance.VOTING_PERIOD()
|
||||
executionExpiration = await governance.EXECUTION_EXPIRATION()
|
||||
executionDelay = await governance.EXECUTION_DELAY()
|
||||
extendTime = await governance.VOTE_EXTEND_TIME()
|
||||
proposalStartTime = new BN(timestamp).add(votingDelay)
|
||||
proposalEndTime = votingPeriod.add(toBN(proposalStartTime))
|
||||
lockingPeriod = Number(extendTime) + Number(executionExpiration) + Number(executionDelay)
|
||||
domain = {
|
||||
name: await torn.name(),
|
||||
version: '1',
|
||||
chainId,
|
||||
verifyingContract: torn.address,
|
||||
}
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
beforeEach(async () => {
|
||||
await torn.approve(governance.address, cap.div(toBN(4)), { from: proposer })
|
||||
await governance.lockWithApproval(cap.div(toBN(4)), { from: proposer })
|
||||
const balance = await governance.lockedBalance(proposer)
|
||||
balance.should.be.eq.BN(cap.div(toBN(4)))
|
||||
})
|
||||
describe('#constructor', () => {
|
||||
it('should work', async () => {
|
||||
const proposalCount = await governance.proposalCount()
|
||||
proposalCount.should.be.eq.BN(0)
|
||||
|
||||
const p = await governance.proposals(0)
|
||||
p.proposer.should.be.equal(governance.address)
|
||||
p.target.should.be.equal('0x000000000000000000000000000000000000dEaD')
|
||||
p.endTime.should.be.eq.BN(toBN(0))
|
||||
p.forVotes.should.be.eq.BN(toBN(0))
|
||||
p.againstVotes.should.be.eq.BN(toBN(0))
|
||||
p.executed.should.be.equal(true)
|
||||
p.extended.should.be.equal(false)
|
||||
})
|
||||
})
|
||||
describe('#propose', () => {
|
||||
it('should work', async () => {
|
||||
const { logs } = await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
const proposalCount = await governance.proposalCount()
|
||||
proposalCount.should.be.eq.BN(1)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.proposer.should.be.equal(proposer)
|
||||
proposal.startTime.should.be.eq.BN(proposalStartTime)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
proposal.executed.should.be.equal(false)
|
||||
|
||||
// emit ProposalCreated(newProposal.id, msg.sender, target, startBlock, endBlock, description);
|
||||
logs[0].event.should.be.equal('ProposalCreated')
|
||||
logs[0].args.id.should.be.eq.BN(id)
|
||||
logs[0].args.proposer.should.be.eq.BN(proposer)
|
||||
logs[0].args.target.should.be.eq.BN(dummy.address)
|
||||
logs[0].args.description.should.be.eq.BN('dummy')
|
||||
logs[0].args.startTime.should.be.eq.BN(proposalStartTime)
|
||||
logs[0].args.endTime.should.be.eq.BN(votingPeriod.add(toBN(proposalStartTime)))
|
||||
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Pending)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
|
||||
const accountLock = await governance.canWithdrawAfter(proposer)
|
||||
accountLock.should.be.eq.BN(proposalEndTime.add(toBN(lockingPeriod)))
|
||||
})
|
||||
it('fails if target is not a contract', async () => {
|
||||
await governance
|
||||
.propose(accounts[9], 'dummy', { from: proposer })
|
||||
.should.be.rejectedWith('not a contract')
|
||||
})
|
||||
it('fails if proposer has already pending proposal', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance
|
||||
.propose(dummy.address, 'dummy', { from: proposer })
|
||||
.should.be.rejectedWith(
|
||||
'Governance::propose: one live proposal per proposer, found an already active proposal',
|
||||
)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
await governance
|
||||
.propose(dummy.address, 'dummy', { from: proposer })
|
||||
.should.be.rejectedWith(
|
||||
'Governance::propose: one live proposal per proposer, found an already active proposal',
|
||||
)
|
||||
})
|
||||
it('fails if proposer does not have voting power', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(999))
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterBob })
|
||||
await governance
|
||||
.propose(dummy.address, 'dummy', { from: voterBob })
|
||||
.should.be.rejectedWith('Governance::propose: proposer votes below proposal threshold.')
|
||||
})
|
||||
})
|
||||
describe('#castVote', () => {
|
||||
it('should work if support is true', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
const { logs } = await governance.castVote(id, true, { from: proposer })
|
||||
logs[0].event.should.be.equal('Voted')
|
||||
logs[0].args.voter.should.be.equal(proposer)
|
||||
logs[0].args.proposalId.should.be.eq.BN(id)
|
||||
logs[0].args.support.should.be.equal(true)
|
||||
logs[0].args.votes.should.be.eq.BN(votesCount)
|
||||
await governance.getReceipt(id, proposer)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(votesCount)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
})
|
||||
it('should work if support is false', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
const { logs } = await governance.castVote(id, false, { from: proposer })
|
||||
logs[0].event.should.be.equal('Voted')
|
||||
logs[0].args.voter.should.be.equal(proposer)
|
||||
logs[0].args.proposalId.should.be.eq.BN(id)
|
||||
logs[0].args.support.should.be.equal(false)
|
||||
logs[0].args.votes.should.be.eq.BN(votesCount)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount)
|
||||
})
|
||||
it('should be able to change the choice later if already voted before', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
const { logs } = await governance.castVote(id, false, { from: proposer })
|
||||
logs[0].event.should.be.equal('Voted')
|
||||
logs[0].args.voter.should.be.equal(proposer)
|
||||
logs[0].args.proposalId.should.be.eq.BN(id)
|
||||
logs[0].args.support.should.be.equal(false)
|
||||
logs[0].args.votes.should.be.eq.BN(votesCount)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount)
|
||||
})
|
||||
it('should work if there are multiple voters', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const voterAlice = accounts[7]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000)) // todo
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.transfer(voterAlice, tenThousandTorn.mul(toBN(2)), { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
await torn.approve(governance.address, tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterBob })
|
||||
await governance.lockWithApproval(tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
await governance.castVote(id, true, { from: voterAlice })
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(tenThousandTorn.mul(toBN(2)))
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount.add(tenThousandTorn))
|
||||
})
|
||||
it('fails if voter does not have voting power', async () => {
|
||||
const voterBob = accounts[5]
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance
|
||||
.castVote(id, false, { from: voterBob })
|
||||
.should.be.rejectedWith('Governance: balance is 0')
|
||||
})
|
||||
it('should be able to update number of votes count if the same decision is chosen after more tokens are locked', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000)) // todo
|
||||
const fiveThousandTorn = tenThousandTorn.div(toBN(2))
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.lockWithApproval(fiveThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
|
||||
let proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(toBN(0))
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount.add(fiveThousandTorn))
|
||||
|
||||
await governance.lockWithApproval(fiveThousandTorn, { from: voterBob })
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
|
||||
proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(toBN(0))
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount.add(tenThousandTorn))
|
||||
})
|
||||
it('extends time if the vote changes the outcome during the CLOSING_PERIOD', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const voterAlice = accounts[7]
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.transfer(voterAlice, tenThousandTorn.mul(toBN(2)), { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
await torn.approve(governance.address, tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterBob })
|
||||
await governance.lockWithApproval(tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
await governance.castVote(id, true, { from: voterAlice })
|
||||
|
||||
let MAX_EXTENDED_TIME = await governance.VOTE_EXTEND_TIME()
|
||||
let proposal = await governance.proposals(id)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
proposal = await governance.proposals(id)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime.add(MAX_EXTENDED_TIME))
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(duration.hours(5))))
|
||||
|
||||
const stateAfter = await governance.state(id)
|
||||
stateAfter.should.be.eq.BN(ProposalState.Active)
|
||||
})
|
||||
it('locks tokens after vote', async () => {
|
||||
const voterAlice = accounts[7]
|
||||
await torn.transfer(voterAlice, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterAlice })
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterAlice })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
|
||||
const lockBefore = await governance.canWithdrawAfter(voterAlice)
|
||||
lockBefore.should.be.eq.BN(toBN(0))
|
||||
|
||||
await governance.castVote(id, true, { from: voterAlice })
|
||||
|
||||
const lockAfter = await governance.canWithdrawAfter(voterAlice)
|
||||
lockAfter.should.be.eq.BN(proposalEndTime.add(toBN(lockingPeriod)))
|
||||
})
|
||||
it('does not reduce lock time', async () => {
|
||||
const voterAlice = accounts[7]
|
||||
await torn.transfer(voterAlice, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterAlice })
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterAlice })
|
||||
await torn.approve(governance.address, balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
await governance.lockWithApproval(balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id1 = await governance.latestProposalIds(proposer)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.sub(votingDelay).sub(toBN(1)))
|
||||
|
||||
await governance.propose(dummy.address, 'dummy2', { from: secondProposer })
|
||||
const id2 = await governance.latestProposalIds(secondProposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state1 = await governance.state(id1)
|
||||
state1.should.be.eq.BN(ProposalState.Active)
|
||||
const state2 = await governance.state(id2)
|
||||
state2.should.be.eq.BN(ProposalState.Active)
|
||||
|
||||
const lockBefore = await governance.canWithdrawAfter(voterAlice)
|
||||
lockBefore.should.be.eq.BN(toBN(0))
|
||||
|
||||
await governance.castVote(id2, true, { from: voterAlice })
|
||||
|
||||
const lockAfter1 = await governance.canWithdrawAfter(voterAlice)
|
||||
|
||||
await governance.castVote(id1, true, { from: voterAlice })
|
||||
const lockAfter2 = await governance.canWithdrawAfter(voterAlice)
|
||||
lockAfter1.should.be.eq.BN(lockAfter2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#execute', () => {
|
||||
let proposal
|
||||
before(async () => {
|
||||
proposal = await Proposal.new()
|
||||
})
|
||||
it('should work', async () => {
|
||||
await governance.propose(proposal.address, 'proposal', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(executionDelay).add(toBN(duration.days(1)))))
|
||||
|
||||
const receipt = await governance.execute(id)
|
||||
const debugLog = receipt.receipt.rawLogs[0]
|
||||
const decodedLog = web3.eth.abi.decodeLog(
|
||||
[
|
||||
{
|
||||
type: 'address',
|
||||
name: 'output',
|
||||
},
|
||||
],
|
||||
debugLog.data,
|
||||
debugLog.topics[0],
|
||||
)
|
||||
const newDummy = await Dummy.at(decodedLog.output)
|
||||
const dummyText = await newDummy.text()
|
||||
dummyText.should.be.equal('dummy')
|
||||
receipt.logs[0].event.should.be.equal('ProposalExecuted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#lock', () => {
|
||||
let owner = miningPublicKey
|
||||
let tokensAmount = toBN(10).pow(toBN(21)).mul(toBN(1337))
|
||||
it('permitClass works', async () => {
|
||||
const args = {
|
||||
owner,
|
||||
spender: governance.address,
|
||||
value: tokensAmount,
|
||||
nonce: '0x00',
|
||||
deadline: new BN('123123123123123'),
|
||||
}
|
||||
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
permitSigner.getPayload()
|
||||
|
||||
// Generate the signature in place
|
||||
const privateKey = '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c'
|
||||
const address = '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b'
|
||||
const signature = await permitSigner.getSignature(privateKey)
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
address.should.be.equal(signer)
|
||||
})
|
||||
|
||||
it('calls approve if signature is valid', async () => {
|
||||
const chainIdFromContract = await torn.chainId()
|
||||
chainIdFromContract.should.be.eq.BN(new BN(domain.chainId))
|
||||
const args = {
|
||||
owner,
|
||||
spender: governance.address,
|
||||
value: tokensAmount,
|
||||
nonce: 0,
|
||||
deadline: new BN('5609459200'),
|
||||
}
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
const signature = await permitSigner.getSignature(miningPrivateKey)
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
signer.should.be.equal(miningPublicKey)
|
||||
|
||||
const balanceBefore = await torn.balanceOf(governance.address)
|
||||
const lockedBalanceBefore = await governance.lockedBalance(owner)
|
||||
await governance.lock(
|
||||
args.owner,
|
||||
// args.spender,
|
||||
args.value.toString(),
|
||||
args.deadline.toString(),
|
||||
signature.v,
|
||||
signature.r,
|
||||
signature.s,
|
||||
{ from: owner },
|
||||
)
|
||||
const balanceAfter = await torn.balanceOf(governance.address)
|
||||
const lockedBalanceAfter = await governance.lockedBalance(owner)
|
||||
|
||||
balanceAfter.should.be.eq.BN(balanceBefore.add(args.value))
|
||||
lockedBalanceAfter.should.be.eq.BN(lockedBalanceBefore.add(args.value))
|
||||
})
|
||||
it('adds up tokens if already existing', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000)) // todo
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn.div(toBN(2)), { from: voterBob })
|
||||
await governance.lockWithApproval(tenThousandTorn.div(toBN(2)), { from: voterBob })
|
||||
|
||||
const balanceAfter = await torn.balanceOf(voterBob)
|
||||
const lockedBalanceAfter = await governance.lockedBalance(voterBob)
|
||||
balanceAfter.should.be.eq.BN(toBN(0))
|
||||
lockedBalanceAfter.should.be.eq.BN(tenThousandTorn)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#unlock', () => {
|
||||
it('should work if there is no activity made', async () => {
|
||||
const balanceBeforeTorn = await torn.balanceOf(proposer)
|
||||
const balanceBefore = await governance.lockedBalance(proposer)
|
||||
|
||||
await governance.unlock(balanceProposer, { from: proposer })
|
||||
const balanceAfterTorn = await torn.balanceOf(proposer)
|
||||
const balanceAfter = await governance.lockedBalance(proposer)
|
||||
balanceBefore.should.be.eq.BN(balanceAfter.add(balanceProposer))
|
||||
balanceAfterTorn.should.be.eq.BN(balanceBeforeTorn.add(balanceProposer))
|
||||
})
|
||||
it('fails if asking more than balance', async () => {
|
||||
await governance
|
||||
.unlock(balanceProposer + 1, { from: proposer })
|
||||
.should.be.rejectedWith('Governance: insufficient balance')
|
||||
//todo check lockedBalance
|
||||
})
|
||||
it('fail if there is active proposal', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance
|
||||
.unlock(balanceProposer, { from: proposer })
|
||||
.should.be.rejectedWith('Governance: tokens are locked')
|
||||
})
|
||||
it('unlock if there proposals expired', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(lockingPeriod + duration.minutes(1))))
|
||||
await governance.unlock(balanceProposer, { from: proposer })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#undelegate', () => {
|
||||
it('should work', async () => {
|
||||
let delegatee = accounts[5]
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
const { logs } = await governance.undelegate({ from: proposer })
|
||||
logs[0].args.account.should.be.equal(proposer)
|
||||
logs[0].args.from.should.be.equal(delegatee)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#delegate', () => {
|
||||
it('should work', async () => {
|
||||
let delegatee = accounts[5]
|
||||
let vp = await governance.delegatedTo(proposer)
|
||||
vp.should.be.equal('0x0000000000000000000000000000000000000000')
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
vp = await governance.delegatedTo(proposer)
|
||||
vp.should.be.equal(delegatee)
|
||||
})
|
||||
it('emits undelegate event if delegate called with non empty delegateTo', async () => {
|
||||
let delegatee = accounts[5]
|
||||
let delegateeSecond = accounts[6]
|
||||
const receipt = await governance.delegate(delegatee, { from: proposer })
|
||||
receipt.logs.length.should.be.equal(1)
|
||||
await governance
|
||||
.delegate(delegatee, { from: proposer })
|
||||
.should.be.rejectedWith('Governance: invalid delegatee')
|
||||
const receiptTwo = await governance.delegate(delegateeSecond, { from: proposer })
|
||||
receiptTwo.logs.length.should.be.equal(2)
|
||||
receiptTwo.logs[0].event.should.be.equal('Undelegated')
|
||||
receiptTwo.logs[0].args.account.should.be.equal(proposer)
|
||||
receiptTwo.logs[0].args.from.should.be.equal(delegatee)
|
||||
|
||||
receiptTwo.logs[1].event.should.be.equal('Delegated')
|
||||
receiptTwo.logs[1].args.account.should.be.equal(proposer)
|
||||
receiptTwo.logs[1].args.to.should.be.equal(delegateeSecond)
|
||||
const vp = await governance.delegatedTo(proposer)
|
||||
vp.should.be.equal(delegateeSecond)
|
||||
})
|
||||
it('can propose with delegated votes', async () => {
|
||||
let delegatee = accounts[5]
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
|
||||
await governance.proposeByDelegate(proposer, dummy.address, 'dummy', { from: delegatee })
|
||||
const proposalCount = await governance.proposalCount()
|
||||
proposalCount.should.be.eq.BN(1)
|
||||
const latestProposalId = await governance.latestProposalIds(proposer)
|
||||
latestProposalId.should.be.eq.BN(1)
|
||||
const proposal = await governance.proposals(1)
|
||||
proposal.proposer.should.be.equal(proposer)
|
||||
})
|
||||
it('can vote with delegated votes', async () => {
|
||||
let delegatee = accounts[5]
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
await governance.castDelegatedVote([proposer], id, true, { from: delegatee })
|
||||
|
||||
await governance.getReceipt(id, proposer)
|
||||
let proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(votesCount)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.getReceipt(id, proposer)
|
||||
proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('#getAllProposals', () => {
|
||||
it('fetches proposals', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const proposals = await governance.getAllProposals(0, 0)
|
||||
const proposal = proposals[0]
|
||||
proposal.id.should.be.eq.BN(1)
|
||||
proposal.proposer.should.be.equal(proposer)
|
||||
proposal.startTime.should.be.eq.BN(proposalStartTime)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
proposal.executed.should.be.equal(false)
|
||||
proposal.state.should.be.eq.BN(ProposalState.Active)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('#getBalances', () => {
|
||||
it('fetches lockedBalance', async () => {
|
||||
const lockedBalanceOne = await governance.getBalances([proposer, secondProposer])
|
||||
lockedBalanceOne.should.be.eq.BN([balanceProposer, toBN('0')])
|
||||
await torn.approve(governance.address, balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
await governance.lockWithApproval(balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
|
||||
const lockedBalance = await governance.getBalances([proposer, secondProposer])
|
||||
lockedBalance.should.be.eq.BN([balanceProposer, balanceProposer.div(toBN(2))])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#upgrades', () => {
|
||||
it('allows to change variable state', async () => {
|
||||
const proposal = await ProposalStateChangeGovernance.new()
|
||||
await governance.propose(proposal.address, 'proposal', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(executionDelay).add(toBN(duration.days(1)))))
|
||||
|
||||
const EXECUTION_DELAY_BEFORE = await governance.EXECUTION_DELAY()
|
||||
EXECUTION_DELAY_BEFORE.should.be.eq.BN(duration.days(2))
|
||||
const receipt = await governance.execute(id)
|
||||
const EXECUTION_DELAY_AFTER = await governance.EXECUTION_DELAY()
|
||||
EXECUTION_DELAY_AFTER.should.be.eq.BN(duration.days(3))
|
||||
receipt.logs[0].event.should.be.equal('ProposalExecuted')
|
||||
})
|
||||
it('upgrades implementation with variables change', async () => {
|
||||
await NewImplementation.new({ from: accounts[9] })
|
||||
const proposal = await ProposalUpgrade.new()
|
||||
// console.log(newImpl.address) // 0xF7E3e47e06F1bDDecb1b2F3a7F60b6b25fd2e233
|
||||
|
||||
await governance.propose(proposal.address, 'proposal', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(executionDelay).add(toBN(duration.days(1)))))
|
||||
|
||||
const newGovernance = await NewImplementation.at(governance.address)
|
||||
const receipt = await governance.execute(id)
|
||||
let newVariable = await newGovernance.newVariable()
|
||||
newVariable.should.be.eq.BN(0)
|
||||
const receiptExecute = await newGovernance.execute(123)
|
||||
newVariable = await newGovernance.newVariable()
|
||||
newVariable.should.be.eq.BN(999)
|
||||
receipt.logs[0].event.should.be.equal('ProposalExecuted')
|
||||
receiptExecute.logs[0].event.should.be.equal('Overriden')
|
||||
})
|
||||
it('cannot initialize implementation contract', async () => {
|
||||
const impl = await NewImplementation.new({ from: accounts[9] })
|
||||
await impl
|
||||
.initialize(accounts[9])
|
||||
.should.be.rejectedWith('Contract instance has already been initialized')
|
||||
})
|
||||
it('cannot destroy implementation contract')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
93
test/tests.data.json
Normal file
93
test/tests.data.json
Normal file
@ -0,0 +1,93 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
942
test/v1/governance.v1.test.js
Normal file
942
test/v1/governance.v1.test.js
Normal file
@ -0,0 +1,942 @@
|
||||
const { ethers } = require('hardhat')
|
||||
const { expect } = require('chai')
|
||||
const { BigNumber } = require('@ethersproject/bignumber')
|
||||
const { PermitSigner } = require('../../scripts/v1/Permit.js')
|
||||
const tornConfig = require('torn-token')
|
||||
|
||||
const ProposalState = {
|
||||
Pending: 0,
|
||||
Active: 1,
|
||||
Defeated: 2,
|
||||
Timelocked: 3,
|
||||
AwaitingExecution: 4,
|
||||
Executed: 5,
|
||||
Expired: 6,
|
||||
}
|
||||
|
||||
const duration = {
|
||||
seconds: function (val) {
|
||||
return val
|
||||
},
|
||||
minutes: function (val) {
|
||||
return val * this.seconds(60)
|
||||
},
|
||||
hours: function (val) {
|
||||
return val * this.minutes(60)
|
||||
},
|
||||
days: function (val) {
|
||||
return val * this.hours(24)
|
||||
},
|
||||
weeks: function (val) {
|
||||
return val * this.days(7)
|
||||
},
|
||||
years: function (val) {
|
||||
return val * this.days(365)
|
||||
},
|
||||
}
|
||||
|
||||
describe('Governance tests', () => {
|
||||
/// NETWORK && DOMAIN
|
||||
let chainId
|
||||
let domain
|
||||
|
||||
//// SIGNERS
|
||||
let signerArray
|
||||
let proposer // = accounts[3] #TODO: set this
|
||||
let secondProposer // = accounts[8] #TODO: set this
|
||||
let proxy
|
||||
|
||||
/// CONTRACTS
|
||||
let governance, dummy
|
||||
let snapshotId
|
||||
let timestamp = 1577836800 // 01/01/2020 00:00
|
||||
let torn
|
||||
|
||||
/// GOVERNANCE VARS
|
||||
let votingDelay
|
||||
let votingPeriod
|
||||
let executionExpiration
|
||||
let executionDelay
|
||||
let extendTime
|
||||
let proposalStartTime
|
||||
let proposalEndTime
|
||||
let lockingPeriod
|
||||
|
||||
/// ON-CHAIN
|
||||
let balanceProposer
|
||||
const cap = BigNumber.from(tornConfig.torn.cap)
|
||||
const tenThousandTorn = BigNumber.from(10).pow(BigNumber.from(18)).mul(BigNumber.from(10000))
|
||||
const miningPrivateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'
|
||||
let miningPublicKey = '0x' + ethers.utils.computeAddress(Buffer.from(miningPrivateKey.slice(2), 'hex'))
|
||||
|
||||
before(async function () {
|
||||
signerArray = await ethers.getSigners()
|
||||
proposer = signerArray[3]
|
||||
secondProposer = signerArray[8]
|
||||
|
||||
chainId = (await signerArray[0].provider.getNetwork()).chainId
|
||||
|
||||
governance = await ethers.getContractFactory('MockGovernance')
|
||||
governance = await governance.deploy()
|
||||
|
||||
torn = await ethers.getContractFactory('TORNMock2')
|
||||
|
||||
miningPublicKey = miningPublicKey.slice(2)
|
||||
|
||||
proxy = await ethers.getContractFactory('MockProxy')
|
||||
|
||||
proxy = await proxy.deploy(governance.address, [])
|
||||
|
||||
governance = await ethers.getContractAt('MockGovernance', proxy.address)
|
||||
|
||||
torn = await torn.deploy(proxy.address, duration.days(30), [
|
||||
{ to: miningPublicKey, amount: cap.toString() },
|
||||
])
|
||||
|
||||
await governance.initialize(torn.address + '000000000000000000000000')
|
||||
|
||||
expect(await governance.torn()).to.equal(torn.address)
|
||||
|
||||
dummy = await ethers.getContractFactory('Dummy')
|
||||
dummy = await dummy.deploy()
|
||||
|
||||
balanceProposer = cap.div(BigNumber.from(4))
|
||||
|
||||
await ethers.provider.send('hardhat_impersonateAccount', [miningPublicKey])
|
||||
miningPublicKey = await ethers.getSigner(miningPublicKey)
|
||||
|
||||
await signerArray[0].sendTransaction({ value: ethers.utils.parseEther('3'), to: miningPublicKey.address })
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
|
||||
await torn.transfer(secondProposer.address, balanceProposer.div(BigNumber.from(2)))
|
||||
|
||||
await torn.transfer(proposer.address, balanceProposer)
|
||||
|
||||
await torn.setChainId(chainId)
|
||||
await governance.setTimestamp(timestamp)
|
||||
|
||||
votingDelay = await governance.VOTING_DELAY()
|
||||
votingPeriod = await governance.VOTING_PERIOD()
|
||||
executionExpiration = await governance.EXECUTION_EXPIRATION()
|
||||
executionDelay = await governance.EXECUTION_DELAY()
|
||||
extendTime = await governance.VOTE_EXTEND_TIME()
|
||||
|
||||
proposalStartTime = BigNumber.from(timestamp).add(votingDelay)
|
||||
proposalEndTime = votingPeriod.add(BigNumber.from(proposalStartTime))
|
||||
|
||||
lockingPeriod = Number(extendTime) + Number(executionExpiration) + Number(executionDelay)
|
||||
|
||||
domain = {
|
||||
name: await torn.name(),
|
||||
version: '1',
|
||||
chainId,
|
||||
verifyingContract: torn.address,
|
||||
}
|
||||
|
||||
snapshotId = await ethers.provider.send('evm_snapshot', [])
|
||||
})
|
||||
|
||||
beforeEach(async function () {
|
||||
torn = await torn.connect(proposer)
|
||||
await torn.approve(governance.address, balanceProposer)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.lockWithApproval(balanceProposer)
|
||||
|
||||
const balance = await governance.lockedBalance(proposer.address)
|
||||
expect(balance).to.equal(balanceProposer)
|
||||
})
|
||||
|
||||
describe('#contructor', () => {
|
||||
it('should work', async () => {
|
||||
const proposalCount = await governance.proposalCount()
|
||||
expect(proposalCount).to.equal(BigNumber.from(0))
|
||||
|
||||
const p = await governance.proposals(0)
|
||||
|
||||
expect(p.proposer).to.equal(governance.address)
|
||||
expect(p.target).to.equal('0x000000000000000000000000000000000000dEaD')
|
||||
expect(p.endTime).to.equal(BigNumber.from(0))
|
||||
expect(p.forVotes).to.equal(BigNumber.from(0))
|
||||
expect(p.againstVotes).to.equal(BigNumber.from(0))
|
||||
expect(p.executed).to.equal(true)
|
||||
expect(p.extended).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#propose', () => {
|
||||
it('should work', async () => {
|
||||
const response = await governance.propose(dummy.address, 'dummy')
|
||||
const receipt = await response.wait()
|
||||
const logs = receipt.events
|
||||
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
const proposalCount = await governance.proposalCount()
|
||||
|
||||
expect(proposalCount).to.equal(1)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.proposer).to.equal(proposer.address)
|
||||
expect(proposal.startTime).to.equal(proposalStartTime)
|
||||
expect(proposal.endTime).to.equal(proposalEndTime)
|
||||
expect(proposal.forVotes).to.equal(0)
|
||||
expect(proposal.againstVotes).to.equal(0)
|
||||
expect(proposal.executed).to.equal(false)
|
||||
|
||||
// emit ProposalCreated(newProposal.id, msg.sender, target, startBlock, endBlock, description);
|
||||
expect(logs[0].event).to.equal('ProposalCreated')
|
||||
expect(logs[0].args.id).to.equal(id)
|
||||
expect(logs[0].args.proposer).to.equal(proposer.address)
|
||||
expect(logs[0].args.target).to.equal(dummy.address)
|
||||
expect(logs[0].args.description).to.equal('dummy')
|
||||
expect(logs[0].args.startTime).to.equal(proposalStartTime)
|
||||
expect(logs[0].args.endTime).to.equal(votingPeriod.add(BigNumber.from(proposalStartTime)))
|
||||
|
||||
let state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Pending)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
const accountLock = await governance.canWithdrawAfter(proposer.address)
|
||||
|
||||
expect(accountLock).to.equal(proposalEndTime.add(BigNumber.from(lockingPeriod)))
|
||||
})
|
||||
it('fails if target is not a contract', async () => {
|
||||
governance = await governance.connect(proposer)
|
||||
await expect(governance.propose(signerArray[9].address, 'dummy')).to.be.revertedWith('not a contract')
|
||||
})
|
||||
it('fails if proposer has already pending proposal', async () => {
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
await expect(governance.propose(dummy.address, 'dummy')).to.be.revertedWith(
|
||||
'Governance::propose: one live proposal per proposer, found an already active proposal',
|
||||
)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
await expect(governance.propose(dummy.address, 'dummy')).to.be.revertedWith(
|
||||
'Governance::propose: one live proposal per proposer, found an already active proposal',
|
||||
)
|
||||
})
|
||||
it('fails if proposer does not have voting power', async function () {
|
||||
const voterBob = signerArray[5]
|
||||
const oneThousandTorn = ethers.utils.parseEther('1000')
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
|
||||
await torn.transfer(voterBob.address, oneThousandTorn)
|
||||
|
||||
torn = await torn.connect(voterBob)
|
||||
|
||||
await torn.approve(governance.address, oneThousandTorn)
|
||||
|
||||
expect(await governance.torn()).to.equal(torn.address)
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
|
||||
await governance.lockWithApproval(oneThousandTorn.sub(1))
|
||||
|
||||
await expect(governance.propose(dummy.address, 'dummy')).to.be.revertedWith(
|
||||
'Governance::propose: proposer votes below proposal threshold',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#castVote', () => {
|
||||
it('should work if support is true', async () => {
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
const response = await governance.castVote(id, true)
|
||||
const receipt = await response.wait()
|
||||
const logs = await receipt.events
|
||||
|
||||
expect(logs[0].event).to.equal('Voted')
|
||||
expect(logs[0].args.voter).to.equal(proposer.address)
|
||||
expect(logs[0].args.proposalId).to.equal(id)
|
||||
expect(logs[0].args.support).to.equal(true)
|
||||
expect(logs[0].args.votes).to.equal(votesCount)
|
||||
|
||||
await governance.getReceipt(id, proposer.address)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
expect(proposal.forVotes).to.equal(votesCount)
|
||||
expect(proposal.againstVotes).to.equal(0)
|
||||
})
|
||||
it('should work if support is false', async () => {
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state = await governance.state(id)
|
||||
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
const response = await governance.castVote(id, false)
|
||||
const receipt = await response.wait()
|
||||
const logs = await receipt.events
|
||||
|
||||
expect(logs[0].event).to.equal('Voted')
|
||||
expect(logs[0].args.voter).to.equal(proposer.address)
|
||||
expect(logs[0].args.proposalId).to.equal(id)
|
||||
expect(logs[0].args.support).to.equal(false)
|
||||
expect(logs[0].args.votes).to.equal(votesCount)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.forVotes).to.equal(0)
|
||||
expect(proposal.againstVotes).to.equal(votesCount)
|
||||
})
|
||||
|
||||
it('should be able to change the choice later if already voted before', async () => {
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
await governance.castVote(id, false)
|
||||
await governance.castVote(id, true)
|
||||
|
||||
const response = await governance.castVote(id, false)
|
||||
const receipt = await response.wait()
|
||||
const logs = await receipt.events
|
||||
|
||||
expect(logs[0].event).to.equal('Voted')
|
||||
expect(logs[0].args.voter).to.equal(proposer.address)
|
||||
expect(logs[0].args.proposalId).to.equal(id)
|
||||
expect(logs[0].args.support).to.equal(false)
|
||||
expect(logs[0].args.votes).to.equal(votesCount)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.forVotes).to.equal(0)
|
||||
expect(proposal.againstVotes).to.equal(votesCount)
|
||||
})
|
||||
|
||||
it('should work if there are multiple voters', async () => {
|
||||
const voterBob = signerArray[5]
|
||||
const voterAlice = signerArray[7]
|
||||
const tenThousandTorn = ethers.utils.parseEther('10000')
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
|
||||
await torn.transfer(voterBob.address, tenThousandTorn)
|
||||
|
||||
await torn.transfer(voterAlice.address, tenThousandTorn.mul(BigNumber.from(2)))
|
||||
|
||||
torn = await torn.connect(voterBob)
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn)
|
||||
|
||||
torn = await torn.connect(voterAlice)
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn.mul(BigNumber.from(2)))
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
await governance.lockWithApproval(tenThousandTorn)
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.lockWithApproval(tenThousandTorn.mul(BigNumber.from(2)))
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await expect(governance.propose(dummy.address, 'dummy')).to.not.be.reverted
|
||||
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state = await governance.state(id)
|
||||
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
await governance.castVote(id, false)
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
await governance.castVote(id, false)
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.castVote(id, true)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
expect(proposal.forVotes).to.equal(tenThousandTorn.mul(BigNumber.from(2)))
|
||||
expect(proposal.againstVotes).to.equal(votesCount.add(tenThousandTorn))
|
||||
})
|
||||
|
||||
it('fails if voter does not have voting power', async () => {
|
||||
const voterBob = signerArray[5]
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
|
||||
await expect(governance.castVote(id, false)).to.be.revertedWith('Governance: balance is 0')
|
||||
})
|
||||
|
||||
it('should be able to update number of votes count if the same decision is chosen after more tokens are locked', async () => {
|
||||
const voterBob = signerArray[5]
|
||||
|
||||
const tenThousandTorn = ethers.utils.parseEther('10000')
|
||||
const fiveThousandTorn = tenThousandTorn.div(BigNumber.from(2))
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
await torn.transfer(voterBob.address, tenThousandTorn)
|
||||
|
||||
torn = await torn.connect(voterBob)
|
||||
await torn.approve(governance.address, tenThousandTorn)
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
await governance.lockWithApproval(fiveThousandTorn)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.castVote(id, false)
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
await governance.castVote(id, false)
|
||||
|
||||
let proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.forVotes).to.equal(BigNumber.from(0))
|
||||
expect(proposal.againstVotes).to.equal(votesCount.add(fiveThousandTorn))
|
||||
|
||||
await governance.lockWithApproval(fiveThousandTorn)
|
||||
await governance.castVote(id, false)
|
||||
|
||||
proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.forVotes).to.equal(BigNumber.from(0))
|
||||
expect(proposal.againstVotes).to.equal(votesCount.add(tenThousandTorn))
|
||||
})
|
||||
|
||||
it('extends time if the vote changes the outcome during the CLOSING_PERIOD', async () => {
|
||||
const voterBob = signerArray[5]
|
||||
const voterAlice = signerArray[7]
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
|
||||
await torn.transfer(voterBob.address, tenThousandTorn)
|
||||
|
||||
await torn.transfer(voterAlice.address, tenThousandTorn.mul(BigNumber.from(2)))
|
||||
|
||||
torn = await torn.connect(voterBob)
|
||||
await torn.approve(governance.address, tenThousandTorn)
|
||||
|
||||
torn = await torn.connect(voterAlice)
|
||||
await torn.approve(governance.address, tenThousandTorn.mul(BigNumber.from(2)))
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
await governance.lockWithApproval(tenThousandTorn)
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.lockWithApproval(tenThousandTorn.mul(BigNumber.from(2)))
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
|
||||
await governance.setTimestamp(proposalStartTime.add(BigNumber.from(1)))
|
||||
|
||||
const state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
await governance.castVote(id, false)
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.castVote(id, true)
|
||||
|
||||
let MAX_EXTENDED_TIME = await governance.VOTE_EXTEND_TIME()
|
||||
let proposal = await governance.proposals(id)
|
||||
expect(proposal.endTime).to.equal(proposalEndTime)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.castVote(id, false)
|
||||
|
||||
proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.endTime).to.equal(proposalEndTime.add(MAX_EXTENDED_TIME))
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.add(BigNumber.from(duration.hours(5))))
|
||||
|
||||
const stateAfter = await governance.state(id)
|
||||
|
||||
expect(stateAfter).to.equal(ProposalState.Active)
|
||||
})
|
||||
|
||||
it('locks tokens after vote', async () => {
|
||||
const voterAlice = signerArray[7]
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
await torn.transfer(voterAlice.address, tenThousandTorn)
|
||||
|
||||
torn = await torn.connect(voterAlice)
|
||||
await torn.approve(governance.address, tenThousandTorn)
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.lockWithApproval(tenThousandTorn)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
await governance.setTimestamp(proposalStartTime.add(BigNumber.from(1)))
|
||||
|
||||
const state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
const lockBefore = await governance.canWithdrawAfter(voterAlice.address)
|
||||
expect(lockBefore).to.equal(BigNumber.from(0))
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.castVote(id, true)
|
||||
|
||||
const lockAfter = await governance.canWithdrawAfter(voterAlice.address)
|
||||
expect(lockAfter).to.equal(proposalEndTime.add(BigNumber.from(lockingPeriod)))
|
||||
})
|
||||
|
||||
it('does not reduce lock time', async () => {
|
||||
const voterAlice = signerArray[7]
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
await torn.transfer(voterAlice.address, tenThousandTorn)
|
||||
|
||||
torn = await torn.connect(voterAlice)
|
||||
await torn.approve(governance.address, tenThousandTorn)
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.lockWithApproval(tenThousandTorn)
|
||||
|
||||
torn = await torn.connect(secondProposer)
|
||||
await torn.approve(governance.address, balanceProposer.div(BigNumber.from(2)))
|
||||
|
||||
governance = await governance.connect(secondProposer)
|
||||
await governance.lockWithApproval(balanceProposer.div(BigNumber.from(2)))
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const id1 = await governance.latestProposalIds(proposer.address)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.sub(votingDelay).sub(BigNumber.from(1)))
|
||||
|
||||
governance = await governance.connect(secondProposer)
|
||||
await governance.propose(dummy.address, 'dummy2')
|
||||
const id2 = await governance.latestProposalIds(secondProposer.address)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state1 = await governance.state(id1)
|
||||
expect(state1).to.equal(ProposalState.Active)
|
||||
|
||||
const state2 = await governance.state(id2)
|
||||
expect(state2).to.equal(ProposalState.Active)
|
||||
|
||||
const lockBefore = await governance.canWithdrawAfter(voterAlice.address)
|
||||
expect(lockBefore).to.equal(BigNumber.from(0))
|
||||
|
||||
governance = await governance.connect(voterAlice)
|
||||
await governance.castVote(id2, true)
|
||||
|
||||
const lockAfter1 = await governance.canWithdrawAfter(voterAlice.address)
|
||||
|
||||
await governance.castVote(id1, true)
|
||||
const lockAfter2 = await governance.canWithdrawAfter(voterAlice.address)
|
||||
|
||||
expect(lockAfter1).to.equal(lockAfter2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#lock', () => {
|
||||
let owner = miningPublicKey
|
||||
let tokensAmount = BigNumber.from(10).pow(BigNumber.from(21)).mul(BigNumber.from(1337))
|
||||
|
||||
it('permitClass works', async () => {
|
||||
owner = owner.slice(2)
|
||||
owner = await ethers.getSigner(owner)
|
||||
|
||||
const args = {
|
||||
owner,
|
||||
spender: governance.address,
|
||||
value: tokensAmount,
|
||||
nonce: '0x00',
|
||||
deadline: BigNumber.from('123123123123123'),
|
||||
}
|
||||
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
|
||||
permitSigner.getPayload()
|
||||
|
||||
// Generate the signature in place
|
||||
const privateKey = '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c'
|
||||
|
||||
const address = '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b'
|
||||
|
||||
const signature = await permitSigner.getSignature(privateKey)
|
||||
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
|
||||
expect(address).to.equal(signer)
|
||||
})
|
||||
|
||||
it('calls approve if signature is valid', async () => {
|
||||
const chainIdFromContract = await torn.chainId()
|
||||
expect(chainIdFromContract).to.equal(new BigNumber.from(domain.chainId))
|
||||
const args = {
|
||||
owner,
|
||||
spender: governance.address,
|
||||
value: tokensAmount,
|
||||
nonce: 0,
|
||||
deadline: BigNumber.from('5609459200'),
|
||||
}
|
||||
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
const signature = await permitSigner.getSignature(miningPrivateKey)
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
|
||||
expect(signer).to.equal(miningPublicKey.address)
|
||||
|
||||
const balanceBefore = await torn.balanceOf(governance.address)
|
||||
|
||||
const lockedBalanceBefore = await governance.lockedBalance(owner.address)
|
||||
|
||||
governance = await governance.connect(owner)
|
||||
|
||||
await governance.lock(
|
||||
args.owner,
|
||||
// args.spender,
|
||||
args.value.toString(),
|
||||
args.deadline.toString(),
|
||||
signature.v,
|
||||
signature.r,
|
||||
signature.s,
|
||||
)
|
||||
|
||||
const balanceAfter = await torn.balanceOf(governance.address)
|
||||
const lockedBalanceAfter = await governance.lockedBalance(owner.address)
|
||||
|
||||
expect(balanceAfter).to.equal(balanceBefore.add(args.value))
|
||||
expect(lockedBalanceAfter).to.equal(lockedBalanceBefore.add(args.value))
|
||||
})
|
||||
|
||||
it('adds up tokens if already existing', async () => {
|
||||
const voterBob = signerArray[5]
|
||||
const tenThousandTorn = ethers.utils.parseEther('10000')
|
||||
|
||||
torn = await torn.connect(miningPublicKey)
|
||||
await torn.transfer(voterBob.address, tenThousandTorn)
|
||||
|
||||
torn = await torn.connect(voterBob)
|
||||
await torn.approve(governance.address, tenThousandTorn)
|
||||
|
||||
governance = await governance.connect(voterBob)
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn.div(BigNumber.from(2)))
|
||||
await governance.lockWithApproval(tenThousandTorn.div(BigNumber.from(2)))
|
||||
|
||||
const balanceAfter = await torn.balanceOf(voterBob.address)
|
||||
const lockedBalanceAfter = await governance.lockedBalance(voterBob.address)
|
||||
|
||||
expect(balanceAfter).to.equal(BigNumber.from(0))
|
||||
expect(lockedBalanceAfter).to.equal(tenThousandTorn)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#unlock', () => {
|
||||
it('should work if there is no activity made', async () => {
|
||||
const balanceBeforeTorn = await torn.balanceOf(proposer.address)
|
||||
const balanceBefore = await governance.lockedBalance(proposer.address)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.unlock(balanceProposer)
|
||||
|
||||
const balanceAfterTorn = await torn.balanceOf(proposer.address)
|
||||
const balanceAfter = await governance.lockedBalance(proposer.address)
|
||||
|
||||
expect(balanceBefore).to.equal(balanceAfter.add(balanceProposer))
|
||||
expect(balanceAfterTorn).to.equal(balanceBeforeTorn.add(balanceProposer))
|
||||
})
|
||||
it('fails if asking more than balance', async () => {
|
||||
governance = await governance.connect(proposer)
|
||||
await expect(governance.unlock(balanceProposer + 1)).to.be.revertedWith(
|
||||
'Governance: insufficient balance',
|
||||
)
|
||||
})
|
||||
it('fail if there is active proposal', async () => {
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
await expect(governance.unlock(balanceProposer)).to.be.revertedWith('Governance: tokens are locked')
|
||||
})
|
||||
it('unlock if there proposals expired', async () => {
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
await governance.setTimestamp(proposalEndTime.add(BigNumber.from(lockingPeriod + duration.minutes(1))))
|
||||
await governance.unlock(balanceProposer)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#undelegate', () => {
|
||||
it('should work', async () => {
|
||||
let delegatee = signerArray[5]
|
||||
await governance.delegate(delegatee.address)
|
||||
const response = await governance.undelegate()
|
||||
const receipt = await response.wait()
|
||||
const logs = receipt.events
|
||||
expect(logs[0].args.account).to.equal(proposer.address)
|
||||
expect(logs[0].args[1]).to.equal(delegatee.address)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#delegate', () => {
|
||||
it('should work', async () => {
|
||||
let delegatee = signerArray[5]
|
||||
|
||||
let vp = await governance.delegatedTo(proposer.address)
|
||||
expect(String(vp)).to.equal('0x0000000000000000000000000000000000000000')
|
||||
|
||||
await governance.delegate(delegatee.address)
|
||||
vp = await governance.delegatedTo(proposer.address)
|
||||
expect(String(vp)).to.equal(delegatee.address)
|
||||
})
|
||||
|
||||
it('emits undelegate event if delegate called with non empty delegateTo', async () => {
|
||||
let delegatee = signerArray[5]
|
||||
let delegateeSecond = signerArray[6]
|
||||
|
||||
const response = await governance.delegate(delegatee.address)
|
||||
const receipt = await response.wait()
|
||||
|
||||
expect(receipt.logs.length).to.equal(1)
|
||||
|
||||
await expect(governance.delegate(delegatee.address)).to.be.revertedWith('Governance: invalid delegatee')
|
||||
|
||||
const responseTwo = await governance.delegate(delegateeSecond.address)
|
||||
let receiptTwo = await responseTwo.wait()
|
||||
receiptTwo.logs = receiptTwo.events
|
||||
|
||||
expect(receiptTwo.logs.length).to.equal(2)
|
||||
expect(receiptTwo.logs[0].event).to.equal('Undelegated')
|
||||
expect(receiptTwo.logs[0].args.account).to.equal(proposer.address)
|
||||
expect(receiptTwo.logs[0].args.from).to.equal(delegatee.address)
|
||||
|
||||
expect(receiptTwo.logs[1].event).to.equal('Delegated')
|
||||
expect(receiptTwo.logs[1].args.account).to.equal(proposer.address)
|
||||
expect(receiptTwo.logs[1].args[1]).to.equal(delegateeSecond.address)
|
||||
|
||||
const vp = await governance.delegatedTo(proposer.address)
|
||||
|
||||
expect(vp).to.equal(delegateeSecond.address)
|
||||
})
|
||||
it('can propose with delegated votes', async () => {
|
||||
let delegatee = signerArray[5]
|
||||
await governance.delegate(delegatee.address)
|
||||
|
||||
governance = await governance.connect(delegatee)
|
||||
await governance.proposeByDelegate(proposer.address, dummy.address, 'dummy')
|
||||
|
||||
const proposalCount = await governance.proposalCount()
|
||||
expect(proposalCount).to.equal(1)
|
||||
|
||||
const latestProposalId = await governance.latestProposalIds(proposer.address)
|
||||
expect(latestProposalId).to.equal(1)
|
||||
|
||||
const proposal = await governance.proposals(1)
|
||||
expect(proposal.proposer).to.equal(proposer.address)
|
||||
})
|
||||
|
||||
it('can vote with delegated votes', async () => {
|
||||
let delegatee = signerArray[5]
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.delegate(delegatee.address)
|
||||
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
|
||||
const votesCount = balanceProposer
|
||||
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
governance = await governance.connect(delegatee)
|
||||
await governance.castDelegatedVote([proposer.address], id, true)
|
||||
|
||||
await governance.getReceipt(id, proposer.address)
|
||||
|
||||
let proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.forVotes).to.equal(votesCount)
|
||||
expect(proposal.againstVotes).to.equal(0)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.castVote(id, false)
|
||||
await governance.getReceipt(id, proposer.address)
|
||||
|
||||
proposal = await governance.proposals(id)
|
||||
|
||||
expect(proposal.forVotes).to.equal(0)
|
||||
expect(proposal.againstVotes).to.equal(votesCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('#getAllProposals', () => {
|
||||
it('fetches proposals', async () => {
|
||||
await governance.propose(dummy.address, 'dummy')
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const proposals = await governance.getAllProposals(0, 0)
|
||||
const proposal = proposals[0]
|
||||
|
||||
expect(proposal.id).to.equal(1)
|
||||
expect(proposal.proposer).to.equal(proposer.address)
|
||||
expect(proposal.startTime).to.equal(proposalStartTime)
|
||||
expect(proposal.endTime).to.equal(proposalEndTime)
|
||||
expect(proposal.forVotes).to.equal(0)
|
||||
expect(proposal.againstVotes).to.equal(0)
|
||||
expect(proposal.executed).to.equal(false)
|
||||
expect(proposal.state).to.equal(ProposalState.Active)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('#getBalances', () => {
|
||||
it('fetches lockedBalance', async () => {
|
||||
const lockedBalanceOne = await governance.getBalances([proposer.address, secondProposer.address])
|
||||
|
||||
lockedBalanceOne.to.equal([balanceProposer, BigNumber.from('0')])
|
||||
|
||||
torn = await torn.connect(secondProposer)
|
||||
await torn.approve(governance.address, balanceProposer.div(BigNumber.from(2)))
|
||||
|
||||
governance = await governance.connect(secondProposer)
|
||||
await governance.lockWithApproval(balanceProposer.div(BigNumber.from(2)))
|
||||
|
||||
const lockedBalance = await governance.getBalances([proposer.address, secondProposer.address])
|
||||
|
||||
expect(lockedBalance).to.equal([balanceProposer, balanceProposer.div(BigNumber.from(2))])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#upgrades', () => {
|
||||
it('allows to change variable state', async () => {
|
||||
let proposal = await ethers.getContractFactory('ProposalStateChangeGovernance')
|
||||
proposal = await proposal.deploy()
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.propose(proposal.address, 'proposal')
|
||||
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
await governance.setTimestamp(proposalStartTime.add(BigNumber.from(1)))
|
||||
|
||||
let state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
await governance.castVote(id, true)
|
||||
|
||||
await governance.setTimestamp(
|
||||
proposalEndTime.add(BigNumber.from(executionDelay).add(BigNumber.from(duration.days(1)))),
|
||||
)
|
||||
|
||||
const EXECUTION_DELAY_BEFORE = await governance.EXECUTION_DELAY()
|
||||
expect(EXECUTION_DELAY_BEFORE).to.equal(duration.days(2))
|
||||
|
||||
const response = await governance.execute(id)
|
||||
let receipt = await response.wait()
|
||||
receipt.logs = receipt.events
|
||||
|
||||
const EXECUTION_DELAY_AFTER = await governance.EXECUTION_DELAY()
|
||||
|
||||
expect(EXECUTION_DELAY_AFTER).to.equal(duration.days(3))
|
||||
expect(receipt.logs[0].event).to.equal('ProposalExecuted')
|
||||
})
|
||||
it('upgrades implementation with variables change', async () => {
|
||||
let NewImplementation = await ethers.getContractFactory('NewImplementation')
|
||||
NewImplementation = await NewImplementation.deploy()
|
||||
|
||||
let proposal = await ethers.getContractFactory('ProposalUpgrade')
|
||||
proposal = await proposal.deploy(NewImplementation.address)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.propose(proposal.address, 'proposal')
|
||||
|
||||
const id = await governance.latestProposalIds(proposer.address)
|
||||
await governance.setTimestamp(proposalStartTime.add(BigNumber.from(1)))
|
||||
|
||||
let state = await governance.state(id)
|
||||
expect(state).to.equal(ProposalState.Active)
|
||||
|
||||
governance = await governance.connect(proposer)
|
||||
await governance.castVote(id, true)
|
||||
|
||||
await governance.setTimestamp(
|
||||
proposalEndTime.add(BigNumber.from(executionDelay).add(BigNumber.from(duration.days(1)))),
|
||||
)
|
||||
|
||||
const newGovernance = await ethers.getContractAt('NewImplementation', governance.address)
|
||||
const response = await governance.execute(id)
|
||||
let receipt = await response.wait()
|
||||
receipt.logs = receipt.events
|
||||
|
||||
let newVariable = await newGovernance.newVariable()
|
||||
expect(newVariable).to.equal(0)
|
||||
|
||||
const responseExecute = await newGovernance.execute(123)
|
||||
let receiptExecute = await responseExecute.wait()
|
||||
receiptExecute.logs = receiptExecute.events
|
||||
|
||||
newVariable = await newGovernance.newVariable()
|
||||
expect(newVariable).to.equal(999)
|
||||
|
||||
expect(receipt.logs[1].event).to.equal('ProposalExecuted')
|
||||
expect(receiptExecute.logs[0].event).to.equal('Overriden')
|
||||
})
|
||||
it('cannot initialize implementation contract', async () => {
|
||||
const impl = await (await ethers.getContractFactory('NewImplementation')).deploy()
|
||||
await expect(impl.initialize(signerArray[0].address + '000000000000000000000000')).to.be.revertedWith(
|
||||
'Contract instance has already been initialized',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await ethers.provider.send('evm_revert', [snapshotId])
|
||||
snapshotId = await ethers.provider.send('evm_snapshot', [])
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await ethers.provider.send('hardhat_reset', [
|
||||
{
|
||||
forking: {
|
||||
jsonRpcUrl: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`,
|
||||
blockNumber: process.env.use_latest_block == 'true' ? undefined : 13042331,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
797
test/v2/all.governance.v2.test.js
Normal file
797
test/v2/all.governance.v2.test.js
Normal file
@ -0,0 +1,797 @@
|
||||
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(40)
|
||||
const sellAmount = pE(3.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('100'), 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(0.53 + 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('100'),
|
||||
ethers.utils.parseUnits('1', 'szabo'),
|
||||
)
|
||||
|
||||
/// Now revert and test with below funding
|
||||
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(0.03 + 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)
|
||||
|
||||
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',
|
||||
)
|
||||
}
|
||||
|
||||
expect(await TornToken.balanceOf(TornadoAuctionHandler.address)).to.equal(
|
||||
ethers.utils.parseEther('100'),
|
||||
)
|
||||
})
|
||||
|
||||
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://mainnet.infura.io/v3/${process.env.INFURA_KEY}`,
|
||||
blockNumber: process.env.use_latest_block == 'true' ? undefined : 13211966,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
5599
yarn-error.log
Normal file
5599
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user