mirror of
https://github.com/tornadocash/tornado-governance
synced 2024-02-02 14:53:55 +01:00
initial
This commit is contained in:
commit
71fa601fd3
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
© 2020 GitHub, Inc.
|
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
||||
PRIVATE_KEY=
|
||||
INFURA_TOKEN=
|
||||
TORN_ADDRESS=
|
26
.eslintrc
Normal file
26
.eslintrc
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"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"
|
||||
}
|
||||
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sol linguist-language=Solidity
|
28
.github/workflows/build.yml
vendored
Normal file
28
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- run: yarn install
|
||||
- run: yarn test
|
||||
- run: yarn lint
|
||||
- name: Telegram Failure Notification
|
||||
uses: appleboy/telegram-action@0.0.7
|
||||
if: failure()
|
||||
with:
|
||||
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) because of ${{ github.actor }}
|
||||
format: markdown
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
99
.gitignore
vendored
Normal file
99
.gitignore
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
build
|
||||
.vscode
|
||||
/index.js
|
||||
flats/*
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
ERC20Tornado_flat.sol
|
||||
ETHTornado_flat.sol
|
||||
.env
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
.vscode
|
||||
build
|
||||
scripts
|
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
14
.solhint.json
Normal file
14
.solhint.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "solhint:recommended",
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 110
|
||||
}
|
||||
],
|
||||
"quotes": ["error", "double"],
|
||||
"indent": ["error", 2]
|
||||
},
|
||||
"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.
|
||||
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Tornado.Cash Governance [](https://github.com/tornadocash/governance/actions)
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
yarn
|
||||
cp .env.example .env
|
||||
yarn deploy:kovan
|
||||
```
|
||||
|
||||
## How to upgrade implementation
|
||||
|
||||
1. Make sure once you deploy new Governance implementation, call `initialize` methods right after it.
|
72
contracts/Configuration.sol
Normal file
72
contracts/Configuration.sol
Normal file
@ -0,0 +1,72 @@
|
||||
//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;
|
||||
}
|
||||
}
|
8
contracts/Core.sol
Normal file
8
contracts/Core.sol
Normal file
@ -0,0 +1,8 @@
|
||||
//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;
|
||||
}
|
65
contracts/Delegation.sol
Normal file
65
contracts/Delegation.sol
Normal file
@ -0,0 +1,65 @@
|
||||
//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 {
|
||||
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;
|
||||
}
|
281
contracts/Governance.sol
Normal file
281
contracts/Governance.sol
Normal file
@ -0,0 +1,281 @@
|
||||
//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
|
||||
) external {
|
||||
torn.permit(owner, address(this), amount, deadline, v, r, s);
|
||||
_transferTokens(owner, amount);
|
||||
}
|
||||
|
||||
function lockWithApproval(uint256 amount) external {
|
||||
_transferTokens(msg.sender, amount);
|
||||
}
|
||||
|
||||
function unlock(uint256 amount) external {
|
||||
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 virtual payable {
|
||||
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 {
|
||||
_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 {
|
||||
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 virtual view returns (uint256) {
|
||||
// solium-disable-next-line security/no-block-members
|
||||
return block.timestamp;
|
||||
}
|
||||
}
|
25
contracts/LoopbackProxy.sol
Normal file
25
contracts/LoopbackProxy.sol
Normal file
@ -0,0 +1,25 @@
|
||||
// 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(bytes32 _logic, bytes memory _data)
|
||||
public
|
||||
payable
|
||||
TransparentUpgradeableProxy(resolve(_logic), address(this), _data)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @dev Override to allow admin (itself) access the fallback function.
|
||||
*/
|
||||
function _beforeFallback() internal override {}
|
||||
}
|
31
contracts/Mocks/Dummy.sol
Normal file
31
contracts/Mocks/Dummy.sol
Normal file
@ -0,0 +1,31 @@
|
||||
//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);
|
||||
// }
|
||||
}
|
22
contracts/Mocks/MockGovernance.sol
Normal file
22
contracts/Mocks/MockGovernance.sol
Normal file
@ -0,0 +1,22 @@
|
||||
//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 override view returns (uint256) {
|
||||
// solium-disable-next-line security/no-block-members
|
||||
return time;
|
||||
}
|
||||
|
||||
function resolve(bytes32 addr) public override view returns (address) {
|
||||
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||
}
|
||||
}
|
12
contracts/Mocks/MockProxy.sol
Normal file
12
contracts/Mocks/MockProxy.sol
Normal file
@ -0,0 +1,12 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.6.0;
|
||||
|
||||
import "../LoopbackProxy.sol";
|
||||
|
||||
contract MockProxy is LoopbackProxy {
|
||||
constructor(bytes32 _logic, bytes memory _data) public payable LoopbackProxy(_logic, _data) {}
|
||||
|
||||
function resolve(bytes32 addr) public override view returns (address) {
|
||||
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||
}
|
||||
}
|
18
contracts/Mocks/Proposal.sol
Normal file
18
contracts/Mocks/Proposal.sol
Normal file
@ -0,0 +1,18 @@
|
||||
//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));
|
||||
}
|
||||
}
|
12
contracts/Mocks/ProposalStateChangeGovernance.sol
Normal file
12
contracts/Mocks/ProposalStateChangeGovernance.sol
Normal file
@ -0,0 +1,12 @@
|
||||
//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);
|
||||
}
|
||||
}
|
25
contracts/Mocks/ProposalUpgrade.sol
Normal file
25
contracts/Mocks/ProposalUpgrade.sol
Normal file
@ -0,0 +1,25 @@
|
||||
//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 override payable {
|
||||
newVariable = 999;
|
||||
emit Overriden(proposalId);
|
||||
}
|
||||
}
|
||||
|
||||
contract ProposalUpgrade {
|
||||
function executeProposal() public {
|
||||
IProxy(address(this)).upgradeTo(0xF7E3e47e06F1bDDecb1b2F3a7F60b6b25fd2e233);
|
||||
}
|
||||
}
|
5
contracts/Mocks/TORNMock.sol
Normal file
5
contracts/Mocks/TORNMock.sol
Normal file
@ -0,0 +1,5 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.6.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "torn-token/contracts/mocks/TORNMock.sol";
|
3
flat.sh
Executable file
3
flat.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
npx truffle-flattener contracts/Governance.sol > flats/Governance_flat.sol
|
||||
npx truffle-flattener contracts/LoopbackProxy.sol > flats/MyProxy_flat.sol
|
43
lib/Permit.js
Normal file
43
lib/Permit.js
Normal file
@ -0,0 +1,43 @@
|
||||
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) {
|
||||
const payload = this.getPayload()
|
||||
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 }
|
47
package.json
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "torn_governance",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"contracts/*"
|
||||
],
|
||||
"scripts": {
|
||||
"compile": "truffle compile",
|
||||
"deploy:mainnet": "truffle migrate --network mainnet",
|
||||
"deploy:kovan": "truffle migrate --network kovan",
|
||||
"deploy:dev": "truffle migrate --skip-dry-run --network development",
|
||||
"test": "truffle test",
|
||||
"test:stacktrace": "yarn test --stacktrace",
|
||||
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||
"prettier:check": "prettier --check . --config .prettierrc",
|
||||
"prettier:fix": "prettier --write . --config .prettierrc",
|
||||
"lint": "yarn eslint && yarn prettier:check"
|
||||
},
|
||||
"author": "Tornado.cash team <hello@tornado.cash>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^3.2.0-rc.0",
|
||||
"@openzeppelin/upgrades-core": "^1.0.1",
|
||||
"torn-token": "git+https://github.com/tornadocash/torn-token.git#d7b86c01538cd26afae73885f66269a98c02aa3f"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openzeppelin/truffle-upgrades": "^1.0.2",
|
||||
"@ticket721/e712": "^0.4.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bn-chai": "^1.0.1",
|
||||
"bn.js": "^5.1.3",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^7.8.1",
|
||||
"prettier": "^2.1.1",
|
||||
"prettier-plugin-solidity": "^1.0.0-alpha.57",
|
||||
"solhint-plugin-prettier": "^0.0.5",
|
||||
"solidity-coverage": "^0.7.10",
|
||||
"truffle": "^5.1.43",
|
||||
"truffle-flattener": "^1.4.4",
|
||||
"truffle-hdwallet-provider": "^1.0.17"
|
||||
}
|
||||
}
|
55
scripts/ganacheHelper.js
Normal file
55
scripts/ganacheHelper.js
Normal file
@ -0,0 +1,55 @@
|
||||
// This module is used only for tests
|
||||
function send(method, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
web3.currentProvider.send(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method,
|
||||
params,
|
||||
},
|
||||
(err, res) => {
|
||||
return err ? reject(err) : resolve(res)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const takeSnapshot = async () => {
|
||||
return await send('evm_snapshot')
|
||||
}
|
||||
|
||||
const traceTransaction = async (tx) => {
|
||||
return await send('debug_traceTransaction', [tx, {}])
|
||||
}
|
||||
|
||||
const revertSnapshot = async (id) => {
|
||||
await send('evm_revert', [id])
|
||||
}
|
||||
|
||||
const mineBlock = async (timestamp) => {
|
||||
await send('evm_mine', [timestamp])
|
||||
}
|
||||
|
||||
const increaseTime = async (seconds) => {
|
||||
await send('evm_increaseTime', [seconds])
|
||||
}
|
||||
|
||||
const minerStop = async () => {
|
||||
await send('miner_stop', [])
|
||||
}
|
||||
|
||||
const minerStart = async () => {
|
||||
await send('miner_start', [])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
takeSnapshot,
|
||||
revertSnapshot,
|
||||
mineBlock,
|
||||
minerStop,
|
||||
minerStart,
|
||||
increaseTime,
|
||||
traceTransaction,
|
||||
}
|
721
test/governance.test.js
Normal file
721
test/governance.test.js
Normal file
@ -0,0 +1,721 @@
|
||||
/* global artifacts, web3, contract */
|
||||
require('chai').use(require('bn-chai')(web3.utils.BN)).use(require('chai-as-promised')).should()
|
||||
const util = require('ethereumjs-util')
|
||||
|
||||
const Governance = artifacts.require('./MockGovernance.sol')
|
||||
const Dummy = artifacts.require('./Dummy.sol')
|
||||
const Proposal = artifacts.require('./Proposal.sol')
|
||||
const Torn = artifacts.require('./TORNMock.sol')
|
||||
const TransparentUpgradeableProxy = artifacts.require('./MockProxy.sol')
|
||||
const ProposalStateChangeGovernance = artifacts.require('./ProposalStateChangeGovernance.sol')
|
||||
const NewImplementation = artifacts.require('./NewImplementation.sol')
|
||||
const ProposalUpgrade = artifacts.require('./ProposalUpgrade.sol')
|
||||
|
||||
const { PermitSigner } = require('../lib/Permit')
|
||||
|
||||
const { toBN, toChecksumAddress } = require('web3-utils')
|
||||
const { takeSnapshot, revertSnapshot } = require('../scripts/ganacheHelper')
|
||||
const BN = require('bn.js')
|
||||
const tornConfig = require('torn-token')
|
||||
const RLP = require('rlp')
|
||||
|
||||
const ProposalState = {
|
||||
Pending: 0,
|
||||
Active: 1,
|
||||
Defeated: 2,
|
||||
Timelocked: 3,
|
||||
AwaitingExecution: 4,
|
||||
Executed: 5,
|
||||
Expired: 6,
|
||||
}
|
||||
|
||||
const duration = {
|
||||
seconds: function (val) {
|
||||
return val
|
||||
},
|
||||
minutes: function (val) {
|
||||
return val * this.seconds(60)
|
||||
},
|
||||
hours: function (val) {
|
||||
return val * this.minutes(60)
|
||||
},
|
||||
days: function (val) {
|
||||
return val * this.hours(24)
|
||||
},
|
||||
weeks: function (val) {
|
||||
return val * this.days(7)
|
||||
},
|
||||
years: function (val) {
|
||||
return val * this.days(365)
|
||||
},
|
||||
}
|
||||
|
||||
async function getNextAddr(sender, offset = 0) {
|
||||
const nonce = await web3.eth.getTransactionCount(sender)
|
||||
return (
|
||||
'0x' +
|
||||
web3.utils
|
||||
.sha3(RLP.encode([sender, Number(nonce) + Number(offset)]))
|
||||
.slice(12)
|
||||
.substring(14)
|
||||
)
|
||||
}
|
||||
|
||||
contract('Governance', (accounts) => {
|
||||
let governance, dummy
|
||||
let proposer = accounts[3]
|
||||
let secondProposer = accounts[8]
|
||||
let snapshotId
|
||||
let timestamp = 1577836800 // 01/01/2020 00:00
|
||||
let torn
|
||||
let chainId
|
||||
let domain
|
||||
let votingDelay
|
||||
let votingPeriod
|
||||
let executionDelay
|
||||
let proposalStartTime
|
||||
let proposalEndTime
|
||||
let delay
|
||||
let balanceProposer
|
||||
const cap = toBN(tornConfig.torn.cap)
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000))
|
||||
const miningPrivateKey = '0xc87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'
|
||||
const miningPublicKey = toChecksumAddress(
|
||||
'0x' + util.privateToAddress(Buffer.from(miningPrivateKey.slice(2), 'hex')).toString('hex'),
|
||||
)
|
||||
|
||||
before(async () => {
|
||||
chainId = await web3.eth.net.getId()
|
||||
const governanceExpectedAddr = await getNextAddr(accounts[0], 2)
|
||||
torn = await Torn.new(governanceExpectedAddr, duration.days(30), [
|
||||
{ to: miningPublicKey, amount: cap.toString() },
|
||||
])
|
||||
const governanceImplementation = await Governance.new()
|
||||
const calldata = governanceImplementation.contract.methods.initialize(torn.address).encodeABI()
|
||||
const proxy = await TransparentUpgradeableProxy.new(governanceImplementation.address, calldata)
|
||||
governance = await Governance.at(proxy.address)
|
||||
dummy = await Dummy.new()
|
||||
balanceProposer = cap.div(toBN(4))
|
||||
await torn.transfer(secondProposer, balanceProposer.div(toBN(2)), { from: miningPublicKey })
|
||||
await torn.transfer(proposer, balanceProposer, { from: miningPublicKey })
|
||||
await torn.setChainId(chainId)
|
||||
await governance.setTimestamp(timestamp)
|
||||
delay = duration.days(2)
|
||||
votingDelay = await governance.VOTING_DELAY()
|
||||
votingPeriod = await governance.VOTING_PERIOD()
|
||||
executionDelay = await governance.EXECUTION_DELAY()
|
||||
proposalStartTime = new BN(timestamp).add(votingDelay)
|
||||
proposalEndTime = votingPeriod.add(toBN(proposalStartTime))
|
||||
domain = {
|
||||
name: await torn.name(),
|
||||
version: '1',
|
||||
chainId,
|
||||
verifyingContract: torn.address,
|
||||
}
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
beforeEach(async () => {
|
||||
await torn.approve(governance.address, cap.div(toBN(4)), { from: proposer })
|
||||
await governance.lockWithApproval(cap.div(toBN(4)), { from: proposer })
|
||||
const balance = await governance.lockedBalance(proposer)
|
||||
balance.should.be.eq.BN(cap.div(toBN(4)))
|
||||
})
|
||||
describe('#constructor', () => {
|
||||
it('should work', async () => {
|
||||
const proposalCount = await governance.proposalCount()
|
||||
proposalCount.should.be.eq.BN(0)
|
||||
|
||||
const p = await governance.proposals(0)
|
||||
p.proposer.should.be.equal(governance.address)
|
||||
p.target.should.be.equal('0x000000000000000000000000000000000000dEaD')
|
||||
p.endTime.should.be.eq.BN(toBN(0))
|
||||
p.forVotes.should.be.eq.BN(toBN(0))
|
||||
p.againstVotes.should.be.eq.BN(toBN(0))
|
||||
p.executed.should.be.equal(true)
|
||||
p.extended.should.be.equal(false)
|
||||
})
|
||||
})
|
||||
describe('#propose', () => {
|
||||
it('should work', async () => {
|
||||
const { logs } = await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
const proposalCount = await governance.proposalCount()
|
||||
proposalCount.should.be.eq.BN(1)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.proposer.should.be.equal(proposer)
|
||||
proposal.startTime.should.be.eq.BN(proposalStartTime)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
proposal.executed.should.be.equal(false)
|
||||
|
||||
// emit ProposalCreated(newProposal.id, msg.sender, target, startBlock, endBlock, description);
|
||||
logs[0].event.should.be.equal('ProposalCreated')
|
||||
logs[0].args.id.should.be.eq.BN(id)
|
||||
logs[0].args.proposer.should.be.eq.BN(proposer)
|
||||
logs[0].args.target.should.be.eq.BN(dummy.address)
|
||||
logs[0].args.description.should.be.eq.BN('dummy')
|
||||
logs[0].args.startTime.should.be.eq.BN(proposalStartTime)
|
||||
logs[0].args.endTime.should.be.eq.BN(votingPeriod.add(toBN(proposalStartTime)))
|
||||
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Pending)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
|
||||
const accountLock = await governance.canWithdrawAfter(proposer)
|
||||
accountLock.should.be.eq.BN(proposalEndTime.add(toBN(delay)))
|
||||
})
|
||||
it('fails if target is not a contract', async () => {
|
||||
await governance
|
||||
.propose(accounts[9], 'dummy', { from: proposer })
|
||||
.should.be.rejectedWith('not a contract')
|
||||
})
|
||||
it('fails if proposer has already pending proposal', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance
|
||||
.propose(dummy.address, 'dummy', { from: proposer })
|
||||
.should.be.rejectedWith(
|
||||
'Governance::propose: one live proposal per proposer, found an already active proposal',
|
||||
)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
await governance
|
||||
.propose(dummy.address, 'dummy', { from: proposer })
|
||||
.should.be.rejectedWith(
|
||||
'Governance::propose: one live proposal per proposer, found an already active proposal',
|
||||
)
|
||||
})
|
||||
it('fails if proposer does not have voting power', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(9999))
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterBob })
|
||||
await governance
|
||||
.propose(dummy.address, 'dummy', { from: voterBob })
|
||||
.should.be.rejectedWith('Governance::propose: proposer votes below proposal threshold.')
|
||||
})
|
||||
})
|
||||
describe('#castVote', () => {
|
||||
it('should work if support is true', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
const { logs } = await governance.castVote(id, true, { from: proposer })
|
||||
logs[0].event.should.be.equal('Voted')
|
||||
logs[0].args.voter.should.be.equal(proposer)
|
||||
logs[0].args.proposalId.should.be.eq.BN(id)
|
||||
logs[0].args.support.should.be.equal(true)
|
||||
logs[0].args.votes.should.be.eq.BN(votesCount)
|
||||
await governance.getReceipt(id, proposer)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(votesCount)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
})
|
||||
it('should work if support is false', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
const { logs } = await governance.castVote(id, false, { from: proposer })
|
||||
logs[0].event.should.be.equal('Voted')
|
||||
logs[0].args.voter.should.be.equal(proposer)
|
||||
logs[0].args.proposalId.should.be.eq.BN(id)
|
||||
logs[0].args.support.should.be.equal(false)
|
||||
logs[0].args.votes.should.be.eq.BN(votesCount)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount)
|
||||
})
|
||||
it('should be able to change the choice later if already voted before', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
const { logs } = await governance.castVote(id, false, { from: proposer })
|
||||
logs[0].event.should.be.equal('Voted')
|
||||
logs[0].args.voter.should.be.equal(proposer)
|
||||
logs[0].args.proposalId.should.be.eq.BN(id)
|
||||
logs[0].args.support.should.be.equal(false)
|
||||
logs[0].args.votes.should.be.eq.BN(votesCount)
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount)
|
||||
})
|
||||
it('should work if there are multiple voters', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const voterAlice = accounts[7]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000)) // todo
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.transfer(voterAlice, tenThousandTorn.mul(toBN(2)), { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
await torn.approve(governance.address, tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterBob })
|
||||
await governance.lockWithApproval(tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
await governance.castVote(id, true, { from: voterAlice })
|
||||
|
||||
const proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(tenThousandTorn.mul(toBN(2)))
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount.add(tenThousandTorn))
|
||||
})
|
||||
it('fails if voter does not have voting power', async () => {
|
||||
const voterBob = accounts[5]
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance
|
||||
.castVote(id, false, { from: voterBob })
|
||||
.should.be.rejectedWith('Governance: balance is 0')
|
||||
})
|
||||
it('should be able to update number of votes count if the same decision is chosen after more tokens are locked', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000)) // todo
|
||||
const fiveThousandTorn = tenThousandTorn.div(toBN(2))
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.lockWithApproval(fiveThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
|
||||
let proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(toBN(0))
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount.add(fiveThousandTorn))
|
||||
|
||||
await governance.lockWithApproval(fiveThousandTorn, { from: voterBob })
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
|
||||
proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(toBN(0))
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount.add(tenThousandTorn))
|
||||
})
|
||||
it('extends time if the vote changes the outcome during the CLOSING_PERIOD', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const voterAlice = accounts[7]
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.transfer(voterAlice, tenThousandTorn.mul(toBN(2)), { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
await torn.approve(governance.address, tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterBob })
|
||||
await governance.lockWithApproval(tenThousandTorn.mul(toBN(2)), { from: voterAlice })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, false, { from: voterBob })
|
||||
await governance.castVote(id, true, { from: voterAlice })
|
||||
|
||||
let MAX_EXTENDED_TIME = await governance.VOTE_EXTEND_TIME()
|
||||
let proposal = await governance.proposals(id)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
proposal = await governance.proposals(id)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime.add(MAX_EXTENDED_TIME))
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(duration.hours(5))))
|
||||
|
||||
const stateAfter = await governance.state(id)
|
||||
stateAfter.should.be.eq.BN(ProposalState.Active)
|
||||
})
|
||||
it('locks tokens after vote', async () => {
|
||||
const voterAlice = accounts[7]
|
||||
await torn.transfer(voterAlice, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterAlice })
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterAlice })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
|
||||
const state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
|
||||
const lockBefore = await governance.canWithdrawAfter(voterAlice)
|
||||
lockBefore.should.be.eq.BN(toBN(0))
|
||||
|
||||
await governance.castVote(id, true, { from: voterAlice })
|
||||
|
||||
const lockAfter = await governance.canWithdrawAfter(voterAlice)
|
||||
lockAfter.should.be.eq.BN(proposalEndTime.add(executionDelay))
|
||||
})
|
||||
it('does not reduce lock time', async () => {
|
||||
const voterAlice = accounts[7]
|
||||
await torn.transfer(voterAlice, tenThousandTorn, { from: miningPublicKey })
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterAlice })
|
||||
await governance.lockWithApproval(tenThousandTorn, { from: voterAlice })
|
||||
await torn.approve(governance.address, balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
await governance.lockWithApproval(balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const id1 = await governance.latestProposalIds(proposer)
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.sub(votingDelay).sub(toBN(1)))
|
||||
|
||||
await governance.propose(dummy.address, 'dummy2', { from: secondProposer })
|
||||
const id2 = await governance.latestProposalIds(secondProposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
const state1 = await governance.state(id1)
|
||||
state1.should.be.eq.BN(ProposalState.Active)
|
||||
const state2 = await governance.state(id2)
|
||||
state2.should.be.eq.BN(ProposalState.Active)
|
||||
|
||||
const lockBefore = await governance.canWithdrawAfter(voterAlice)
|
||||
lockBefore.should.be.eq.BN(toBN(0))
|
||||
|
||||
await governance.castVote(id2, true, { from: voterAlice })
|
||||
|
||||
const lockAfter1 = await governance.canWithdrawAfter(voterAlice)
|
||||
|
||||
await governance.castVote(id1, true, { from: voterAlice })
|
||||
const lockAfter2 = await governance.canWithdrawAfter(voterAlice)
|
||||
lockAfter1.should.be.eq.BN(lockAfter2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#execute', () => {
|
||||
let proposal
|
||||
before(async () => {
|
||||
proposal = await Proposal.new()
|
||||
})
|
||||
it('should work', async () => {
|
||||
await governance.propose(proposal.address, 'proposal', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(1)))
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(delay).add(toBN(duration.days(1)))))
|
||||
|
||||
const receipt = await governance.execute(id)
|
||||
const debugLog = receipt.receipt.rawLogs[0]
|
||||
const decodedLog = web3.eth.abi.decodeLog(
|
||||
[
|
||||
{
|
||||
type: 'address',
|
||||
name: 'output',
|
||||
},
|
||||
],
|
||||
debugLog.data,
|
||||
debugLog.topics[0],
|
||||
)
|
||||
const newDummy = await Dummy.at(decodedLog.output)
|
||||
const dummyText = await newDummy.text()
|
||||
dummyText.should.be.equal('dummy')
|
||||
receipt.logs[0].event.should.be.equal('ProposalExecuted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#lock', () => {
|
||||
let owner = miningPublicKey
|
||||
let tokensAmount = toBN(10).pow(toBN(21)).mul(toBN(1337))
|
||||
it('permitClass works', async () => {
|
||||
const args = {
|
||||
owner,
|
||||
spender: governance.address,
|
||||
value: tokensAmount,
|
||||
nonce: '0x00',
|
||||
deadline: new BN('123123123123123'),
|
||||
}
|
||||
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
permitSigner.getPayload()
|
||||
|
||||
// Generate the signature in place
|
||||
const privateKey = '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c'
|
||||
const address = '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b'
|
||||
const signature = await permitSigner.getSignature(privateKey)
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
address.should.be.equal(signer)
|
||||
})
|
||||
|
||||
it('calls approve if signature is valid', async () => {
|
||||
const chainIdFromContract = await torn.chainId()
|
||||
chainIdFromContract.should.be.eq.BN(new BN(domain.chainId))
|
||||
const args = {
|
||||
owner,
|
||||
spender: governance.address,
|
||||
value: tokensAmount,
|
||||
nonce: 0,
|
||||
deadline: new BN('1609459200'), // 01/01/2021 @ 12:00am (UTC)
|
||||
}
|
||||
const permitSigner = new PermitSigner(domain, args)
|
||||
const signature = await permitSigner.getSignature(miningPrivateKey)
|
||||
const signer = await permitSigner.getSignerAddress(args, signature.hex)
|
||||
signer.should.be.equal(miningPublicKey)
|
||||
|
||||
const balanceBefore = await torn.balanceOf(governance.address)
|
||||
const lockedBalanceBefore = await governance.lockedBalance(owner)
|
||||
await governance.lock(
|
||||
args.owner,
|
||||
// args.spender,
|
||||
args.value.toString(),
|
||||
args.deadline.toString(),
|
||||
signature.v,
|
||||
signature.r,
|
||||
signature.s,
|
||||
{ from: owner },
|
||||
)
|
||||
const balanceAfter = await torn.balanceOf(governance.address)
|
||||
const lockedBalanceAfter = await governance.lockedBalance(owner)
|
||||
|
||||
balanceAfter.should.be.eq.BN(balanceBefore.add(args.value))
|
||||
lockedBalanceAfter.should.be.eq.BN(lockedBalanceBefore.add(args.value))
|
||||
})
|
||||
it('adds up tokens if already existing', async () => {
|
||||
const voterBob = accounts[5]
|
||||
const tenThousandTorn = toBN(10).pow(toBN(18)).mul(toBN(10000)) // todo
|
||||
await torn.transfer(voterBob, tenThousandTorn, { from: miningPublicKey })
|
||||
|
||||
await torn.approve(governance.address, tenThousandTorn, { from: voterBob })
|
||||
|
||||
await governance.lockWithApproval(tenThousandTorn.div(toBN(2)), { from: voterBob })
|
||||
await governance.lockWithApproval(tenThousandTorn.div(toBN(2)), { from: voterBob })
|
||||
|
||||
const balanceAfter = await torn.balanceOf(voterBob)
|
||||
const lockedBalanceAfter = await governance.lockedBalance(voterBob)
|
||||
balanceAfter.should.be.eq.BN(toBN(0))
|
||||
lockedBalanceAfter.should.be.eq.BN(tenThousandTorn)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#unlock', () => {
|
||||
it('should work if there is no activity made', async () => {
|
||||
const balanceBeforeTorn = await torn.balanceOf(proposer)
|
||||
const balanceBefore = await governance.lockedBalance(proposer)
|
||||
|
||||
await governance.unlock(balanceProposer, { from: proposer })
|
||||
const balanceAfterTorn = await torn.balanceOf(proposer)
|
||||
const balanceAfter = await governance.lockedBalance(proposer)
|
||||
balanceBefore.should.be.eq.BN(balanceAfter.add(balanceProposer))
|
||||
balanceAfterTorn.should.be.eq.BN(balanceBeforeTorn.add(balanceProposer))
|
||||
})
|
||||
it('fails if asking more than balance', async () => {
|
||||
await governance
|
||||
.unlock(balanceProposer + 1, { from: proposer })
|
||||
.should.be.rejectedWith('Governance: insufficient balance')
|
||||
//todo check lockedBalance
|
||||
})
|
||||
it('fail if there is active proposal', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance
|
||||
.unlock(balanceProposer, { from: proposer })
|
||||
.should.be.rejectedWith('Governance: tokens are locked')
|
||||
})
|
||||
it('unlock if there proposals expired', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(delay + duration.minutes(1))))
|
||||
await governance.unlock(balanceProposer, { from: proposer })
|
||||
})
|
||||
})
|
||||
|
||||
describe('#undelegate', () => {
|
||||
it('should work', async () => {
|
||||
let delegatee = accounts[5]
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
const { logs } = await governance.undelegate({ from: proposer })
|
||||
logs[0].args.account.should.be.equal(proposer)
|
||||
logs[0].args.from.should.be.equal(delegatee)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#delegate', () => {
|
||||
it('should work', async () => {
|
||||
let delegatee = accounts[5]
|
||||
let vp = await governance.delegatedTo(proposer)
|
||||
vp.should.be.equal('0x0000000000000000000000000000000000000000')
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
vp = await governance.delegatedTo(proposer)
|
||||
vp.should.be.equal(delegatee)
|
||||
})
|
||||
it('emits undelegate event if delegate called with non empty delegateTo', async () => {
|
||||
let delegatee = accounts[5]
|
||||
let delegateeSecond = accounts[6]
|
||||
const receipt = await governance.delegate(delegatee, { from: proposer })
|
||||
receipt.logs.length.should.be.equal(1)
|
||||
await governance
|
||||
.delegate(delegatee, { from: proposer })
|
||||
.should.be.rejectedWith('Governance: invalid delegatee')
|
||||
const receiptTwo = await governance.delegate(delegateeSecond, { from: proposer })
|
||||
receiptTwo.logs.length.should.be.equal(2)
|
||||
receiptTwo.logs[0].event.should.be.equal('Undelegated')
|
||||
receiptTwo.logs[0].args.account.should.be.equal(proposer)
|
||||
receiptTwo.logs[0].args.from.should.be.equal(delegatee)
|
||||
|
||||
receiptTwo.logs[1].event.should.be.equal('Delegated')
|
||||
receiptTwo.logs[1].args.account.should.be.equal(proposer)
|
||||
receiptTwo.logs[1].args.to.should.be.equal(delegateeSecond)
|
||||
const vp = await governance.delegatedTo(proposer)
|
||||
vp.should.be.equal(delegateeSecond)
|
||||
})
|
||||
it('can propose with delegated votes', async () => {
|
||||
let delegatee = accounts[5]
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
|
||||
await governance.proposeByDelegate(proposer, dummy.address, 'dummy', { from: delegatee })
|
||||
const proposalCount = await governance.proposalCount()
|
||||
proposalCount.should.be.eq.BN(1)
|
||||
const latestProposalId = await governance.latestProposalIds(proposer)
|
||||
latestProposalId.should.be.eq.BN(1)
|
||||
const proposal = await governance.proposals(1)
|
||||
proposal.proposer.should.be.equal(proposer)
|
||||
})
|
||||
it('can vote with delegated votes', async () => {
|
||||
let delegatee = accounts[5]
|
||||
await governance.delegate(delegatee, { from: proposer })
|
||||
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
const votesCount = balanceProposer
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
|
||||
await governance.castDelegatedVote([proposer], id, true, { from: delegatee })
|
||||
|
||||
await governance.getReceipt(id, proposer)
|
||||
let proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(votesCount)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
|
||||
await governance.castVote(id, false, { from: proposer })
|
||||
await governance.getReceipt(id, proposer)
|
||||
proposal = await governance.proposals(id)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(votesCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('#getAllProposals', () => {
|
||||
it('fetches proposals', async () => {
|
||||
await governance.propose(dummy.address, 'dummy', { from: proposer })
|
||||
await governance.setTimestamp(proposalEndTime)
|
||||
const proposals = await governance.getAllProposals(0, 0)
|
||||
const proposal = proposals[0]
|
||||
proposal.id.should.be.eq.BN(1)
|
||||
proposal.proposer.should.be.equal(proposer)
|
||||
proposal.startTime.should.be.eq.BN(proposalStartTime)
|
||||
proposal.endTime.should.be.eq.BN(proposalEndTime)
|
||||
proposal.forVotes.should.be.eq.BN(0)
|
||||
proposal.againstVotes.should.be.eq.BN(0)
|
||||
proposal.executed.should.be.equal(false)
|
||||
proposal.state.should.be.eq.BN(ProposalState.Active)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('#getBalances', () => {
|
||||
it('fetches lockedBalance', async () => {
|
||||
const lockedBalanceOne = await governance.getBalances([proposer, secondProposer])
|
||||
lockedBalanceOne.should.be.eq.BN([balanceProposer, toBN('0')])
|
||||
await torn.approve(governance.address, balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
await governance.lockWithApproval(balanceProposer.div(toBN(2)), { from: secondProposer })
|
||||
|
||||
const lockedBalance = await governance.getBalances([proposer, secondProposer])
|
||||
lockedBalance.should.be.eq.BN([balanceProposer, balanceProposer.div(toBN(2))])
|
||||
})
|
||||
})
|
||||
|
||||
describe('#upgrades', () => {
|
||||
it('allows to change variable state', async () => {
|
||||
const proposal = await ProposalStateChangeGovernance.new()
|
||||
await governance.propose(proposal.address, 'proposal', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(1)))
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(delay).add(toBN(duration.days(1)))))
|
||||
|
||||
const EXECUTION_DELAY_BEFORE = await governance.EXECUTION_DELAY()
|
||||
EXECUTION_DELAY_BEFORE.should.be.eq.BN(delay)
|
||||
const receipt = await governance.execute(id)
|
||||
const EXECUTION_DELAY_AFTER = await governance.EXECUTION_DELAY()
|
||||
EXECUTION_DELAY_AFTER.should.be.eq.BN(duration.days(3))
|
||||
receipt.logs[0].event.should.be.equal('ProposalExecuted')
|
||||
})
|
||||
it('upgrades implementation with variables change', async () => {
|
||||
await NewImplementation.new({ from: accounts[9] })
|
||||
const proposal = await ProposalUpgrade.new()
|
||||
// console.log(newImpl.address) // 0xF7E3e47e06F1bDDecb1b2F3a7F60b6b25fd2e233
|
||||
|
||||
await governance.propose(proposal.address, 'proposal', { from: proposer })
|
||||
const id = await governance.latestProposalIds(proposer)
|
||||
await governance.setTimestamp(proposalStartTime.add(toBN(1)))
|
||||
let state = await governance.state(id)
|
||||
state.should.be.eq.BN(ProposalState.Active)
|
||||
await governance.castVote(id, true, { from: proposer })
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(1)))
|
||||
|
||||
await governance.setTimestamp(proposalEndTime.add(toBN(delay).add(toBN(duration.days(1)))))
|
||||
|
||||
const newGovernance = await NewImplementation.at(governance.address)
|
||||
const receipt = await governance.execute(id)
|
||||
let newVariable = await newGovernance.newVariable()
|
||||
newVariable.should.be.eq.BN(0)
|
||||
const receiptExecute = await newGovernance.execute(123)
|
||||
newVariable = await newGovernance.newVariable()
|
||||
newVariable.should.be.eq.BN(999)
|
||||
receipt.logs[0].event.should.be.equal('ProposalExecuted')
|
||||
receiptExecute.logs[0].event.should.be.equal('Overriden')
|
||||
})
|
||||
it('cannot initialize implementation contract', async () => {
|
||||
const impl = await NewImplementation.new({ from: accounts[9] })
|
||||
await impl
|
||||
.initialize(accounts[9])
|
||||
.should.be.rejectedWith('Contract instance has already been initialized')
|
||||
})
|
||||
it('cannot destroy implementation contract')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await revertSnapshot(snapshotId.result)
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
snapshotId = await takeSnapshot()
|
||||
})
|
||||
})
|
58
truffle-config.js
Normal file
58
truffle-config.js
Normal file
@ -0,0 +1,58 @@
|
||||
require('dotenv').config()
|
||||
const HDWalletProvider = require('truffle-hdwallet-provider')
|
||||
const utils = require('web3-utils')
|
||||
const { PRIVATE_KEY, INFURA_TOKEN } = process.env
|
||||
|
||||
module.exports = {
|
||||
networks: {
|
||||
// development: {
|
||||
// // host: '127.0.0.1', // Localhost (default: none)
|
||||
// // port: 8545, // Standard Ethereum port (default: none)
|
||||
// network_id: '*', // Any network (default: none)
|
||||
// accounts: 20,
|
||||
// },
|
||||
mainnet: {
|
||||
provider: () => new HDWalletProvider(PRIVATE_KEY, `https://mainnet.infura.io/v3/${INFURA_TOKEN}`),
|
||||
network_id: 1,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('100', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
kovan: {
|
||||
provider: () => new HDWalletProvider(PRIVATE_KEY, `https://kovan.infura.io/v3/${INFURA_TOKEN}`),
|
||||
network_id: 42,
|
||||
gas: 6000000,
|
||||
gasPrice: utils.toWei('1', 'gwei'),
|
||||
// confirmations: 0,
|
||||
// timeoutBlocks: 200,
|
||||
skipDryRun: true,
|
||||
},
|
||||
coverage: {
|
||||
host: 'localhost',
|
||||
network_id: '*',
|
||||
port: 8554, // <-- If you change this, also set the port option in .solcover.js.
|
||||
gas: 0xfffffffffff, // <-- Use this high gas value
|
||||
gasPrice: 0x01, // <-- Use this low gas price
|
||||
},
|
||||
},
|
||||
|
||||
mocha: {
|
||||
// timeout: 100000
|
||||
},
|
||||
|
||||
compilers: {
|
||||
solc: {
|
||||
version: '0.6.12',
|
||||
docker: false,
|
||||
settings: {
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 200,
|
||||
},
|
||||
// evmVersion: "byzantium"
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user