This commit is contained in:
poma 2020-12-15 18:07:50 +03:00
commit 71fa601fd3
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657
30 changed files with 8185 additions and 0 deletions

10
.editorconfig Normal file
View 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
View File

@ -0,0 +1,3 @@
PRIVATE_KEY=
INFURA_TOKEN=
TORN_ADDRESS=

26
.eslintrc Normal file
View 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
View File

@ -0,0 +1 @@
*.sol linguist-language=Solidity

28
.github/workflows/build.yml vendored Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
.vscode
build
scripts

16
.prettierrc Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,13 @@
# Tornado.Cash Governance [![Build Status](https://github.com/tornadocash/governance/workflows/build/badge.svg)](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.

View 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
View 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
View 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
View 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;
}
}

View 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
View 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);
// }
}

View 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)));
}
}

View 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)));
}
}

View 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));
}
}

View 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);
}
}

View 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);
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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"
},
},
},
}

6447
yarn.lock Normal file

File diff suppressed because it is too large Load Diff