Init
This commit is contained in:
commit
eb0fe77948
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ETHERSCAN_KEY=
|
||||||
|
ALCHEMY_KEY=
|
||||||
|
use_latest_block=false
|
27
.eslintrc
Normal file
27
.eslintrc
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"],
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"indent": ["error", 2],
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"quotes": ["error", "single"],
|
||||||
|
"semi": ["error", "never"],
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
"require-await": "error",
|
||||||
|
"prettier/prettier": ["error", { "printWidth": 110 }]
|
||||||
|
}
|
||||||
|
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.sol linguist-language=Solidity
|
91
.github/workflows/build.yml
vendored
Normal file
91
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['*']
|
||||||
|
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }}
|
||||||
|
ALCHEMY_KEY: ${{ secrets.ALCHEMY_KEY }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn hardhat compile
|
||||||
|
- run: yarn lint
|
||||||
|
- run: yarn coverage
|
||||||
|
- name: Coveralls
|
||||||
|
uses: coverallsapp/github-action@master
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- run: yarn hardhat test
|
||||||
|
- name: Telegram Failure Notification
|
||||||
|
uses: appleboy/telegram-action@0.0.7
|
||||||
|
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 }}
|
111
.gitignore
vendored
Normal file
111
.gitignore
vendored
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
artifacts
|
||||||
|
cache
|
||||||
|
coverage
|
||||||
|
coverage.json
|
||||||
|
|
||||||
|
.vscode
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
14
|
10
.prettierignore
Normal file
10
.prettierignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
cache
|
||||||
|
artifacts
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
README.md
|
||||||
|
contracts/v2-vault-and-gas/libraries/EtherSend.sol
|
||||||
|
coverage
|
||||||
|
coverage.json
|
16
.prettierrc
Normal file
16
.prettierrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"semi": false,
|
||||||
|
"printWidth": 110,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.sol",
|
||||||
|
"options": {
|
||||||
|
"singleQuote": false,
|
||||||
|
"printWidth": 130
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
.solcover.js
Normal file
3
.solcover.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
skipFiles: [],
|
||||||
|
}
|
15
.solhint.json
Normal file
15
.solhint.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "solhint:recommended",
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"printWidth": 110
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quotes": ["error", "double"],
|
||||||
|
"indent": ["error", 2],
|
||||||
|
"compiler-version": ["error", "^0.6.0"]
|
||||||
|
},
|
||||||
|
"plugins": ["prettier"]
|
||||||
|
}
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2018 Truffle
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Tornado governance [![build status](https://github.com/tornadocash/tornado-governance/actions/workflows/build.yml/badge.svg)](https://github.com/tornadocash/tornado-governance/actions/workflows/build.yml) [![Coverage Status](https://coveralls.io/repos/github/tornadocash/tornado-governance/badge.svg?branch=master)](https://coveralls.io/github/tornadocash/tornado-governance?branch=master)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
This repository holds all the tornado.cash governance upgrades and original governance contracts.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
All high-level documentation can be find [here](https://docs.tornado.cash/general/governance).
|
||||||
|
|
||||||
|
## Code architecture
|
||||||
|
Tornado governance infrastructure consists of two types of repository:
|
||||||
|
1. **Governance repository** (this one) - contains the original governance contracts and parts of proposals that upgrade governance itself via loopback proxy. So here you can compile the actual version of the governance contract.
|
||||||
|
2. **Proposal repository** - a separate repository for each governance proposal. It contains the full codebase of a proposal.
|
||||||
|
|
||||||
|
### Loopback proxy
|
||||||
|
[Loopback proxy](https://github.com/tornadocash/tornado-governance/blob/master/contracts/v1/LoopbackProxy.sol) is a special type of proxy contract that is used to add the ability to upgrade the proxy itself. This way governance proposals can upgrade governance implementation.
|
||||||
|
|
||||||
|
### Proposal creation manual
|
||||||
|
To create your custom governance proposal you need to:
|
||||||
|
1. Create a proposal repository (for [example](https://github.com/Rezan-vm/tornado-relayer-registry)):
|
||||||
|
- a proposal is executed from the governance contract using delegatecall of __executeProposal()__ method
|
||||||
|
- as a proposal is executed using delegatecall, it should not store any storage variables - use constants and immutable variables instead
|
||||||
|
2. If your proposal is upgrading governance itself, you need to create a pull request to the governance repository. PR should add folder with governance contract upgrade (separate folder in contracts folder - for [example](https://github.com/tornadocash/tornado-governance/pull/6/commits/5f36d5744a9f279a58e9ba1f0e0cd9d493af41c7)).
|
||||||
|
3. Deploy proposal. The proposal must be smart contracts with verified code.
|
||||||
|
4. Go to Tornado governance [UI](https://tornadocash.eth.limo/governance) to start the proposal voting process.
|
||||||
|
|
||||||
|
|
||||||
|
## Tests/Coverage
|
||||||
|
|
||||||
|
Setting up the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/tornadocash/tornado-governance.git
|
||||||
|
yarn
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Please fill out .env according to the template provided in it. Please ensure that all of the example values are set to the correct addresses.
|
||||||
|
|
||||||
|
To run test scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
To run tests coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn coverage
|
||||||
|
```
|
6
config.js
Normal file
6
config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
governance: '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
|
||||||
|
TORN: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
|
||||||
|
tornWhale: '0xF977814e90dA44bFA03b6295A0616a897441aceC',
|
||||||
|
forkBlockNumber: 14352372,
|
||||||
|
}
|
73
contracts/v1/Configuration.sol
Normal file
73
contracts/v1/Configuration.sol
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
contract Configuration {
|
||||||
|
/// @notice Time delay between proposal vote completion and its execution
|
||||||
|
uint256 public EXECUTION_DELAY;
|
||||||
|
/// @notice Time before a passed proposal is considered expired
|
||||||
|
uint256 public EXECUTION_EXPIRATION;
|
||||||
|
/// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed
|
||||||
|
uint256 public QUORUM_VOTES;
|
||||||
|
/// @notice The number of votes required in order for a voter to become a proposer
|
||||||
|
uint256 public PROPOSAL_THRESHOLD;
|
||||||
|
/// @notice The delay before voting on a proposal may take place, once proposed
|
||||||
|
/// It is needed to prevent reorg attacks that replace the proposal
|
||||||
|
uint256 public VOTING_DELAY;
|
||||||
|
/// @notice The duration of voting on a proposal
|
||||||
|
uint256 public VOTING_PERIOD;
|
||||||
|
/// @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 CLOSING_PERIOD;
|
||||||
|
/// @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() {
|
||||||
|
require(msg.sender == address(this), "Governance: unauthorized");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initializeConfiguration() internal {
|
||||||
|
EXECUTION_DELAY = 2 days;
|
||||||
|
EXECUTION_EXPIRATION = 3 days;
|
||||||
|
QUORUM_VOTES = 25000e18; // 0.25% of TORN
|
||||||
|
PROPOSAL_THRESHOLD = 1000e18; // 0.01% of TORN
|
||||||
|
VOTING_DELAY = 75 seconds;
|
||||||
|
VOTING_PERIOD = 3 days;
|
||||||
|
CLOSING_PERIOD = 1 hours;
|
||||||
|
VOTE_EXTEND_TIME = 6 hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setExecutionDelay(uint256 executionDelay) external onlySelf {
|
||||||
|
EXECUTION_DELAY = executionDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setExecutionExpiration(uint256 executionExpiration) external onlySelf {
|
||||||
|
EXECUTION_EXPIRATION = executionExpiration;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQuorumVotes(uint256 quorumVotes) external onlySelf {
|
||||||
|
QUORUM_VOTES = quorumVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProposalThreshold(uint256 proposalThreshold) external onlySelf {
|
||||||
|
PROPOSAL_THRESHOLD = proposalThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVotingDelay(uint256 votingDelay) external onlySelf {
|
||||||
|
VOTING_DELAY = votingDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVotingPeriod(uint256 votingPeriod) external onlySelf {
|
||||||
|
VOTING_PERIOD = votingPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setClosingPeriod(uint256 closingPeriod) external onlySelf {
|
||||||
|
CLOSING_PERIOD = closingPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVoteExtendTime(uint256 voteExtendTime) external onlySelf {
|
||||||
|
// VOTE_EXTEND_TIME should be less EXECUTION_DELAY to prevent double voting
|
||||||
|
require(voteExtendTime < EXECUTION_DELAY, "Governance: incorrect voteExtendTime");
|
||||||
|
VOTE_EXTEND_TIME = voteExtendTime;
|
||||||
|
}
|
||||||
|
}
|
9
contracts/v1/Core.sol
Normal file
9
contracts/v1/Core.sol
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
abstract contract Core {
|
||||||
|
/// @notice Locked token balance for each account
|
||||||
|
mapping(address => uint256) public lockedBalance;
|
||||||
|
}
|
66
contracts/v1/Delegation.sol
Normal file
66
contracts/v1/Delegation.sol
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "./Core.sol";
|
||||||
|
|
||||||
|
abstract contract Delegation is Core {
|
||||||
|
/// @notice Delegatee records
|
||||||
|
mapping(address => address) public delegatedTo;
|
||||||
|
|
||||||
|
event Delegated(address indexed account, address indexed to);
|
||||||
|
event Undelegated(address indexed account, address indexed from);
|
||||||
|
|
||||||
|
function delegate(address to) external {
|
||||||
|
address previous = delegatedTo[msg.sender];
|
||||||
|
require(to != msg.sender && to != address(this) && to != address(0) && to != previous, "Governance: invalid delegatee");
|
||||||
|
if (previous != address(0)) {
|
||||||
|
emit Undelegated(msg.sender, previous);
|
||||||
|
}
|
||||||
|
delegatedTo[msg.sender] = to;
|
||||||
|
emit Delegated(msg.sender, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
function undelegate() external {
|
||||||
|
address previous = delegatedTo[msg.sender];
|
||||||
|
require(previous != address(0), "Governance: tokens are already undelegated");
|
||||||
|
|
||||||
|
delegatedTo[msg.sender] = address(0);
|
||||||
|
emit Undelegated(msg.sender, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposeByDelegate(
|
||||||
|
address from,
|
||||||
|
address target,
|
||||||
|
string memory description
|
||||||
|
) external returns (uint256) {
|
||||||
|
require(delegatedTo[from] == msg.sender, "Governance: not authorized");
|
||||||
|
return _propose(from, target, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _propose(
|
||||||
|
address proposer,
|
||||||
|
address target,
|
||||||
|
string memory description
|
||||||
|
) internal virtual returns (uint256);
|
||||||
|
|
||||||
|
function castDelegatedVote(
|
||||||
|
address[] memory from,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support
|
||||||
|
) external virtual {
|
||||||
|
for (uint256 i = 0; i < from.length; i++) {
|
||||||
|
require(delegatedTo[from[i]] == msg.sender, "Governance: not authorized");
|
||||||
|
_castVote(from[i], proposalId, support);
|
||||||
|
}
|
||||||
|
if (lockedBalance[msg.sender] > 0) {
|
||||||
|
_castVote(msg.sender, proposalId, support);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _castVote(
|
||||||
|
address voter,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support
|
||||||
|
) internal virtual;
|
||||||
|
}
|
290
contracts/v1/Governance.sol
Normal file
290
contracts/v1/Governance.sol
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/math/SafeMath.sol";
|
||||||
|
import "@openzeppelin/upgrades-core/contracts/Initializable.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/Address.sol";
|
||||||
|
import "torn-token/contracts/ENS.sol";
|
||||||
|
import "torn-token/contracts/TORN.sol";
|
||||||
|
import "./Delegation.sol";
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Proposal {
|
||||||
|
// Creator of the proposal
|
||||||
|
address proposer;
|
||||||
|
// target addresses for the call to be made
|
||||||
|
address target;
|
||||||
|
// The block at which voting begins
|
||||||
|
uint256 startTime;
|
||||||
|
// The block at which voting ends: votes must be cast prior to this block
|
||||||
|
uint256 endTime;
|
||||||
|
// Current number of votes in favor of this proposal
|
||||||
|
uint256 forVotes;
|
||||||
|
// Current number of votes in opposition to this proposal
|
||||||
|
uint256 againstVotes;
|
||||||
|
// Flag marking whether the proposal has been executed
|
||||||
|
bool executed;
|
||||||
|
// Flag marking whether the proposal voting time has been extended
|
||||||
|
// Voting time can be extended once, if the proposal outcome has changed during CLOSING_PERIOD
|
||||||
|
bool extended;
|
||||||
|
// Receipts of ballots for the entire set of voters
|
||||||
|
mapping(address => Receipt) receipts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Ballot receipt record for a voter
|
||||||
|
struct Receipt {
|
||||||
|
// Whether or not a vote has been cast
|
||||||
|
bool hasVoted;
|
||||||
|
// Whether or not the voter supports the proposal
|
||||||
|
bool support;
|
||||||
|
// The number of votes the voter had, which were cast
|
||||||
|
uint256 votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice The official record of all proposals ever proposed
|
||||||
|
Proposal[] public proposals;
|
||||||
|
/// @notice The latest proposal for each proposer
|
||||||
|
mapping(address => uint256) public latestProposalIds;
|
||||||
|
/// @notice Timestamp when a user can withdraw tokens
|
||||||
|
mapping(address => uint256) public canWithdrawAfter;
|
||||||
|
|
||||||
|
TORN public torn;
|
||||||
|
|
||||||
|
/// @notice An event emitted when a new proposal is created
|
||||||
|
event ProposalCreated(
|
||||||
|
uint256 indexed id,
|
||||||
|
address indexed proposer,
|
||||||
|
address target,
|
||||||
|
uint256 startTime,
|
||||||
|
uint256 endTime,
|
||||||
|
string description
|
||||||
|
);
|
||||||
|
|
||||||
|
/// @notice An event emitted when a vote has been cast on a proposal
|
||||||
|
event Voted(uint256 indexed proposalId, address indexed voter, bool indexed support, uint256 votes);
|
||||||
|
|
||||||
|
/// @notice An event emitted when a proposal has been executed
|
||||||
|
event ProposalExecuted(uint256 indexed proposalId);
|
||||||
|
|
||||||
|
/// @notice Makes this instance inoperable to prevent selfdestruct attack
|
||||||
|
/// Proxy will still be able to properly initialize its storage
|
||||||
|
constructor() public initializer {
|
||||||
|
torn = TORN(0x000000000000000000000000000000000000dEaD);
|
||||||
|
_initializeConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize(bytes32 _torn) public initializer {
|
||||||
|
torn = TORN(resolve(_torn));
|
||||||
|
// Create a dummy proposal so that indexes start from 1
|
||||||
|
proposals.push(
|
||||||
|
Proposal({
|
||||||
|
proposer: address(this),
|
||||||
|
target: 0x000000000000000000000000000000000000dEaD,
|
||||||
|
startTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
forVotes: 0,
|
||||||
|
againstVotes: 0,
|
||||||
|
executed: true,
|
||||||
|
extended: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
_initializeConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock(
|
||||||
|
address owner,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) public virtual {
|
||||||
|
torn.permit(owner, address(this), amount, deadline, v, r, s);
|
||||||
|
_transferTokens(owner, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockWithApproval(uint256 amount) public virtual {
|
||||||
|
_transferTokens(msg.sender, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlock(uint256 amount) public virtual {
|
||||||
|
require(getBlockTimestamp() > canWithdrawAfter[msg.sender], "Governance: tokens are locked");
|
||||||
|
lockedBalance[msg.sender] = lockedBalance[msg.sender].sub(amount, "Governance: insufficient balance");
|
||||||
|
require(torn.transfer(msg.sender, amount), "TORN: transfer failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function propose(address target, string memory description) external returns (uint256) {
|
||||||
|
return _propose(msg.sender, target, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Propose implementation
|
||||||
|
* @param proposer proposer address
|
||||||
|
* @param target smart contact address that will be executed as result of voting
|
||||||
|
* @param description description of the proposal
|
||||||
|
* @return the new proposal id
|
||||||
|
*/
|
||||||
|
function _propose(
|
||||||
|
address proposer,
|
||||||
|
address target,
|
||||||
|
string memory description
|
||||||
|
) internal override(Delegation) returns (uint256) {
|
||||||
|
uint256 votingPower = lockedBalance[proposer];
|
||||||
|
require(votingPower >= PROPOSAL_THRESHOLD, "Governance::propose: proposer votes below proposal threshold");
|
||||||
|
// target should be a contract
|
||||||
|
require(Address.isContract(target), "Governance::propose: not a contract");
|
||||||
|
|
||||||
|
uint256 latestProposalId = latestProposalIds[proposer];
|
||||||
|
if (latestProposalId != 0) {
|
||||||
|
ProposalState proposersLatestProposalState = state(latestProposalId);
|
||||||
|
require(
|
||||||
|
proposersLatestProposalState != ProposalState.Active && proposersLatestProposalState != ProposalState.Pending,
|
||||||
|
"Governance::propose: one live proposal per proposer, found an already active proposal"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 startTime = getBlockTimestamp().add(VOTING_DELAY);
|
||||||
|
uint256 endTime = startTime.add(VOTING_PERIOD);
|
||||||
|
|
||||||
|
Proposal memory newProposal = Proposal({
|
||||||
|
proposer: proposer,
|
||||||
|
target: target,
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
forVotes: 0,
|
||||||
|
againstVotes: 0,
|
||||||
|
executed: false,
|
||||||
|
extended: false
|
||||||
|
});
|
||||||
|
|
||||||
|
proposals.push(newProposal);
|
||||||
|
uint256 proposalId = proposalCount();
|
||||||
|
latestProposalIds[newProposal.proposer] = proposalId;
|
||||||
|
|
||||||
|
_lockTokens(proposer, endTime.add(VOTE_EXTEND_TIME).add(EXECUTION_EXPIRATION).add(EXECUTION_DELAY));
|
||||||
|
emit ProposalCreated(proposalId, proposer, target, startTime, endTime, description);
|
||||||
|
return proposalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
address target = proposal.target;
|
||||||
|
require(Address.isContract(target), "Governance::execute: not a contract");
|
||||||
|
(bool success, bytes memory data) = target.delegatecall(abi.encodeWithSignature("executeProposal()"));
|
||||||
|
if (!success) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
revert(string(data));
|
||||||
|
} else {
|
||||||
|
revert("Proposal execution failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit ProposalExecuted(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function castVote(uint256 proposalId, bool support) external virtual {
|
||||||
|
_castVote(msg.sender, proposalId, support);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _castVote(
|
||||||
|
address voter,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support
|
||||||
|
) internal override(Delegation) {
|
||||||
|
require(state(proposalId) == ProposalState.Active, "Governance::_castVote: voting is closed");
|
||||||
|
Proposal storage proposal = proposals[proposalId];
|
||||||
|
Receipt storage receipt = proposal.receipts[voter];
|
||||||
|
bool beforeVotingState = proposal.forVotes <= proposal.againstVotes;
|
||||||
|
uint256 votes = lockedBalance[voter];
|
||||||
|
require(votes > 0, "Governance: balance is 0");
|
||||||
|
if (receipt.hasVoted) {
|
||||||
|
if (receipt.support) {
|
||||||
|
proposal.forVotes = proposal.forVotes.sub(receipt.votes);
|
||||||
|
} else {
|
||||||
|
proposal.againstVotes = proposal.againstVotes.sub(receipt.votes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (support) {
|
||||||
|
proposal.forVotes = proposal.forVotes.add(votes);
|
||||||
|
} else {
|
||||||
|
proposal.againstVotes = proposal.againstVotes.add(votes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposal.extended && proposal.endTime.sub(getBlockTimestamp()) < CLOSING_PERIOD) {
|
||||||
|
bool afterVotingState = proposal.forVotes <= proposal.againstVotes;
|
||||||
|
if (beforeVotingState != afterVotingState) {
|
||||||
|
proposal.extended = true;
|
||||||
|
proposal.endTime = proposal.endTime.add(VOTE_EXTEND_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
receipt.hasVoted = true;
|
||||||
|
receipt.support = support;
|
||||||
|
receipt.votes = votes;
|
||||||
|
_lockTokens(voter, proposal.endTime.add(VOTE_EXTEND_TIME).add(EXECUTION_EXPIRATION).add(EXECUTION_DELAY));
|
||||||
|
emit Voted(proposalId, voter, support, votes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _lockTokens(address owner, uint256 timestamp) internal {
|
||||||
|
if (timestamp > canWithdrawAfter[owner]) {
|
||||||
|
canWithdrawAfter[owner] = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _transferTokens(address owner, uint256 amount) internal virtual {
|
||||||
|
require(torn.transferFrom(owner, address(this), amount), "TORN: transferFrom failed");
|
||||||
|
lockedBalance[owner] = lockedBalance[owner].add(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReceipt(uint256 proposalId, address voter) public view returns (Receipt memory) {
|
||||||
|
return proposals[proposalId].receipts[voter];
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(uint256 proposalId) public view returns (ProposalState) {
|
||||||
|
require(proposalId <= proposalCount() && proposalId > 0, "Governance::state: invalid proposal id");
|
||||||
|
Proposal storage proposal = proposals[proposalId];
|
||||||
|
if (getBlockTimestamp() <= proposal.startTime) {
|
||||||
|
return ProposalState.Pending;
|
||||||
|
} else if (getBlockTimestamp() <= proposal.endTime) {
|
||||||
|
return ProposalState.Active;
|
||||||
|
} else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes + proposal.againstVotes < QUORUM_VOTES) {
|
||||||
|
return ProposalState.Defeated;
|
||||||
|
} else if (proposal.executed) {
|
||||||
|
return ProposalState.Executed;
|
||||||
|
} else if (getBlockTimestamp() >= proposal.endTime.add(EXECUTION_DELAY).add(EXECUTION_EXPIRATION)) {
|
||||||
|
return ProposalState.Expired;
|
||||||
|
} else if (getBlockTimestamp() >= proposal.endTime.add(EXECUTION_DELAY)) {
|
||||||
|
return ProposalState.AwaitingExecution;
|
||||||
|
} else {
|
||||||
|
return ProposalState.Timelocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposalCount() public view returns (uint256) {
|
||||||
|
return proposals.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlockTimestamp() internal view virtual returns (uint256) {
|
||||||
|
// solium-disable-next-line security/no-block-members
|
||||||
|
return block.timestamp;
|
||||||
|
}
|
||||||
|
}
|
22
contracts/v1/LoopbackProxy.sol
Normal file
22
contracts/v1/LoopbackProxy.sol
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/proxy/TransparentUpgradeableProxy.sol";
|
||||||
|
import "torn-token/contracts/ENS.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev TransparentUpgradeableProxy that sets its admin to the implementation itself.
|
||||||
|
* It is also allowed to call implementation methods.
|
||||||
|
*/
|
||||||
|
contract LoopbackProxy is TransparentUpgradeableProxy, EnsResolve {
|
||||||
|
/**
|
||||||
|
* @dev Initializes an upgradeable proxy backed by the implementation at `_logic`.
|
||||||
|
*/
|
||||||
|
constructor(address _logic, bytes memory _data) public payable TransparentUpgradeableProxy(_logic, address(this), _data) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Override to allow admin (itself) access the fallback function.
|
||||||
|
*/
|
||||||
|
function _beforeFallback() internal override {}
|
||||||
|
}
|
32
contracts/v1/Mocks/Dummy.sol
Normal file
32
contracts/v1/Mocks/Dummy.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
contract Dummy {
|
||||||
|
uint256 public value;
|
||||||
|
string public text;
|
||||||
|
|
||||||
|
function initialize() public {
|
||||||
|
value = 1;
|
||||||
|
text = "dummy";
|
||||||
|
}
|
||||||
|
|
||||||
|
// function update(address _impl) public {
|
||||||
|
// MyProxy(address(uint160(address(this)))).upgradeTo(_impl);
|
||||||
|
// // MyProxy(address(this)).upgradeTo(_impl);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
contract DummySecond {
|
||||||
|
uint256 public value;
|
||||||
|
string public text;
|
||||||
|
|
||||||
|
function initialize() public {
|
||||||
|
value = 2;
|
||||||
|
text = "dummy2";
|
||||||
|
}
|
||||||
|
|
||||||
|
// function update(address _impl) public {
|
||||||
|
// MyProxy(address(uint160(address(this)))).upgradeTo(_impl);
|
||||||
|
// }
|
||||||
|
}
|
27
contracts/v1/Mocks/MockGovernance.sol
Normal file
27
contracts/v1/Mocks/MockGovernance.sol
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "../Governance.sol";
|
||||||
|
|
||||||
|
contract MockGovernance is Governance {
|
||||||
|
uint256 public time = block.timestamp;
|
||||||
|
|
||||||
|
function setTimestamp(uint256 time_) public {
|
||||||
|
time = time_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlockTimestamp() internal view override returns (uint256) {
|
||||||
|
// solium-disable-next-line security/no-block-members
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTorn(address torna) external {
|
||||||
|
torn = TORN(torna);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(bytes32 addr) public view override returns (address) {
|
||||||
|
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||||
|
}
|
||||||
|
}
|
13
contracts/v1/Mocks/MockProxy.sol
Normal file
13
contracts/v1/Mocks/MockProxy.sol
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "../LoopbackProxy.sol";
|
||||||
|
|
||||||
|
contract MockProxy is LoopbackProxy {
|
||||||
|
constructor(address _logic, bytes memory _data) public payable LoopbackProxy(_logic, _data) {}
|
||||||
|
|
||||||
|
function resolve(bytes32 addr) public view override returns (address) {
|
||||||
|
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||||
|
}
|
||||||
|
}
|
19
contracts/v1/Mocks/Proposal.sol
Normal file
19
contracts/v1/Mocks/Proposal.sol
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "./Dummy.sol";
|
||||||
|
|
||||||
|
contract Proposal {
|
||||||
|
// bytes32 public constant WEIRD = keccak256("Hey Proposal");
|
||||||
|
// uint256 public someValue = 111;
|
||||||
|
// Dummy public dummyInstance;
|
||||||
|
event Debug(address output);
|
||||||
|
|
||||||
|
function executeProposal() public {
|
||||||
|
// someValue = 321;
|
||||||
|
Dummy dummyInstance = new Dummy();
|
||||||
|
dummyInstance.initialize();
|
||||||
|
emit Debug(address(dummyInstance));
|
||||||
|
}
|
||||||
|
}
|
13
contracts/v1/Mocks/ProposalStateChangeGovernance.sol
Normal file
13
contracts/v1/Mocks/ProposalStateChangeGovernance.sol
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
interface IGovernance {
|
||||||
|
function setExecutionDelay(uint256 delay) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
contract ProposalStateChangeGovernance {
|
||||||
|
function executeProposal() public {
|
||||||
|
IGovernance(address(this)).setExecutionDelay(3 days);
|
||||||
|
}
|
||||||
|
}
|
32
contracts/v1/Mocks/ProposalUpgrade.sol
Normal file
32
contracts/v1/Mocks/ProposalUpgrade.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "./MockGovernance.sol";
|
||||||
|
|
||||||
|
interface IProxy {
|
||||||
|
function upgradeTo(address newImplementation) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
contract NewImplementation is MockGovernance {
|
||||||
|
uint256 public newVariable;
|
||||||
|
event Overriden(uint256 x);
|
||||||
|
|
||||||
|
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(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;
|
||||||
|
}
|
||||||
|
}
|
58
contracts/v2-vault-and-gas/GasCompensator.sol
Normal file
58
contracts/v2-vault-and-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/GovernanceGasUpgrade.sol
Normal file
155
contracts/v2-vault-and-gas/GovernanceGasUpgrade.sol
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { GovernanceVaultUpgrade } from "./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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
contracts/v2-vault-and-gas/GovernanceVaultUpgrade.sol
Normal file
43
contracts/v2-vault-and-gas/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);
|
||||||
|
}
|
||||||
|
}
|
29
contracts/v2-vault-and-gas/ProposalChanges.md
Normal file
29
contracts/v2-vault-and-gas/ProposalChanges.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Tornado Governance Changes
|
||||||
|
|
||||||
|
Governance proposal [repo](https://github.com/peppersec/tornado-vault-and-gas-proposal).
|
||||||
|
|
||||||
|
## 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 |
|
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;
|
||||||
|
}
|
16
contracts/v2-vault-and-gas/testing/MockProposal.sol
Normal file
16
contracts/v2-vault-and-gas/testing/MockProposal.sol
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
|
||||||
|
import "../../v1/Governance.sol";
|
||||||
|
|
||||||
|
contract MockProposal {
|
||||||
|
address public constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
|
||||||
|
|
||||||
|
function executeProposal() external {
|
||||||
|
Governance gov = Governance(GovernanceAddress);
|
||||||
|
|
||||||
|
gov.setVotingPeriod(27000);
|
||||||
|
require(gov.VOTING_PERIOD() == 27000, "Voting period change failed!");
|
||||||
|
}
|
||||||
|
}
|
61
contracts/v3-relayer-registry/GovernanceStakingUpgrade.sol
Normal file
61
contracts/v3-relayer-registry/GovernanceStakingUpgrade.sol
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { GovernanceGasUpgrade } from "../v2-vault-and-gas/GovernanceGasUpgrade.sol";
|
||||||
|
import { ITornadoStakingRewards } from "./interfaces/ITornadoStakingRewards.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice The Governance staking upgrade. Adds modifier to any un/lock operation to update rewards
|
||||||
|
* @dev CONTRACT RISKS:
|
||||||
|
* - if updateRewards reverts (should not happen due to try/catch) locks/unlocks could be blocked
|
||||||
|
* - generally inherits risks from former governance upgrades
|
||||||
|
*/
|
||||||
|
contract GovernanceStakingUpgrade is GovernanceGasUpgrade {
|
||||||
|
ITornadoStakingRewards public immutable Staking;
|
||||||
|
|
||||||
|
event RewardUpdateSuccessful(address indexed account);
|
||||||
|
event RewardUpdateFailed(address indexed account, bytes indexed errorData);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
address stakingRewardsAddress,
|
||||||
|
address gasCompLogic,
|
||||||
|
address userVaultAddress
|
||||||
|
) public GovernanceGasUpgrade(gasCompLogic, userVaultAddress) {
|
||||||
|
Staking = ITornadoStakingRewards(stakingRewardsAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This modifier should make a call to Staking to update the rewards for account without impacting logic on revert
|
||||||
|
* @dev try / catch block to handle reverts
|
||||||
|
* @param account Account to update rewards for.
|
||||||
|
* */
|
||||||
|
modifier updateRewards(address account) {
|
||||||
|
try Staking.updateRewardsOnLockedBalanceChange(account, lockedBalance[account]) {
|
||||||
|
emit RewardUpdateSuccessful(account);
|
||||||
|
} catch (bytes memory errorData) {
|
||||||
|
emit RewardUpdateFailed(account, errorData);
|
||||||
|
}
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock(
|
||||||
|
address owner,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) public virtual override updateRewards(owner) {
|
||||||
|
super.lock(owner, amount, deadline, v, r, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockWithApproval(uint256 amount) public virtual override updateRewards(msg.sender) {
|
||||||
|
super.lockWithApproval(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlock(uint256 amount) public virtual override updateRewards(msg.sender) {
|
||||||
|
super.unlock(amount);
|
||||||
|
}
|
||||||
|
}
|
14
contracts/v3-relayer-registry/ProposalChanges.md
Normal file
14
contracts/v3-relayer-registry/ProposalChanges.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Tornado Relayer Registry
|
||||||
|
|
||||||
|
Governance proposal [repo](https://github.com/Rezan-vm/tornado-relayer-registry).
|
||||||
|
|
||||||
|
Governance upgrade which includes a registry for relayer registration and staking mechanisms for the TORN token.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
1. Anyone can become a relayer by staking TORN into Registry contract.
|
||||||
|
2. Minimum stake is governed by the Governance.
|
||||||
|
3. Each Pool has its own fee % which is also set by the Governance.
|
||||||
|
4. On every withdrawal via relayer, the relayer has to pay the Tornado Pool fee in TORN. The fee is deducted from his staked balance.
|
||||||
|
5. All collected fees are stored into StakingReward contract.
|
||||||
|
6. Any TORN holder can stake their TORN into Governance contract like they were before, but earning fees proportionately to their stake.
|
@ -0,0 +1,7 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
|
||||||
|
interface ITornadoStakingRewards {
|
||||||
|
function updateRewardsOnLockedBalanceChange(address account, uint256 amountLockedBeforehand) external;
|
||||||
|
}
|
56
hardhat.config.js
Normal file
56
hardhat.config.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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('solidity-coverage')
|
||||||
|
|
||||||
|
const config = require('./config')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type import('hardhat/config').HardhatUserConfig
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
solidity: {
|
||||||
|
compilers: [
|
||||||
|
{
|
||||||
|
version: '0.6.12',
|
||||||
|
settings: {
|
||||||
|
optimizer: {
|
||||||
|
enabled: true,
|
||||||
|
runs: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
hardhat: {
|
||||||
|
forking: {
|
||||||
|
url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
|
||||||
|
blockNumber: config.forkBlockNumber,
|
||||||
|
},
|
||||||
|
initialBaseFeePerGas: 5,
|
||||||
|
},
|
||||||
|
localhost: {
|
||||||
|
url: 'http://localhost:8545',
|
||||||
|
timeout: 120000,
|
||||||
|
},
|
||||||
|
mainnet: {
|
||||||
|
url: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
|
||||||
|
accounts: ['900e9f0e8ce24c022026649c48a059fb6ffa0a2523811d797b47d789bf106def'], // random pk off keys.lol
|
||||||
|
timeout: 2147483647,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mocha: { timeout: 9999999999 },
|
||||||
|
spdxLicenseIdentifier: {
|
||||||
|
overwrite: true,
|
||||||
|
runOnCompile: true,
|
||||||
|
},
|
||||||
|
etherscan: {
|
||||||
|
apiKey: `${process.env.ETHERSCAN_KEY}`,
|
||||||
|
},
|
||||||
|
}
|
56
package.json
Normal file
56
package.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"name": "tornado-governance",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"files": [
|
||||||
|
"contracts/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"coverage": "yarn hardhat coverage"
|
||||||
|
},
|
||||||
|
"author": "Tornado.cash team <hello@tornado.cash>",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@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.4",
|
||||||
|
"tornado-governance": "^1.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@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",
|
||||||
|
"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",
|
||||||
|
"solidity-coverage": "^0.7.21"
|
||||||
|
}
|
||||||
|
}
|
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
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
|
44
scripts/v1/Permit.js
Normal file
44
scripts/v1/Permit.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const { EIP712Signer } = require('@ticket721/e712')
|
||||||
|
|
||||||
|
const Permit = [
|
||||||
|
{ name: 'owner', type: 'address' },
|
||||||
|
{ name: 'spender', type: 'address' },
|
||||||
|
{ name: 'value', type: 'uint256' },
|
||||||
|
{ name: 'nonce', type: 'uint256' },
|
||||||
|
{ name: 'deadline', type: 'uint256' },
|
||||||
|
]
|
||||||
|
|
||||||
|
class PermitSigner extends EIP712Signer {
|
||||||
|
constructor(_domain, _permitArgs) {
|
||||||
|
super(_domain, ['Permit', Permit])
|
||||||
|
this.permitArgs = _permitArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
|
||||||
|
setPermitInfo(_permitArgs) {
|
||||||
|
this.permitArgs = _permitArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
getPayload() {
|
||||||
|
return this.generatePayload(this.permitArgs, 'Permit')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSignature(privateKey) {
|
||||||
|
let payload = this.getPayload()
|
||||||
|
payload.message.owner = payload.message.owner.address
|
||||||
|
const { hex, v, r, s } = await this.sign(privateKey, payload)
|
||||||
|
return {
|
||||||
|
hex,
|
||||||
|
v,
|
||||||
|
r: '0x' + r,
|
||||||
|
s: '0x' + s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignerAddress(permitArgs, signature) {
|
||||||
|
const original_payload = this.generatePayload(permitArgs, 'Permit')
|
||||||
|
return this.verify(original_payload, signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { PermitSigner }
|
37
test/utils.js
Normal file
37
test/utils.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/* global ethers, network */
|
||||||
|
|
||||||
|
async function setTime(timestamp) {
|
||||||
|
await ethers.provider.send('evm_setNextBlockTimestamp', [timestamp])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takeSnapshot() {
|
||||||
|
return await ethers.provider.send('evm_snapshot', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revertSnapshot(id) {
|
||||||
|
await ethers.provider.send('evm_revert', [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function advanceTime(sec) {
|
||||||
|
const now = (await ethers.provider.getBlock('latest')).timestamp
|
||||||
|
await setTime(now + sec)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSignerFromAddress(address) {
|
||||||
|
await network.provider.request({
|
||||||
|
method: 'hardhat_impersonateAccount',
|
||||||
|
params: [address],
|
||||||
|
})
|
||||||
|
|
||||||
|
let signer = await ethers.provider.getSigner(address)
|
||||||
|
signer.address = signer._address
|
||||||
|
return signer
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
setTime,
|
||||||
|
advanceTime,
|
||||||
|
takeSnapshot,
|
||||||
|
revertSnapshot,
|
||||||
|
getSignerFromAddress,
|
||||||
|
}
|
943
test/v1/governance.v1.test.js
Normal file
943
test/v1/governance.v1.test.js
Normal file
@ -0,0 +1,943 @@
|
|||||||
|
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 config = require('../../config')
|
||||||
|
|
||||||
|
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('V1 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://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
|
||||||
|
blockNumber: process.env.use_latest_block == 'true' ? undefined : config.forkBlockNumber,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
496
test/v2/governance.v2.test.js
Normal file
496
test/v2/governance.v2.test.js
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
const { expect } = require('chai')
|
||||||
|
const { ethers } = require('hardhat')
|
||||||
|
const { BigNumber } = require('@ethersproject/bignumber')
|
||||||
|
const { propose } = require('../../scripts/helper/propose_proposal.js')
|
||||||
|
const testcases = require('@ethersproject/testcases')
|
||||||
|
const seedbase = require('../../resources/hdnode.json')
|
||||||
|
const accountList = require('../../resources/accounts.json')
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
|
describe('V2 governance tests', () => {
|
||||||
|
///// ON-CHAIN CONSTANTS
|
||||||
|
let proxy_address = '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce'
|
||||||
|
let quorumVotes
|
||||||
|
|
||||||
|
///////////////////////////// CONTRACTS
|
||||||
|
let GovernanceContract
|
||||||
|
let TornToken
|
||||||
|
|
||||||
|
//////////////////// IMPERSONATED
|
||||||
|
let tornadoMultisig
|
||||||
|
|
||||||
|
//////////////////////////////// MOCK
|
||||||
|
let MockProposalFactory
|
||||||
|
|
||||||
|
/////// GOV PARAMS
|
||||||
|
const ProposalState = {
|
||||||
|
Pending: 0,
|
||||||
|
Active: 1,
|
||||||
|
Defeated: 2,
|
||||||
|
Timelocked: 3,
|
||||||
|
AwaitingExecution: 4,
|
||||||
|
Executed: 5,
|
||||||
|
Expired: 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
///// ACCOUNTS
|
||||||
|
let dore
|
||||||
|
let whale
|
||||||
|
let signerArray = []
|
||||||
|
let whales = []
|
||||||
|
|
||||||
|
//////////////////////////////////// TESTING & UTILITY
|
||||||
|
let randN = Math.floor(Math.random() * 1023)
|
||||||
|
let testseed = seedbase[randN].seed
|
||||||
|
|
||||||
|
let minewait = async (time) => {
|
||||||
|
await ethers.provider.send('evm_increaseTime', [time])
|
||||||
|
await ethers.provider.send('evm_mine', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
let sendr = async (method, params) => {
|
||||||
|
return await ethers.provider.send(method, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
let clog = (...x) => {
|
||||||
|
console.log(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pE = (x) => {
|
||||||
|
return ethers.utils.parseEther(`${x}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let rand = (l, u) => {
|
||||||
|
return testcases.randomNumber(testseed, l, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshotIdArray = []
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////7
|
||||||
|
before(async function () {
|
||||||
|
signerArray = await ethers.getSigners()
|
||||||
|
dore = signerArray[0]
|
||||||
|
|
||||||
|
MockProposalFactory = await ethers.getContractFactory('MockProposal')
|
||||||
|
|
||||||
|
GovernanceContract = await ethers.getContractAt('GovernanceGasUpgrade', proxy_address)
|
||||||
|
|
||||||
|
TornToken = await ethers.getContractAt(
|
||||||
|
'@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20',
|
||||||
|
'0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
|
||||||
|
)
|
||||||
|
|
||||||
|
quorumVotes = await GovernanceContract.QUORUM_VOTES()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#imitation block', () => {
|
||||||
|
it('Should successfully imitate tornado multisig', async function () {
|
||||||
|
await sendr('hardhat_impersonateAccount', ['0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4'])
|
||||||
|
tornadoMultisig = await ethers.getSigner('0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should successfully imitate whale', async function () {
|
||||||
|
await sendr('hardhat_impersonateAccount', ['0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3'])
|
||||||
|
whale = await ethers.getSigner('0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3')
|
||||||
|
GovernanceContract = await GovernanceContract.connect(whale)
|
||||||
|
|
||||||
|
let balance = await TornToken.balanceOf(whale.address)
|
||||||
|
TornToken = await TornToken.connect(whale)
|
||||||
|
|
||||||
|
await TornToken.approve(GovernanceContract.address, ethers.utils.parseEther('8000000000'))
|
||||||
|
await expect(GovernanceContract.lockWithApproval(balance)).to.not.be.reverted
|
||||||
|
|
||||||
|
expect((await GovernanceContract.lockedBalance(whale.address)).toString()).to.equal(balance.toString())
|
||||||
|
snapshotIdArray[0] = await sendr('evm_snapshot', [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#mock rewards + proposal distribution with multiple accounts', () => {
|
||||||
|
let addrArray = []
|
||||||
|
let signerArmy = []
|
||||||
|
let delegatedSignerArmy = []
|
||||||
|
let votingAddressArray = []
|
||||||
|
const numberOfVoters = 80
|
||||||
|
const numberOfDelegators = 30
|
||||||
|
|
||||||
|
it('Should create empty address array', () => {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
votingAddressArray[i] = new Array(numberOfDelegators / 10 + 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should impersonate and fund 80 accounts', async function () {
|
||||||
|
////////// WRITE WHALE ADDRESSES AND PREPARE FOR TRANSFERS
|
||||||
|
addrArray = [
|
||||||
|
'0x6cC5F688a315f3dC28A7781717a9A798a59fDA7b',
|
||||||
|
'0xF977814e90dA44bFA03b6295A0616a897441aceC',
|
||||||
|
'0xA2b2fBCaC668d86265C45f62dA80aAf3Fd1dEde3',
|
||||||
|
'0x055AD5E56c11c0eF55818155c69ed9BA2f4b3e90',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
await sendr('hardhat_impersonateAccount', [addrArray[i]])
|
||||||
|
whales[i] = await ethers.getSigner(addrArray[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < 4; i++) {
|
||||||
|
//last test really unnecessary
|
||||||
|
const torn = await TornToken.connect(whales[i])
|
||||||
|
const whaleBalance = await torn.balanceOf(whales[i].address)
|
||||||
|
await torn.approve(addrArray[0], whaleBalance)
|
||||||
|
await expect(() => torn.transfer(addrArray[0], whaleBalance)).to.changeTokenBalance(
|
||||||
|
torn,
|
||||||
|
whales[0],
|
||||||
|
whaleBalance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const whale0Balance = await TornToken.balanceOf(whales[0].address)
|
||||||
|
const toTransfer = whale0Balance.sub(pE(10000)).div(numberOfVoters * 3)
|
||||||
|
let torn0 = await TornToken.connect(whales[0])
|
||||||
|
const oldBalance = await TornToken.balanceOf(await GovernanceContract.userVault())
|
||||||
|
let lockedSum = BigNumber.from(0)
|
||||||
|
|
||||||
|
////////// TRANSFER TO 50 ACCOUNTS + DELEGATION TO 10
|
||||||
|
|
||||||
|
for (let i = 0; i < numberOfVoters; i++) {
|
||||||
|
/// PREPARE ACCOUNTS
|
||||||
|
const accAddress = accountList[i + 7].checksumAddress
|
||||||
|
await sendr('hardhat_impersonateAccount', [accAddress])
|
||||||
|
|
||||||
|
signerArmy[i] = await ethers.getSigner(accAddress)
|
||||||
|
const tx = { to: signerArmy[i].address, value: pE(1) }
|
||||||
|
|
||||||
|
await signerArray[0].sendTransaction(tx)
|
||||||
|
|
||||||
|
/// FILL WITH GAS FOR LATER
|
||||||
|
await expect(() => torn0.transfer(signerArmy[i].address, toTransfer)).to.changeTokenBalance(
|
||||||
|
torn0,
|
||||||
|
signerArmy[i],
|
||||||
|
toTransfer,
|
||||||
|
)
|
||||||
|
let torn = await torn0.connect(signerArmy[i])
|
||||||
|
|
||||||
|
/// APPROVE TO GOVERNANCE FOR LOCK
|
||||||
|
await expect(torn.approve(GovernanceContract.address, toTransfer)).to.not.be.reverted
|
||||||
|
const gov = await GovernanceContract.connect(signerArmy[i])
|
||||||
|
|
||||||
|
///// LOCK
|
||||||
|
if (i > numberOfVoters / 2) {
|
||||||
|
await expect(() => gov.lockWithApproval(toTransfer.div(i))).to.changeTokenBalance(
|
||||||
|
torn,
|
||||||
|
signerArmy[i],
|
||||||
|
BigNumber.from(0).sub(toTransfer.div(i)),
|
||||||
|
)
|
||||||
|
lockedSum = lockedSum.add(toTransfer.div(i))
|
||||||
|
} else {
|
||||||
|
await expect(() => gov.lockWithApproval(toTransfer)).to.changeTokenBalance(
|
||||||
|
torn,
|
||||||
|
signerArmy[i],
|
||||||
|
BigNumber.from(0).sub(toTransfer),
|
||||||
|
)
|
||||||
|
lockedSum = lockedSum.add(toTransfer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > numberOfVoters - numberOfDelegators - 1) {
|
||||||
|
delegatedSignerArmy[i - (numberOfVoters - numberOfDelegators)] = signerArmy[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < 10) {
|
||||||
|
votingAddressArray[i][0] = signerArmy[i].address
|
||||||
|
}
|
||||||
|
|
||||||
|
const restBalance = await torn.balanceOf(signerArmy[i].address)
|
||||||
|
await torn.transfer(whale.address, restBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < numberOfDelegators; i++) {
|
||||||
|
const gov = await GovernanceContract.connect(delegatedSignerArmy[i])
|
||||||
|
/// DELEGATE TO 10 FIRST SIGNERS
|
||||||
|
await expect(gov.delegate(signerArmy[i % 10].address)).to.emit(gov, 'Delegated')
|
||||||
|
votingAddressArray[i % 10][Math.floor(i / 10) + 1] = delegatedSignerArmy[i].address
|
||||||
|
}
|
||||||
|
|
||||||
|
const TornVault = await GovernanceContract.userVault()
|
||||||
|
expect(await TornToken.balanceOf(TornVault)).to.equal(lockedSum.add(oldBalance))
|
||||||
|
|
||||||
|
const gov = await GovernanceContract.connect(whales[0])
|
||||||
|
await expect(torn0.approve(GovernanceContract.address, pE(10000))).to.not.be.reverted
|
||||||
|
await expect(() => gov.lockWithApproval(toTransfer)).to.changeTokenBalance(
|
||||||
|
torn0,
|
||||||
|
whales[0],
|
||||||
|
BigNumber.from(0).sub(toTransfer),
|
||||||
|
)
|
||||||
|
|
||||||
|
snapshotIdArray[1] = await sendr('evm_snapshot', [])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Test multiple accounts proposal', async function () {
|
||||||
|
let checkIfQuorumFulfilled = async function (proposalId) {
|
||||||
|
const proposalData = await GovernanceContract.proposals(proposalId)
|
||||||
|
const allVotes = proposalData[4].add(proposalData[5])
|
||||||
|
return allVotes.gte(quorumVotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProposalContract = await MockProposalFactory.deploy()
|
||||||
|
|
||||||
|
clog(
|
||||||
|
'Torn balance of governance contract: ',
|
||||||
|
(await TornToken.balanceOf(GovernanceContract.address)).toString(),
|
||||||
|
)
|
||||||
|
|
||||||
|
////////////// STANDARD PROPOSAL ARGS TEST //////////////////////
|
||||||
|
let response, id, state
|
||||||
|
;[response, id, state] = await propose([whales[0], ProposalContract, 'LotteryUpgrade'])
|
||||||
|
const { events } = await response.wait()
|
||||||
|
const args = events.find(({ event }) => event == 'ProposalCreated').args
|
||||||
|
expect(args.id).to.be.equal(id)
|
||||||
|
expect(args.target).to.be.equal(ProposalContract.address)
|
||||||
|
expect(args.description).to.be.equal('LotteryUpgrade')
|
||||||
|
expect(state).to.be.equal(ProposalState.Pending)
|
||||||
|
|
||||||
|
////////////////////////INCREMENT TO VOTING TIME////////////////////////
|
||||||
|
await minewait((await GovernanceContract.VOTING_DELAY()).add(1).toNumber())
|
||||||
|
|
||||||
|
/////////////////// PREPARE MULTISIG AND COMPENSATIONS
|
||||||
|
let multiGov = await GovernanceContract.connect(tornadoMultisig)
|
||||||
|
|
||||||
|
await dore.sendTransaction({ to: tornadoMultisig.address, value: pE(1) })
|
||||||
|
await expect(multiGov.setGasCompensations(pE(500))).to.not.be.reverted
|
||||||
|
///////////////////////////// VOTE ////////////////////////////
|
||||||
|
const overrides = {
|
||||||
|
gasPrice: BigNumber.from(5),
|
||||||
|
}
|
||||||
|
|
||||||
|
let signerArmyBalanceInitial = []
|
||||||
|
let signerArmyBalanceDiff = []
|
||||||
|
let gasUsedArray = []
|
||||||
|
|
||||||
|
snapshotIdArray[2] = await sendr('evm_snapshot', [])
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
let gov = await GovernanceContract.connect(signerArmy[i])
|
||||||
|
let randN = rand(i * 5, i * 6)
|
||||||
|
randN = randN % 2
|
||||||
|
let response
|
||||||
|
|
||||||
|
signerArmyBalanceInitial[i] = await signerArmy[i].getBalance()
|
||||||
|
|
||||||
|
if (randN > 0) {
|
||||||
|
response = await gov.castDelegatedVote(votingAddressArray[i], id, true, overrides)
|
||||||
|
} else {
|
||||||
|
response = await gov.castDelegatedVote(votingAddressArray[i], id, false, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
signerArmyBalanceDiff[i] = !(await checkIfQuorumFulfilled(id))
|
||||||
|
? signerArmyBalanceInitial[i].sub(await signerArmy[i].getBalance())
|
||||||
|
: signerArmyBalanceDiff[i - 1]
|
||||||
|
|
||||||
|
const receipt = await response.wait()
|
||||||
|
gasUsedArray[i] = receipt.cumulativeGasUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
|
||||||
|
let gov = await GovernanceContract.connect(signerArmy[i])
|
||||||
|
let randN = rand(i * 5, i * 6)
|
||||||
|
randN = randN % 2
|
||||||
|
let response
|
||||||
|
|
||||||
|
signerArmyBalanceInitial[i] = await signerArmy[i].getBalance()
|
||||||
|
|
||||||
|
if (randN > 0) {
|
||||||
|
response = await gov.castVote(id, true, overrides)
|
||||||
|
} else {
|
||||||
|
response = await gov.castVote(id, false, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
signerArmyBalanceDiff[i] = !(await checkIfQuorumFulfilled(id))
|
||||||
|
? signerArmyBalanceInitial[i].sub(await signerArmy[i].getBalance())
|
||||||
|
: signerArmyBalanceDiff[i - 1]
|
||||||
|
|
||||||
|
const receipt = await response.wait()
|
||||||
|
gasUsedArray[i] = receipt.cumulativeGasUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////// GET STATE ///////////////////////////////
|
||||||
|
state = await GovernanceContract.state(id)
|
||||||
|
expect(state).to.be.equal(ProposalState.Active)
|
||||||
|
|
||||||
|
///////////////////////////// VOTER INFO ///////////////////////////////////
|
||||||
|
// (uncomment for more data)
|
||||||
|
/*
|
||||||
|
for (i = 0; i < numberOfVoters; i+=5) {
|
||||||
|
const j = BigNumber.from(i);
|
||||||
|
console.log(
|
||||||
|
`Voter ${i} sqrt: `,
|
||||||
|
((await GovernanceLottery.lotteryUserData(id,j))[0]).toString(),
|
||||||
|
`Voter ${i+1} sqrt: `,
|
||||||
|
((await GovernanceLottery.lotteryUserData(id,j.add(1)))[0]).toString(),
|
||||||
|
`Voter ${i+2} sqrt: `,
|
||||||
|
((await GovernanceLottery.lotteryUserData(id,j.add(2)))[0]).toString(),
|
||||||
|
`Voter ${i+3} sqrt: `,
|
||||||
|
((await GovernanceLottery.lotteryUserData(id,j.add(3)))[0]).toString(),
|
||||||
|
`Voter ${i+4} sqrt: `,
|
||||||
|
((await GovernanceLottery.lotteryUserData(id,j.add(4)))[0]).toString(),
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < numberOfVoters; i+=5) {
|
||||||
|
console.log(
|
||||||
|
`Voter ${i} ether used: `,
|
||||||
|
gasUsedArray[i],
|
||||||
|
`Voter ${i+1} ether used: `,
|
||||||
|
gasUsedArray[i+1],
|
||||||
|
`Voter ${i+2} ether used: `,
|
||||||
|
gasUsedArray[i+2],
|
||||||
|
`Voter ${i+3} ether used: `,
|
||||||
|
gasUsedArray[i+3],
|
||||||
|
`Voter ${i+4} ether used: `,
|
||||||
|
gasUsedArray[i+4],
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
await sendr('evm_revert', [snapshotIdArray[2]])
|
||||||
|
|
||||||
|
///////////////////////////////// VOTE WITHOUT COMPENSATION //////////////////////////////////////
|
||||||
|
let gasUsedWithoutCompensation = []
|
||||||
|
await multiGov.setGasCompensations(pE(100000))
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
let gov = await GovernanceContract.connect(signerArmy[i])
|
||||||
|
let randN = rand(i * 5, i * 6)
|
||||||
|
randN = randN % 2
|
||||||
|
let response
|
||||||
|
|
||||||
|
if (randN > 0) {
|
||||||
|
response = await gov.castDelegatedVote(votingAddressArray[i], id, true, overrides)
|
||||||
|
} else {
|
||||||
|
response = await gov.castDelegatedVote(votingAddressArray[i], id, false, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
const receipt = await response.wait()
|
||||||
|
gasUsedWithoutCompensation[i] = receipt.cumulativeGasUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
|
||||||
|
let gov = await GovernanceContract.connect(signerArmy[i])
|
||||||
|
let randN = rand(i * 5, i * 6)
|
||||||
|
randN = randN % 2
|
||||||
|
let response
|
||||||
|
|
||||||
|
if (randN > 0) {
|
||||||
|
response = await gov.castVote(id, true, overrides)
|
||||||
|
} else {
|
||||||
|
response = await gov.castVote(id, false, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
const receipt = await response.wait()
|
||||||
|
|
||||||
|
gasUsedWithoutCompensation[i] = receipt.cumulativeGasUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
await multiGov.setGasCompensations(pE(100))
|
||||||
|
//////////////////////////////// GET STATE ///////////////////////////////
|
||||||
|
state = await GovernanceContract.state(id)
|
||||||
|
expect(state).to.be.equal(ProposalState.Active)
|
||||||
|
|
||||||
|
///////////////////////////// VOTING GAS INFO ///////////////////////////////////
|
||||||
|
let gasUsedSumNoComp = BigNumber.from(0)
|
||||||
|
let gasUsedSum = BigNumber.from(0)
|
||||||
|
let gasSumDiff = BigNumber.from(0)
|
||||||
|
let gasUsedSumNoCompDel = BigNumber.from(0)
|
||||||
|
let gasUsedSumDel = BigNumber.from(0)
|
||||||
|
let gasSumDiffDel = BigNumber.from(0)
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
gasUsedSumDel = gasUsedSumDel.add(gasUsedArray[i])
|
||||||
|
gasUsedSumNoCompDel = gasUsedSumNoCompDel.add(gasUsedWithoutCompensation[i])
|
||||||
|
gasSumDiffDel = gasSumDiffDel.add(signerArmyBalanceDiff[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 10; i < numberOfVoters - numberOfDelegators; i++) {
|
||||||
|
gasUsedSum = gasUsedSum.add(gasUsedArray[i])
|
||||||
|
gasUsedSumNoComp = gasUsedSumNoComp.add(gasUsedWithoutCompensation[i])
|
||||||
|
gasSumDiff = gasSumDiff.add(signerArmyBalanceDiff[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
const gasUsedAverageNoCompDel = gasUsedSumNoCompDel.div(10)
|
||||||
|
const gasUsedAverageDel = gasUsedSumDel.div(10)
|
||||||
|
const gasSumAverageDiffDel = gasSumDiffDel.div(10)
|
||||||
|
|
||||||
|
const gasUsedAverageNoComp = gasUsedSumNoComp.div(numberOfVoters - 10)
|
||||||
|
const gasUsedAverage = gasUsedSum.div(numberOfVoters - 10)
|
||||||
|
const gasSumAverageDiff = gasSumDiff.div(numberOfVoters - 10)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'\n',
|
||||||
|
'----------------------------CAST VOTE INFO------------------------',
|
||||||
|
'\n',
|
||||||
|
'Gas use average: ',
|
||||||
|
gasUsedAverage.toString(),
|
||||||
|
'\n',
|
||||||
|
'Gas use without compensation average: ',
|
||||||
|
gasUsedAverageNoComp.toString(),
|
||||||
|
'\n',
|
||||||
|
'Gas diff average: ',
|
||||||
|
gasSumAverageDiff.toString(),
|
||||||
|
'\n',
|
||||||
|
'Gas compensated in average: ',
|
||||||
|
gasUsedAverage.sub(gasSumAverageDiff).toString(),
|
||||||
|
'\n',
|
||||||
|
'--------------------------------------------------------------------',
|
||||||
|
'\n',
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'\n',
|
||||||
|
'----------------------------CAST DELEGATED VOTE INFO------------------------',
|
||||||
|
'\n',
|
||||||
|
'Gas use average: ',
|
||||||
|
gasUsedAverageDel.toString(),
|
||||||
|
'\n',
|
||||||
|
'Gas use without compensation average: ',
|
||||||
|
gasUsedAverageNoCompDel.toString(),
|
||||||
|
'\n',
|
||||||
|
'Gas diff average: ',
|
||||||
|
gasSumAverageDiffDel.toString(),
|
||||||
|
'\n',
|
||||||
|
'Gas compensated in average: ',
|
||||||
|
gasUsedAverageDel.sub(gasSumAverageDiffDel).toString(),
|
||||||
|
'\n',
|
||||||
|
'--------------------------------------------------------------------',
|
||||||
|
'\n',
|
||||||
|
)
|
||||||
|
/////////////////////////////// INCREMENT AGAIN //////////////////////////////////
|
||||||
|
await minewait(
|
||||||
|
(
|
||||||
|
await GovernanceContract.VOTING_PERIOD()
|
||||||
|
)
|
||||||
|
.add(await GovernanceContract.EXECUTION_DELAY())
|
||||||
|
.add(10000)
|
||||||
|
.toNumber(),
|
||||||
|
)
|
||||||
|
|
||||||
|
////////////// EXECUTE
|
||||||
|
if (BigNumber.from(await GovernanceContract.state(id)).eq(ProposalState.Defeated)) {
|
||||||
|
await expect(GovernanceContract.execute(id)).to.be.reverted
|
||||||
|
} else {
|
||||||
|
await expect(GovernanceContract.execute(id)).to.not.be.reverted
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await ethers.provider.send('hardhat_reset', [
|
||||||
|
{
|
||||||
|
forking: {
|
||||||
|
jsonRpcUrl: `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
|
||||||
|
blockNumber: process.env.use_latest_block == 'true' ? undefined : config.forkBlockNumber,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
71
test/v3/governance.v3.test.js
Normal file
71
test/v3/governance.v3.test.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
const { ethers } = require('hardhat')
|
||||||
|
const { expect } = require('chai')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
const { getSignerFromAddress, takeSnapshot, revertSnapshot } = require('../utils')
|
||||||
|
|
||||||
|
describe('V3 governance tests', () => {
|
||||||
|
let snapshotId
|
||||||
|
|
||||||
|
//// CONTRACTS
|
||||||
|
let torn = config.TORN
|
||||||
|
let gov
|
||||||
|
|
||||||
|
//// IMPERSONATED ACCOUNTS
|
||||||
|
let tornWhale
|
||||||
|
|
||||||
|
//// HELPER FN
|
||||||
|
let getToken = async (tokenAddress) => {
|
||||||
|
return await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/ERC20.sol:ERC20', tokenAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
tornWhale = await getSignerFromAddress(config.tornWhale)
|
||||||
|
|
||||||
|
gov = (await ethers.getContractAt('GovernanceStakingUpgrade', config.governance)).connect(tornWhale)
|
||||||
|
|
||||||
|
snapshotId = await takeSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#lock functionality', () => {
|
||||||
|
it('should be able to lock/unlock torn in governance', async () => {
|
||||||
|
const [sender] = await ethers.getSigners()
|
||||||
|
const value = ethers.utils.parseEther('1000')
|
||||||
|
|
||||||
|
const tornToken = await (await getToken(torn)).connect(tornWhale)
|
||||||
|
await tornToken.transfer(sender.address, value)
|
||||||
|
await tornToken.connect(sender).approve(gov.address, value)
|
||||||
|
|
||||||
|
const ethBalanceBeforeLock = await ethers.provider.getBalance(sender.address)
|
||||||
|
const tokenBalanceBeforeLock = await tornToken.balanceOf(sender.address)
|
||||||
|
let tx = await gov.connect(sender).lockWithApproval(value)
|
||||||
|
|
||||||
|
let receipt = await tx.wait()
|
||||||
|
let txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice)
|
||||||
|
const ethBalanceAfterLock = await ethers.provider.getBalance(sender.address)
|
||||||
|
const tokenBalanceAfterLock = await tornToken.balanceOf(sender.address)
|
||||||
|
expect(ethBalanceAfterLock).to.be.equal(ethBalanceBeforeLock.sub(txFee))
|
||||||
|
expect(tokenBalanceAfterLock).to.be.equal(tokenBalanceBeforeLock.sub(value))
|
||||||
|
|
||||||
|
const lockedBalanceAfterLock = await gov.lockedBalance(sender.address)
|
||||||
|
expect(lockedBalanceAfterLock).to.be.equal(value)
|
||||||
|
|
||||||
|
tx = await gov.connect(sender).unlock(value)
|
||||||
|
|
||||||
|
receipt = await tx.wait()
|
||||||
|
txFee = receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice)
|
||||||
|
const ethBalanceAfterUnlock = await ethers.provider.getBalance(sender.address)
|
||||||
|
const tokenBalanceAfterUnlock = await tornToken.balanceOf(sender.address)
|
||||||
|
expect(ethBalanceAfterUnlock).to.be.equal(ethBalanceAfterLock.sub(txFee))
|
||||||
|
expect(tokenBalanceAfterUnlock).to.be.equal(tokenBalanceBeforeLock)
|
||||||
|
|
||||||
|
const lockedBalanceAfterUnlock = await gov.lockedBalance(sender.address)
|
||||||
|
expect(lockedBalanceAfterUnlock).to.be.equal(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await revertSnapshot(snapshotId)
|
||||||
|
snapshotId = await takeSnapshot()
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user