forked from Theo/proposal-22-governance-and-rewards-patch
Init
This commit is contained in:
commit
afd40e28ac
34
.github/workflows/test.yml
vendored
Normal file
34
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
env:
|
||||||
|
FOUNDRY_PROFILE: ci
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
|
||||||
|
name: Foundry project
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Foundry
|
||||||
|
uses: foundry-rs/foundry-toolchain@v1
|
||||||
|
with:
|
||||||
|
version: nightly
|
||||||
|
|
||||||
|
- name: Run Forge build
|
||||||
|
run: |
|
||||||
|
forge --version
|
||||||
|
forge build --sizes
|
||||||
|
id: build
|
||||||
|
|
||||||
|
- name: Run Forge tests
|
||||||
|
run: |
|
||||||
|
forge test -vvv
|
||||||
|
id: test
|
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Compiler files
|
||||||
|
cache/
|
||||||
|
out/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Ignores development broadcast logs
|
||||||
|
!/broadcast
|
||||||
|
/broadcast/*/31337/
|
||||||
|
/broadcast/**/dry-run/
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Dotenv file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn-error*
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "lib/forge-std"]
|
||||||
|
path = lib/forge-std
|
||||||
|
url = https://github.com/foundry-rs/forge-std
|
15
.solhint.json
Normal file
15
.solhint.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "solhint:recommended",
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"printWidth": 110
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quotes": ["error", "double"],
|
||||||
|
"indent": ["error", 2],
|
||||||
|
"compiler-version": ["error", "^0.6.0"]
|
||||||
|
},
|
||||||
|
"plugins": ["prettier"]
|
||||||
|
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"solidity.compileUsingRemoteVersion": "v0.6.12+commit.27d51765"
|
||||||
|
}
|
BIN
.yarn/install-state.gz
Normal file
BIN
.yarn/install-state.gz
Normal file
Binary file not shown.
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
nodeLinker: node-modules
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# proposal-22-forge-tests
|
||||||
|
|
||||||
|
Please do not format the contracts. Besides [this repository](https://git.tornado.ws/AlienTornadosaurusHex/tornado-governance), this is another test suite.
|
8
foundry.toml
Normal file
8
foundry.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[profile.default]
|
||||||
|
solc-version = "0.6.12"
|
||||||
|
src = 'src'
|
||||||
|
out = 'out'
|
||||||
|
libs = ["node_modules", "lib"]
|
||||||
|
chain_id = 1
|
||||||
|
optimizer = true
|
||||||
|
optimizer-runs = 10_000_000
|
1
lib/forge-std
Submodule
1
lib/forge-std
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 73d44ec7d124e3831bc5f832267889ffb6f9bc3f
|
14
package.json
Normal file
14
package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "proposal-22-forge-tests",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"repository": "https://git.tornado.ws/AlienTornadosaurusHex/proposal-22-forge-tests.git",
|
||||||
|
"author": "AlienTornadosaurusHex",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@gnosis.pm/ido-contracts": "^0.5.0",
|
||||||
|
"@openzeppelin/contracts": "^3.2.0-rc.0",
|
||||||
|
"@openzeppelin/upgrades-core": "^1.0.1",
|
||||||
|
"torn-token": "^1.0.4",
|
||||||
|
"tornado-governance": "^1.0.3"
|
||||||
|
}
|
||||||
|
}
|
10
remappings.txt
Normal file
10
remappings.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@proprietary/=src/proprietary/
|
||||||
|
@interfaces/=src/interfaces/
|
||||||
|
@root/=src/
|
||||||
|
@forge-std/=lib/forge-std/src/
|
||||||
|
|
||||||
|
@gnosis.pm/ido-contracts/=node_modules/@gnosis.pm/ido-contracts/
|
||||||
|
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts
|
||||||
|
@openzeppelin/upgrades-core/=node_modules/@openzeppelin/upgrades-core
|
||||||
|
torn-token/=node_modules/torn-token/
|
||||||
|
tornado-governance=node_modules/tornado-governance/
|
17
src/interfaces/IGovernance.sol
Normal file
17
src/interfaces/IGovernance.sol
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
pragma solidity ^0.6.12;
|
||||||
|
|
||||||
|
interface IGovernance {
|
||||||
|
function lockedBalance(address account) external view returns (uint256);
|
||||||
|
|
||||||
|
function propose(address target, string memory description) external returns (uint256);
|
||||||
|
|
||||||
|
function castVote(uint256 proposalId, bool support) external;
|
||||||
|
|
||||||
|
function lock(address owner, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;
|
||||||
|
|
||||||
|
function lockWithApproval(uint256 amount) external;
|
||||||
|
|
||||||
|
function unlock(uint256 amount) external;
|
||||||
|
|
||||||
|
function execute(uint256 proposalId) external payable;
|
||||||
|
}
|
10
src/proprietary/Parameters.sol
Normal file
10
src/proprietary/Parameters.sol
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity 0.6.12;
|
||||||
|
|
||||||
|
contract Parameters {
|
||||||
|
// Beneficary addresses
|
||||||
|
address constant _governanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
|
||||||
|
address constant _governanceVaultAddress = 0x2F50508a8a3D323B91336FA3eA6ae50E55f32185;
|
||||||
|
address constant _tokenAddress = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C;
|
||||||
|
}
|
73
src/v1/Configuration.sol
Normal file
73
src/v1/Configuration.sol
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
contract Configuration {
|
||||||
|
/// @notice Time delay between proposal vote completion and its execution
|
||||||
|
uint256 public EXECUTION_DELAY;
|
||||||
|
/// @notice Time before a passed proposal is considered expired
|
||||||
|
uint256 public EXECUTION_EXPIRATION;
|
||||||
|
/// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed
|
||||||
|
uint256 public QUORUM_VOTES;
|
||||||
|
/// @notice The number of votes required in order for a voter to become a proposer
|
||||||
|
uint256 public PROPOSAL_THRESHOLD;
|
||||||
|
/// @notice The delay before voting on a proposal may take place, once proposed
|
||||||
|
/// It is needed to prevent reorg attacks that replace the proposal
|
||||||
|
uint256 public VOTING_DELAY;
|
||||||
|
/// @notice The duration of voting on a proposal
|
||||||
|
uint256 public VOTING_PERIOD;
|
||||||
|
/// @notice If the outcome of a proposal changes during CLOSING_PERIOD, the vote will be extended by VOTE_EXTEND_TIME (no more than once)
|
||||||
|
uint256 public CLOSING_PERIOD;
|
||||||
|
/// @notice If the outcome of a proposal changes during CLOSING_PERIOD, the vote will be extended by VOTE_EXTEND_TIME (no more than once)
|
||||||
|
uint256 public VOTE_EXTEND_TIME;
|
||||||
|
|
||||||
|
modifier onlySelf() {
|
||||||
|
require(msg.sender == address(this), "Governance: unauthorized");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initializeConfiguration() internal {
|
||||||
|
EXECUTION_DELAY = 2 days;
|
||||||
|
EXECUTION_EXPIRATION = 3 days;
|
||||||
|
QUORUM_VOTES = 25000e18; // 0.25% of TORN
|
||||||
|
PROPOSAL_THRESHOLD = 1000e18; // 0.01% of TORN
|
||||||
|
VOTING_DELAY = 75 seconds;
|
||||||
|
VOTING_PERIOD = 3 days;
|
||||||
|
CLOSING_PERIOD = 1 hours;
|
||||||
|
VOTE_EXTEND_TIME = 6 hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setExecutionDelay(uint256 executionDelay) external onlySelf {
|
||||||
|
EXECUTION_DELAY = executionDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setExecutionExpiration(uint256 executionExpiration) external onlySelf {
|
||||||
|
EXECUTION_EXPIRATION = executionExpiration;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQuorumVotes(uint256 quorumVotes) external onlySelf {
|
||||||
|
QUORUM_VOTES = quorumVotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProposalThreshold(uint256 proposalThreshold) external onlySelf {
|
||||||
|
PROPOSAL_THRESHOLD = proposalThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVotingDelay(uint256 votingDelay) external onlySelf {
|
||||||
|
VOTING_DELAY = votingDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVotingPeriod(uint256 votingPeriod) external onlySelf {
|
||||||
|
VOTING_PERIOD = votingPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setClosingPeriod(uint256 closingPeriod) external onlySelf {
|
||||||
|
CLOSING_PERIOD = closingPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVoteExtendTime(uint256 voteExtendTime) external onlySelf {
|
||||||
|
// VOTE_EXTEND_TIME should be less EXECUTION_DELAY to prevent double voting
|
||||||
|
require(voteExtendTime < EXECUTION_DELAY, "Governance: incorrect voteExtendTime");
|
||||||
|
VOTE_EXTEND_TIME = voteExtendTime;
|
||||||
|
}
|
||||||
|
}
|
9
src/v1/Core.sol
Normal file
9
src/v1/Core.sol
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
abstract contract Core {
|
||||||
|
/// @notice Locked token balance for each account
|
||||||
|
mapping(address => uint256) public lockedBalance;
|
||||||
|
}
|
66
src/v1/Delegation.sol
Normal file
66
src/v1/Delegation.sol
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "./Core.sol";
|
||||||
|
|
||||||
|
abstract contract Delegation is Core {
|
||||||
|
/// @notice Delegatee records
|
||||||
|
mapping(address => address) public delegatedTo;
|
||||||
|
|
||||||
|
event Delegated(address indexed account, address indexed to);
|
||||||
|
event Undelegated(address indexed account, address indexed from);
|
||||||
|
|
||||||
|
function delegate(address to) external {
|
||||||
|
address previous = delegatedTo[msg.sender];
|
||||||
|
require(to != msg.sender && to != address(this) && to != address(0) && to != previous, "Governance: invalid delegatee");
|
||||||
|
if (previous != address(0)) {
|
||||||
|
emit Undelegated(msg.sender, previous);
|
||||||
|
}
|
||||||
|
delegatedTo[msg.sender] = to;
|
||||||
|
emit Delegated(msg.sender, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
function undelegate() external {
|
||||||
|
address previous = delegatedTo[msg.sender];
|
||||||
|
require(previous != address(0), "Governance: tokens are already undelegated");
|
||||||
|
|
||||||
|
delegatedTo[msg.sender] = address(0);
|
||||||
|
emit Undelegated(msg.sender, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposeByDelegate(
|
||||||
|
address from,
|
||||||
|
address target,
|
||||||
|
string memory description
|
||||||
|
) external returns (uint256) {
|
||||||
|
require(delegatedTo[from] == msg.sender, "Governance: not authorized");
|
||||||
|
return _propose(from, target, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _propose(
|
||||||
|
address proposer,
|
||||||
|
address target,
|
||||||
|
string memory description
|
||||||
|
) internal virtual returns (uint256);
|
||||||
|
|
||||||
|
function castDelegatedVote(
|
||||||
|
address[] memory from,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support
|
||||||
|
) external virtual {
|
||||||
|
for (uint256 i = 0; i < from.length; i++) {
|
||||||
|
require(delegatedTo[from[i]] == msg.sender, "Governance: not authorized");
|
||||||
|
_castVote(from[i], proposalId, support);
|
||||||
|
}
|
||||||
|
if (lockedBalance[msg.sender] > 0) {
|
||||||
|
_castVote(msg.sender, proposalId, support);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _castVote(
|
||||||
|
address voter,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support
|
||||||
|
) internal virtual;
|
||||||
|
}
|
290
src/v1/Governance.sol
Normal file
290
src/v1/Governance.sol
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/math/SafeMath.sol";
|
||||||
|
import "@openzeppelin/upgrades-core/contracts/Initializable.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/Address.sol";
|
||||||
|
import "torn-token/contracts/ENS.sol";
|
||||||
|
import "torn-token/contracts/TORN.sol";
|
||||||
|
import "./Delegation.sol";
|
||||||
|
import "./Configuration.sol";
|
||||||
|
|
||||||
|
contract Governance is Initializable, Configuration, Delegation, EnsResolve {
|
||||||
|
using SafeMath for uint256;
|
||||||
|
/// @notice Possible states that a proposal may be in
|
||||||
|
enum ProposalState {
|
||||||
|
Pending,
|
||||||
|
Active,
|
||||||
|
Defeated,
|
||||||
|
Timelocked,
|
||||||
|
AwaitingExecution,
|
||||||
|
Executed,
|
||||||
|
Expired
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Proposal {
|
||||||
|
// Creator of the proposal
|
||||||
|
address proposer;
|
||||||
|
// target addresses for the call to be made
|
||||||
|
address target;
|
||||||
|
// The block at which voting begins
|
||||||
|
uint256 startTime;
|
||||||
|
// The block at which voting ends: votes must be cast prior to this block
|
||||||
|
uint256 endTime;
|
||||||
|
// Current number of votes in favor of this proposal
|
||||||
|
uint256 forVotes;
|
||||||
|
// Current number of votes in opposition to this proposal
|
||||||
|
uint256 againstVotes;
|
||||||
|
// Flag marking whether the proposal has been executed
|
||||||
|
bool executed;
|
||||||
|
// Flag marking whether the proposal voting time has been extended
|
||||||
|
// Voting time can be extended once, if the proposal outcome has changed during CLOSING_PERIOD
|
||||||
|
bool extended;
|
||||||
|
// Receipts of ballots for the entire set of voters
|
||||||
|
mapping(address => Receipt) receipts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Ballot receipt record for a voter
|
||||||
|
struct Receipt {
|
||||||
|
// Whether or not a vote has been cast
|
||||||
|
bool hasVoted;
|
||||||
|
// Whether or not the voter supports the proposal
|
||||||
|
bool support;
|
||||||
|
// The number of votes the voter had, which were cast
|
||||||
|
uint256 votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice The official record of all proposals ever proposed
|
||||||
|
Proposal[] public proposals;
|
||||||
|
/// @notice The latest proposal for each proposer
|
||||||
|
mapping(address => uint256) public latestProposalIds;
|
||||||
|
/// @notice Timestamp when a user can withdraw tokens
|
||||||
|
mapping(address => uint256) public canWithdrawAfter;
|
||||||
|
|
||||||
|
TORN public torn;
|
||||||
|
|
||||||
|
/// @notice An event emitted when a new proposal is created
|
||||||
|
event ProposalCreated(
|
||||||
|
uint256 indexed id,
|
||||||
|
address indexed proposer,
|
||||||
|
address target,
|
||||||
|
uint256 startTime,
|
||||||
|
uint256 endTime,
|
||||||
|
string description
|
||||||
|
);
|
||||||
|
|
||||||
|
/// @notice An event emitted when a vote has been cast on a proposal
|
||||||
|
event Voted(uint256 indexed proposalId, address indexed voter, bool indexed support, uint256 votes);
|
||||||
|
|
||||||
|
/// @notice An event emitted when a proposal has been executed
|
||||||
|
event ProposalExecuted(uint256 indexed proposalId);
|
||||||
|
|
||||||
|
/// @notice Makes this instance inoperable to prevent selfdestruct attack
|
||||||
|
/// Proxy will still be able to properly initialize its storage
|
||||||
|
constructor() public initializer {
|
||||||
|
torn = TORN(0x000000000000000000000000000000000000dEaD);
|
||||||
|
_initializeConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize(bytes32 _torn) public initializer {
|
||||||
|
torn = TORN(resolve(_torn));
|
||||||
|
// Create a dummy proposal so that indexes start from 1
|
||||||
|
proposals.push(
|
||||||
|
Proposal({
|
||||||
|
proposer: address(this),
|
||||||
|
target: 0x000000000000000000000000000000000000dEaD,
|
||||||
|
startTime: 0,
|
||||||
|
endTime: 0,
|
||||||
|
forVotes: 0,
|
||||||
|
againstVotes: 0,
|
||||||
|
executed: true,
|
||||||
|
extended: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
_initializeConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock(
|
||||||
|
address owner,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) public virtual {
|
||||||
|
torn.permit(owner, address(this), amount, deadline, v, r, s);
|
||||||
|
_transferTokens(owner, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockWithApproval(uint256 amount) public virtual {
|
||||||
|
_transferTokens(msg.sender, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlock(uint256 amount) public virtual {
|
||||||
|
require(getBlockTimestamp() > canWithdrawAfter[msg.sender], "Governance: tokens are locked");
|
||||||
|
lockedBalance[msg.sender] = lockedBalance[msg.sender].sub(amount, "Governance: insufficient balance");
|
||||||
|
require(torn.transfer(msg.sender, amount), "TORN: transfer failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function propose(address target, string memory description) external returns (uint256) {
|
||||||
|
return _propose(msg.sender, target, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Propose implementation
|
||||||
|
* @param proposer proposer address
|
||||||
|
* @param target smart contact address that will be executed as result of voting
|
||||||
|
* @param description description of the proposal
|
||||||
|
* @return the new proposal id
|
||||||
|
*/
|
||||||
|
function _propose(
|
||||||
|
address proposer,
|
||||||
|
address target,
|
||||||
|
string memory description
|
||||||
|
) internal virtual 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) public payable virtual {
|
||||||
|
require(state(proposalId) == ProposalState.AwaitingExecution, "Governance::execute: invalid proposal state");
|
||||||
|
Proposal storage proposal = proposals[proposalId];
|
||||||
|
proposal.executed = true;
|
||||||
|
|
||||||
|
address target = proposal.target;
|
||||||
|
require(Address.isContract(target), "Governance::execute: not a contract");
|
||||||
|
(bool success, bytes memory data) = target.delegatecall(abi.encodeWithSignature("executeProposal()"));
|
||||||
|
if (!success) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
revert(string(data));
|
||||||
|
} else {
|
||||||
|
revert("Proposal execution failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit ProposalExecuted(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function castVote(uint256 proposalId, bool support) external virtual {
|
||||||
|
_castVote(msg.sender, proposalId, support);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _castVote(
|
||||||
|
address voter,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support
|
||||||
|
) internal override(Delegation) {
|
||||||
|
require(state(proposalId) == ProposalState.Active, "Governance::_castVote: voting is closed");
|
||||||
|
Proposal storage proposal = proposals[proposalId];
|
||||||
|
Receipt storage receipt = proposal.receipts[voter];
|
||||||
|
bool beforeVotingState = proposal.forVotes <= proposal.againstVotes;
|
||||||
|
uint256 votes = lockedBalance[voter];
|
||||||
|
require(votes > 0, "Governance: balance is 0");
|
||||||
|
if (receipt.hasVoted) {
|
||||||
|
if (receipt.support) {
|
||||||
|
proposal.forVotes = proposal.forVotes.sub(receipt.votes);
|
||||||
|
} else {
|
||||||
|
proposal.againstVotes = proposal.againstVotes.sub(receipt.votes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (support) {
|
||||||
|
proposal.forVotes = proposal.forVotes.add(votes);
|
||||||
|
} else {
|
||||||
|
proposal.againstVotes = proposal.againstVotes.add(votes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposal.extended && proposal.endTime.sub(getBlockTimestamp()) < CLOSING_PERIOD) {
|
||||||
|
bool afterVotingState = proposal.forVotes <= proposal.againstVotes;
|
||||||
|
if (beforeVotingState != afterVotingState) {
|
||||||
|
proposal.extended = true;
|
||||||
|
proposal.endTime = proposal.endTime.add(VOTE_EXTEND_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
receipt.hasVoted = true;
|
||||||
|
receipt.support = support;
|
||||||
|
receipt.votes = votes;
|
||||||
|
_lockTokens(voter, proposal.endTime.add(VOTE_EXTEND_TIME).add(EXECUTION_EXPIRATION).add(EXECUTION_DELAY));
|
||||||
|
emit Voted(proposalId, voter, support, votes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _lockTokens(address owner, uint256 timestamp) internal {
|
||||||
|
if (timestamp > canWithdrawAfter[owner]) {
|
||||||
|
canWithdrawAfter[owner] = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _transferTokens(address owner, uint256 amount) internal virtual {
|
||||||
|
require(torn.transferFrom(owner, address(this), amount), "TORN: transferFrom failed");
|
||||||
|
lockedBalance[owner] = lockedBalance[owner].add(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReceipt(uint256 proposalId, address voter) public view returns (Receipt memory) {
|
||||||
|
return proposals[proposalId].receipts[voter];
|
||||||
|
}
|
||||||
|
|
||||||
|
function state(uint256 proposalId) public view returns (ProposalState) {
|
||||||
|
require(proposalId <= proposalCount() && proposalId > 0, "Governance::state: invalid proposal id");
|
||||||
|
Proposal storage proposal = proposals[proposalId];
|
||||||
|
if (getBlockTimestamp() <= proposal.startTime) {
|
||||||
|
return ProposalState.Pending;
|
||||||
|
} else if (getBlockTimestamp() <= proposal.endTime) {
|
||||||
|
return ProposalState.Active;
|
||||||
|
} else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes + proposal.againstVotes < QUORUM_VOTES) {
|
||||||
|
return ProposalState.Defeated;
|
||||||
|
} else if (proposal.executed) {
|
||||||
|
return ProposalState.Executed;
|
||||||
|
} else if (getBlockTimestamp() >= proposal.endTime.add(EXECUTION_DELAY).add(EXECUTION_EXPIRATION)) {
|
||||||
|
return ProposalState.Expired;
|
||||||
|
} else if (getBlockTimestamp() >= proposal.endTime.add(EXECUTION_DELAY)) {
|
||||||
|
return ProposalState.AwaitingExecution;
|
||||||
|
} else {
|
||||||
|
return ProposalState.Timelocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function proposalCount() public view returns (uint256) {
|
||||||
|
return proposals.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlockTimestamp() internal view virtual returns (uint256) {
|
||||||
|
// solium-disable-next-line security/no-block-members
|
||||||
|
return block.timestamp;
|
||||||
|
}
|
||||||
|
}
|
22
src/v1/LoopbackProxy.sol
Normal file
22
src/v1/LoopbackProxy.sol
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/proxy/TransparentUpgradeableProxy.sol";
|
||||||
|
import "torn-token/contracts/ENS.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev TransparentUpgradeableProxy that sets its admin to the implementation itself.
|
||||||
|
* It is also allowed to call implementation methods.
|
||||||
|
*/
|
||||||
|
contract LoopbackProxy is TransparentUpgradeableProxy, EnsResolve {
|
||||||
|
/**
|
||||||
|
* @dev Initializes an upgradeable proxy backed by the implementation at `_logic`.
|
||||||
|
*/
|
||||||
|
constructor(address _logic, bytes memory _data) public payable TransparentUpgradeableProxy(_logic, address(this), _data) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Override to allow admin (itself) access the fallback function.
|
||||||
|
*/
|
||||||
|
function _beforeFallback() internal override {}
|
||||||
|
}
|
32
src/v1/Mocks/Dummy.sol
Normal file
32
src/v1/Mocks/Dummy.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
contract Dummy {
|
||||||
|
uint256 public value;
|
||||||
|
string public text;
|
||||||
|
|
||||||
|
function initialize() public {
|
||||||
|
value = 1;
|
||||||
|
text = "dummy";
|
||||||
|
}
|
||||||
|
|
||||||
|
// function update(address _impl) public {
|
||||||
|
// MyProxy(address(uint160(address(this)))).upgradeTo(_impl);
|
||||||
|
// // MyProxy(address(this)).upgradeTo(_impl);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
contract DummySecond {
|
||||||
|
uint256 public value;
|
||||||
|
string public text;
|
||||||
|
|
||||||
|
function initialize() public {
|
||||||
|
value = 2;
|
||||||
|
text = "dummy2";
|
||||||
|
}
|
||||||
|
|
||||||
|
// function update(address _impl) public {
|
||||||
|
// MyProxy(address(uint160(address(this)))).upgradeTo(_impl);
|
||||||
|
// }
|
||||||
|
}
|
27
src/v1/Mocks/MockGovernance.sol
Normal file
27
src/v1/Mocks/MockGovernance.sol
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "../Governance.sol";
|
||||||
|
|
||||||
|
contract MockGovernance is Governance {
|
||||||
|
uint256 public time = block.timestamp;
|
||||||
|
|
||||||
|
function setTimestamp(uint256 time_) public {
|
||||||
|
time = time_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlockTimestamp() internal view override returns (uint256) {
|
||||||
|
// solium-disable-next-line security/no-block-members
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTorn(address torna) external {
|
||||||
|
torn = TORN(torna);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(bytes32 addr) public view override returns (address) {
|
||||||
|
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||||
|
}
|
||||||
|
}
|
13
src/v1/Mocks/MockProxy.sol
Normal file
13
src/v1/Mocks/MockProxy.sol
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "../LoopbackProxy.sol";
|
||||||
|
|
||||||
|
contract MockProxy is LoopbackProxy {
|
||||||
|
constructor(address _logic, bytes memory _data) public payable LoopbackProxy(_logic, _data) {}
|
||||||
|
|
||||||
|
function resolve(bytes32 addr) public view override returns (address) {
|
||||||
|
return address(uint160(uint256(addr) >> (12 * 8)));
|
||||||
|
}
|
||||||
|
}
|
19
src/v1/Mocks/Proposal.sol
Normal file
19
src/v1/Mocks/Proposal.sol
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
import "./Dummy.sol";
|
||||||
|
|
||||||
|
contract Proposal {
|
||||||
|
// bytes32 public constant WEIRD = keccak256("Hey Proposal");
|
||||||
|
// uint256 public someValue = 111;
|
||||||
|
// Dummy public dummyInstance;
|
||||||
|
event Debug(address output);
|
||||||
|
|
||||||
|
function executeProposal() public {
|
||||||
|
// someValue = 321;
|
||||||
|
Dummy dummyInstance = new Dummy();
|
||||||
|
dummyInstance.initialize();
|
||||||
|
emit Debug(address(dummyInstance));
|
||||||
|
}
|
||||||
|
}
|
13
src/v1/Mocks/ProposalStateChangeGovernance.sol
Normal file
13
src/v1/Mocks/ProposalStateChangeGovernance.sol
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
|
||||||
|
interface IGovernance {
|
||||||
|
function setExecutionDelay(uint256 delay) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
contract ProposalStateChangeGovernance {
|
||||||
|
function executeProposal() public {
|
||||||
|
IGovernance(address(this)).setExecutionDelay(3 days);
|
||||||
|
}
|
||||||
|
}
|
32
src/v1/Mocks/ProposalUpgrade.sol
Normal file
32
src/v1/Mocks/ProposalUpgrade.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "./MockGovernance.sol";
|
||||||
|
|
||||||
|
interface IProxy {
|
||||||
|
function upgradeTo(address newImplementation) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
contract NewImplementation is MockGovernance {
|
||||||
|
uint256 public newVariable;
|
||||||
|
event Overriden(uint256 x);
|
||||||
|
|
||||||
|
function execute(uint256 proposalId) public payable override {
|
||||||
|
newVariable = 999;
|
||||||
|
emit Overriden(proposalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contract ProposalUpgrade {
|
||||||
|
address public immutable newLogic;
|
||||||
|
|
||||||
|
constructor(address _newLogic) public {
|
||||||
|
newLogic = _newLogic;
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeProposal() public {
|
||||||
|
IProxy(address(this)).upgradeTo(newLogic);
|
||||||
|
}
|
||||||
|
}
|
32
src/v1/Mocks/TORNMock.sol
Normal file
32
src/v1/Mocks/TORNMock.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "torn-token/contracts/mocks/TORNMock.sol";
|
||||||
|
|
||||||
|
struct Recipient2 {
|
||||||
|
address to;
|
||||||
|
uint256 amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
contract TORNMock2 is TORNMock {
|
||||||
|
constructor(
|
||||||
|
address _governance,
|
||||||
|
uint256 _pausePeriod,
|
||||||
|
Recipient2[] memory vesting
|
||||||
|
) public TORNMock(solve(_governance), _pausePeriod, solve2(vesting)) {}
|
||||||
|
|
||||||
|
function solve(address x) private returns (bytes32) {
|
||||||
|
return bytes32(uint256(x) << 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
function solve2(Recipient2[] memory vesting) private returns (Recipient[] memory) {
|
||||||
|
Recipient[] memory realVesting = new Recipient[](vesting.length);
|
||||||
|
for (uint256 i = 0; i < vesting.length; i++) {
|
||||||
|
realVesting[i].to = solve(vesting[i].to);
|
||||||
|
realVesting[i].amount = vesting[i].amount;
|
||||||
|
}
|
||||||
|
return realVesting;
|
||||||
|
}
|
||||||
|
}
|
58
src/v2-vault-and-gas/GasCompensator.sol
Normal file
58
src/v2-vault-and-gas/GasCompensator.sol
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
|
||||||
|
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
|
||||||
|
|
||||||
|
interface IGasCompensationVault {
|
||||||
|
function compensateGas(address recipient, uint256 gasAmount) external;
|
||||||
|
|
||||||
|
function withdrawToGovernance(uint256 amount) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This abstract contract is used to add gas compensation functionality to a contract.
|
||||||
|
* */
|
||||||
|
abstract contract GasCompensator {
|
||||||
|
using SafeMath for uint256;
|
||||||
|
|
||||||
|
/// @notice this vault is necessary for the gas compensation functionality to work
|
||||||
|
IGasCompensationVault public immutable gasCompensationVault;
|
||||||
|
|
||||||
|
constructor(address _gasCompensationVault) public {
|
||||||
|
gasCompensationVault = IGasCompensationVault(_gasCompensationVault);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice modifier which should compensate gas to account if eligible
|
||||||
|
* @dev Consider reentrancy, repeated calling of the function being compensated, eligibility.
|
||||||
|
* @param account address to be compensated
|
||||||
|
* @param eligible if the account is eligible for compensations or not
|
||||||
|
* @param extra extra amount in gas to be compensated, will be multiplied by basefee
|
||||||
|
* */
|
||||||
|
modifier gasCompensation(
|
||||||
|
address account,
|
||||||
|
bool eligible,
|
||||||
|
uint256 extra
|
||||||
|
) {
|
||||||
|
if (eligible) {
|
||||||
|
uint256 startGas = gasleft();
|
||||||
|
_;
|
||||||
|
uint256 gasToCompensate = startGas.sub(gasleft()).add(extra).add(10e3);
|
||||||
|
|
||||||
|
gasCompensationVault.compensateGas(account, gasToCompensate);
|
||||||
|
} else {
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice inheritable unimplemented function to withdraw ether from the vault
|
||||||
|
* */
|
||||||
|
function withdrawFromHelper(uint256 amount) external virtual;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice inheritable unimplemented function to deposit ether into the vault
|
||||||
|
* */
|
||||||
|
function setGasCompensations(uint256 _gasCompensationsLimit) external virtual;
|
||||||
|
}
|
155
src/v2-vault-and-gas/GovernanceGasUpgrade.sol
Normal file
155
src/v2-vault-and-gas/GovernanceGasUpgrade.sol
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { GovernanceVaultUpgrade } from "./GovernanceVaultUpgrade.sol";
|
||||||
|
import { GasCompensator } from "./GasCompensator.sol";
|
||||||
|
import { Math } from "@openzeppelin/contracts/math/Math.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This contract should upgrade governance to be able to compensate gas for certain actions.
|
||||||
|
* These actions are set to castVote, castDelegatedVote in this contract.
|
||||||
|
* */
|
||||||
|
contract GovernanceGasUpgrade is GovernanceVaultUpgrade, GasCompensator {
|
||||||
|
/**
|
||||||
|
* @notice constructor
|
||||||
|
* @param _gasCompLogic gas compensation vault address
|
||||||
|
* @param _userVault tornado vault address
|
||||||
|
* */
|
||||||
|
constructor(address _gasCompLogic, address _userVault)
|
||||||
|
public
|
||||||
|
GovernanceVaultUpgrade(_userVault)
|
||||||
|
GasCompensator(_gasCompLogic)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/// @notice check that msg.sender is multisig
|
||||||
|
modifier onlyMultisig() {
|
||||||
|
require(msg.sender == returnMultisigAddress(), "only multisig");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice receive ether function, does nothing but receive ether
|
||||||
|
* */
|
||||||
|
receive() external payable {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice function to add a certain amount of ether for gas compensations
|
||||||
|
* @dev send ether is used in the logic as we don't expect multisig to make a reentrancy attack on governance
|
||||||
|
* @param gasCompensationsLimit the amount of gas to be compensated
|
||||||
|
* */
|
||||||
|
function setGasCompensations(uint256 gasCompensationsLimit) external virtual override onlyMultisig {
|
||||||
|
require(payable(address(gasCompensationVault)).send(Math.min(gasCompensationsLimit, address(this).balance)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice function to withdraw funds from the gas compensator
|
||||||
|
* @dev send ether is used in the logic as we don't expect multisig to make a reentrancy attack on governance
|
||||||
|
* @param amount the amount of ether to withdraw
|
||||||
|
* */
|
||||||
|
function withdrawFromHelper(uint256 amount) external virtual override onlyMultisig {
|
||||||
|
gasCompensationVault.withdrawToGovernance(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice function to cast callers votes on a proposal
|
||||||
|
* @dev IMPORTANT: This function uses the gasCompensation modifier.
|
||||||
|
* as such this function can trigger a payable fallback.
|
||||||
|
It is not possible to vote without revert more than once,
|
||||||
|
without hasAccountVoted being true, eliminating gas refunds in this case.
|
||||||
|
Gas compensation is also using the low level send(), forwarding 23000 gas
|
||||||
|
as to disallow further logic execution above that threshold.
|
||||||
|
* @param proposalId id of proposal account is voting on
|
||||||
|
* @param support true if yes false if no
|
||||||
|
* */
|
||||||
|
function castVote(uint256 proposalId, bool support)
|
||||||
|
external
|
||||||
|
virtual
|
||||||
|
override
|
||||||
|
gasCompensation(
|
||||||
|
msg.sender,
|
||||||
|
!hasAccountVoted(proposalId, msg.sender) && !checkIfQuorumReached(proposalId),
|
||||||
|
(msg.sender == tx.origin ? 21e3 : 0)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_castVote(msg.sender, proposalId, support);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice function to cast callers votes and votes delegated to the caller
|
||||||
|
* @param from array of addresses that should have delegated to voter
|
||||||
|
* @param proposalId id of proposal account is voting on
|
||||||
|
* @param support true if yes false if no
|
||||||
|
* */
|
||||||
|
function castDelegatedVote(
|
||||||
|
address[] memory from,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support
|
||||||
|
) external virtual override {
|
||||||
|
require(from.length > 0, "Can not be empty");
|
||||||
|
_castDelegatedVote(from, proposalId, support, !hasAccountVoted(proposalId, msg.sender) && !checkIfQuorumReached(proposalId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice checker for success on deployment
|
||||||
|
/// @return returns precise version of governance
|
||||||
|
function version() external pure virtual override returns (string memory) {
|
||||||
|
return "2.lottery-and-gas-upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice function to check if quorum has been reached on a given proposal
|
||||||
|
* @param proposalId id of proposal
|
||||||
|
* @return true if quorum has been reached
|
||||||
|
* */
|
||||||
|
function checkIfQuorumReached(uint256 proposalId) public view returns (bool) {
|
||||||
|
return (proposals[proposalId].forVotes + proposals[proposalId].againstVotes >= QUORUM_VOTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice function to check if account has voted on a proposal
|
||||||
|
* @param proposalId id of proposal account should have voted on
|
||||||
|
* @param account address of the account
|
||||||
|
* @return true if acc has voted
|
||||||
|
* */
|
||||||
|
function hasAccountVoted(uint256 proposalId, address account) public view returns (bool) {
|
||||||
|
return proposals[proposalId].receipts[account].hasVoted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice function to retrieve the multisig address
|
||||||
|
* @dev reasoning: if multisig changes we need governance to approve the next multisig address,
|
||||||
|
* so simply inherit in a governance upgrade from this function and set the new address
|
||||||
|
* @return the multisig address
|
||||||
|
* */
|
||||||
|
function returnMultisigAddress() public pure virtual returns (address) {
|
||||||
|
return 0xb04E030140b30C27bcdfaafFFA98C57d80eDa7B4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This should handle the logic of the external function
|
||||||
|
* @dev IMPORTANT: This function uses the gasCompensation modifier.
|
||||||
|
* as such this function can trigger a payable fallback.
|
||||||
|
* It is not possible to vote without revert more than once,
|
||||||
|
* without hasAccountVoted being true, eliminating gas refunds in this case.
|
||||||
|
* Gas compensation is also using the low level send(), forwarding 23000 gas
|
||||||
|
* as to disallow further logic execution above that threshold.
|
||||||
|
* @param from array of addresses that should have delegated to voter
|
||||||
|
* @param proposalId id of proposal account is voting on
|
||||||
|
* @param support true if yes false if no
|
||||||
|
* @param gasCompensated true if gas should be compensated (given all internal checks pass)
|
||||||
|
* */
|
||||||
|
function _castDelegatedVote(
|
||||||
|
address[] memory from,
|
||||||
|
uint256 proposalId,
|
||||||
|
bool support,
|
||||||
|
bool gasCompensated
|
||||||
|
) internal gasCompensation(msg.sender, gasCompensated, (msg.sender == tx.origin ? 21e3 : 0)) {
|
||||||
|
for (uint256 i = 0; i < from.length; i++) {
|
||||||
|
address delegator = from[i];
|
||||||
|
require(delegatedTo[delegator] == msg.sender || delegator == msg.sender, "Governance: not authorized");
|
||||||
|
require(!gasCompensated || !hasAccountVoted(proposalId, delegator), "Governance: voted already");
|
||||||
|
_castVote(delegator, proposalId, support);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
src/v2-vault-and-gas/GovernanceVaultUpgrade.sol
Normal file
43
src/v2-vault-and-gas/GovernanceVaultUpgrade.sol
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { Governance } from "../v1/Governance.sol";
|
||||||
|
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
|
||||||
|
import { ITornadoVault } from "./interfaces/ITornadoVault.sol";
|
||||||
|
|
||||||
|
/// @title Version 2 Governance contract of the tornado.cash governance
|
||||||
|
contract GovernanceVaultUpgrade is Governance {
|
||||||
|
using SafeMath for uint256;
|
||||||
|
|
||||||
|
// vault which stores user TORN
|
||||||
|
ITornadoVault public immutable userVault;
|
||||||
|
|
||||||
|
// call Governance v1 constructor
|
||||||
|
constructor(address _userVault) public Governance() {
|
||||||
|
userVault = ITornadoVault(_userVault);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Withdraws TORN from governance if conditions permit
|
||||||
|
/// @param amount the amount of TORN to withdraw
|
||||||
|
function unlock(uint256 amount) public virtual override {
|
||||||
|
require(getBlockTimestamp() > canWithdrawAfter[msg.sender], "Governance: tokens are locked");
|
||||||
|
lockedBalance[msg.sender] = lockedBalance[msg.sender].sub(amount, "Governance: insufficient balance");
|
||||||
|
userVault.withdrawTorn(msg.sender, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice checker for success on deployment
|
||||||
|
/// @return returns precise version of governance
|
||||||
|
function version() external pure virtual returns (string memory) {
|
||||||
|
return "2.vault-migration";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice transfers tokens from the contract to the vault, withdrawals are unlock()
|
||||||
|
/// @param owner account/contract which (this) spender will send to the user vault
|
||||||
|
/// @param amount amount which spender will send to the user vault
|
||||||
|
function _transferTokens(address owner, uint256 amount) internal virtual override {
|
||||||
|
require(torn.transferFrom(owner, address(userVault), amount), "TORN: transferFrom failed");
|
||||||
|
lockedBalance[owner] = lockedBalance[owner].add(amount);
|
||||||
|
}
|
||||||
|
}
|
29
src/v2-vault-and-gas/ProposalChanges.md
Normal file
29
src/v2-vault-and-gas/ProposalChanges.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Tornado Governance Changes
|
||||||
|
|
||||||
|
Governance proposal [repo](https://github.com/peppersec/tornado-vault-and-gas-proposal).
|
||||||
|
|
||||||
|
## Governance Vault Upgrade (GovernanceVaultUpgrade.sol)
|
||||||
|
|
||||||
|
`GovernanceVaultUpgrade` is the first major upgrade to tornado governance. This upgrade introduces new logic which is used to communicate with `TornVault` from the governance contract. The motivation behind this upgrade:
|
||||||
|
|
||||||
|
- split DAO member locked TORN from vesting locked TORN.
|
||||||
|
- block Governance from being able to interact with user TORN.
|
||||||
|
|
||||||
|
To solve point 1 of the formerly stated problems, and to reduce the logic bloat of the lock and unlock functionalities, we have opted for calculating the amount of user TORN locked in the governance contract. The calculations and explanations may be found [here](https://github.com/h-ivor/tornado-lottery-period/blob/final_with_auction/scripts/balance_estimation.md).
|
||||||
|
|
||||||
|
### Additions and changes
|
||||||
|
|
||||||
|
| Function/variable signature | is addition or change? | describe significance |
|
||||||
|
| ---------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `_transferTokens(address,uint256)` | change | instead of transferring to the governance contract, funds are now transferred to the torn vault with a `transferFrom` call, this has an effect on both the `lock` and `lockWithApproval` function |
|
||||||
|
| `unlock(uint256)` | change | unlock now triggers `withdrawTorn(address,uint256)` within the vault which reverts on an unsuccessful transfer (safeTransfer) |
|
||||||
|
| `version` | addition | tells current version of governance contract |
|
||||||
|
| `address immutable userVault` | addition | address of the deployed vault |
|
||||||
|
|
||||||
|
### Tornado Vault (TornadoVault.sol)
|
||||||
|
|
||||||
|
The compliment to the above upgrade. Stores user TORN, does not keep records of it. Serves exclusively for deposits and withdrawals. Works in effect as personal store of TORN for a user with the balance being user for voting. Locking mechanisms are still in effect.
|
||||||
|
|
||||||
|
| Function/variable signature | describe significance |
|
||||||
|
| ------------------------------- | --------------------------------------------------- |
|
||||||
|
| `withdrawTorn(address,uint256)` | used for withdrawing TORN balance to users' account |
|
8
src/v2-vault-and-gas/interfaces/ITornadoVault.sol
Normal file
8
src/v2-vault-and-gas/interfaces/ITornadoVault.sol
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
interface ITornadoVault {
|
||||||
|
function withdrawTorn(address recipient, uint256 amount) external;
|
||||||
|
}
|
16
src/v2-vault-and-gas/testing/MockProposal.sol
Normal file
16
src/v2-vault-and-gas/testing/MockProposal.sol
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
|
||||||
|
import "../../v1/Governance.sol";
|
||||||
|
|
||||||
|
contract MockProposal {
|
||||||
|
address public constant GovernanceAddress = 0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce;
|
||||||
|
|
||||||
|
function executeProposal() external {
|
||||||
|
Governance gov = Governance(GovernanceAddress);
|
||||||
|
|
||||||
|
gov.setVotingPeriod(27000);
|
||||||
|
require(gov.VOTING_PERIOD() == 27000, "Voting period change failed!");
|
||||||
|
}
|
||||||
|
}
|
61
src/v3-relayer-registry/GovernanceStakingUpgrade.sol
Normal file
61
src/v3-relayer-registry/GovernanceStakingUpgrade.sol
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { GovernanceGasUpgrade } from "../v2-vault-and-gas/GovernanceGasUpgrade.sol";
|
||||||
|
import { ITornadoStakingRewards } from "./interfaces/ITornadoStakingRewards.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice The Governance staking upgrade. Adds modifier to any un/lock operation to update rewards
|
||||||
|
* @dev CONTRACT RISKS:
|
||||||
|
* - if updateRewards reverts (should not happen due to try/catch) locks/unlocks could be blocked
|
||||||
|
* - generally inherits risks from former governance upgrades
|
||||||
|
*/
|
||||||
|
contract GovernanceStakingUpgrade is GovernanceGasUpgrade {
|
||||||
|
ITornadoStakingRewards public immutable Staking;
|
||||||
|
|
||||||
|
event RewardUpdateSuccessful(address indexed account);
|
||||||
|
event RewardUpdateFailed(address indexed account, bytes indexed errorData);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
address stakingRewardsAddress,
|
||||||
|
address gasCompLogic,
|
||||||
|
address userVaultAddress
|
||||||
|
) public GovernanceGasUpgrade(gasCompLogic, userVaultAddress) {
|
||||||
|
Staking = ITornadoStakingRewards(stakingRewardsAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This modifier should make a call to Staking to update the rewards for account without impacting logic on revert
|
||||||
|
* @dev try / catch block to handle reverts
|
||||||
|
* @param account Account to update rewards for.
|
||||||
|
* */
|
||||||
|
modifier updateRewards(address account) {
|
||||||
|
try Staking.updateRewardsOnLockedBalanceChange(account, lockedBalance[account]) {
|
||||||
|
emit RewardUpdateSuccessful(account);
|
||||||
|
} catch (bytes memory errorData) {
|
||||||
|
emit RewardUpdateFailed(account, errorData);
|
||||||
|
}
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock(
|
||||||
|
address owner,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 deadline,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) public virtual override updateRewards(owner) {
|
||||||
|
super.lock(owner, amount, deadline, v, r, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockWithApproval(uint256 amount) public virtual override updateRewards(msg.sender) {
|
||||||
|
super.lockWithApproval(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlock(uint256 amount) public virtual override updateRewards(msg.sender) {
|
||||||
|
super.unlock(amount);
|
||||||
|
}
|
||||||
|
}
|
14
src/v3-relayer-registry/ProposalChanges.md
Normal file
14
src/v3-relayer-registry/ProposalChanges.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Tornado Relayer Registry
|
||||||
|
|
||||||
|
Governance proposal [repo](https://github.com/Rezan-vm/tornado-relayer-registry).
|
||||||
|
|
||||||
|
Governance upgrade which includes a registry for relayer registration and staking mechanisms for the TORN token.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
1. Anyone can become a relayer by staking TORN into Registry contract.
|
||||||
|
2. Minimum stake is governed by the Governance.
|
||||||
|
3. Each Pool has its own fee % which is also set by the Governance.
|
||||||
|
4. On every withdrawal via relayer, the relayer has to pay the Tornado Pool fee in TORN. The fee is deducted from his staked balance.
|
||||||
|
5. All collected fees are stored into StakingReward contract.
|
||||||
|
6. Any TORN holder can stake their TORN into Governance contract like they were before, but earning fees proportionately to their stake.
|
@ -0,0 +1,7 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
|
||||||
|
interface ITornadoStakingRewards {
|
||||||
|
function updateRewardsOnLockedBalanceChange(address account, uint256 amountLockedBeforehand) external;
|
||||||
|
}
|
70
src/v4-patch/GovernancePatchUpgrade.sol
Normal file
70
src/v4-patch/GovernancePatchUpgrade.sol
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import "../v1/Governance.sol";
|
||||||
|
import "../v3-relayer-registry/GovernanceStakingUpgrade.sol";
|
||||||
|
|
||||||
|
contract GovernancePatchUpgrade is GovernanceStakingUpgrade {
|
||||||
|
mapping(uint256 => bytes32) public proposalCodehashes;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
address stakingRewardsAddress,
|
||||||
|
address gasCompLogic,
|
||||||
|
address userVaultAddress
|
||||||
|
) public GovernanceStakingUpgrade(stakingRewardsAddress, gasCompLogic, userVaultAddress) {}
|
||||||
|
|
||||||
|
/// @notice Return the version of the contract
|
||||||
|
function version() external pure virtual override returns (string memory) {
|
||||||
|
return "4.patch-exploit";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Execute a proposal
|
||||||
|
* @dev This upgrade should protect against Metamorphic contracts by comparing the proposal's extcodehash with a stored one
|
||||||
|
* @param proposalId The proposal's ID
|
||||||
|
*/
|
||||||
|
function execute(uint256 proposalId) public payable virtual override(Governance) {
|
||||||
|
require(msg.sender != address(this), "Governance::propose: pseudo-external function");
|
||||||
|
|
||||||
|
Proposal storage proposal = proposals[proposalId];
|
||||||
|
|
||||||
|
address target = proposal.target;
|
||||||
|
|
||||||
|
bytes32 proposalCodehash;
|
||||||
|
|
||||||
|
assembly {
|
||||||
|
proposalCodehash := extcodehash(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
require(proposalCodehash == proposalCodehashes[proposalId], "Governance::propose: metamorphic contracts not allowed");
|
||||||
|
|
||||||
|
super.execute(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Internal function called from propoese
|
||||||
|
* @dev This should store the extcodehash of the proposal contract
|
||||||
|
* @param proposer proposer address
|
||||||
|
* @param target smart contact address that will be executed as result of voting
|
||||||
|
* @param description description of the proposal
|
||||||
|
* @return proposalId new proposal id
|
||||||
|
*/
|
||||||
|
function _propose(
|
||||||
|
address proposer,
|
||||||
|
address target,
|
||||||
|
string memory description
|
||||||
|
) internal virtual override(Governance) returns (uint256 proposalId) {
|
||||||
|
// Implies all former predicates were valid
|
||||||
|
proposalId = super._propose(proposer, target, description);
|
||||||
|
|
||||||
|
bytes32 proposalCodehash;
|
||||||
|
|
||||||
|
assembly {
|
||||||
|
proposalCodehash := extcodehash(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
proposalCodehashes[proposalId] = proposalCodehash;
|
||||||
|
}
|
||||||
|
}
|
111
src/v4-patch/PatchProposal.sol
Normal file
111
src/v4-patch/PatchProposal.sol
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
|
||||||
|
import { LoopbackProxy } from "../v1/LoopbackProxy.sol";
|
||||||
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
|
||||||
|
|
||||||
|
import { GovernancePatchUpgrade } from "./GovernancePatchUpgrade.sol";
|
||||||
|
import { TornadoStakingRewards } from "./TornadoStakingRewards.sol";
|
||||||
|
import { RelayerRegistry } from "./RelayerRegistry.sol";
|
||||||
|
|
||||||
|
interface Proxy {
|
||||||
|
function upgradeTo(address newImplementation) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Contract which should help the proposal deploy the necessary contracts.
|
||||||
|
*/
|
||||||
|
contract PatchProposalContractsFactory {
|
||||||
|
/**
|
||||||
|
* @notice Create a new TornadoStakingRewards contract.
|
||||||
|
* @param governance The address of Tornado Cash Goveranance.
|
||||||
|
* @param torn The torn token address.
|
||||||
|
* @param registry The address of the relayer registry.
|
||||||
|
* @return The address of the new staking contract.
|
||||||
|
*/
|
||||||
|
function createStakingRewards(
|
||||||
|
address governance,
|
||||||
|
address torn,
|
||||||
|
address registry
|
||||||
|
) external returns (address) {
|
||||||
|
return address(new TornadoStakingRewards(governance, torn, registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Create a new RelayerRegistry contract.
|
||||||
|
* @param torn The torn token address.
|
||||||
|
* @param governance The address of Tornado Cash Goveranance.
|
||||||
|
* @param ens The ens registrar address.
|
||||||
|
* @param staking The TornadoStakingRewards contract address.
|
||||||
|
* @return The address of the new registry contract.
|
||||||
|
*/
|
||||||
|
function createRegistryContract(
|
||||||
|
address torn,
|
||||||
|
address governance,
|
||||||
|
address ens,
|
||||||
|
address staking,
|
||||||
|
address feeManager
|
||||||
|
) external returns (address) {
|
||||||
|
return address(new RelayerRegistry(torn, governance, ens, staking, feeManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Proposal which should patch governance against the metamorphic contract replacement vulnerability.
|
||||||
|
*/
|
||||||
|
contract PatchProposal {
|
||||||
|
using SafeMath for uint256;
|
||||||
|
using Address for address;
|
||||||
|
|
||||||
|
address public constant feeManagerAddress = 0x5f6c97C6AD7bdd0AE7E0Dd4ca33A4ED3fDabD4D7;
|
||||||
|
address public constant ensAddress = 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e;
|
||||||
|
address public immutable registry = 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2;
|
||||||
|
|
||||||
|
IERC20 public constant TORN = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C);
|
||||||
|
|
||||||
|
PatchProposalContractsFactory public immutable patchProposalContractsFactory;
|
||||||
|
|
||||||
|
constructor(address _patchProposalContractsFactory) public {
|
||||||
|
patchProposalContractsFactory = PatchProposalContractsFactory(_patchProposalContractsFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Function to execute the proposal.
|
||||||
|
function executeProposal() external {
|
||||||
|
// address(this) has to be governance
|
||||||
|
address payable governance = payable(address(this));
|
||||||
|
|
||||||
|
// Get the two contracts gov depends on
|
||||||
|
address gasComp = address(GovernancePatchUpgrade(governance).gasCompensationVault());
|
||||||
|
address vault = address(GovernancePatchUpgrade(governance).userVault());
|
||||||
|
|
||||||
|
// Get the old staking contract
|
||||||
|
TornadoStakingRewards oldStaking = TornadoStakingRewards(address(GovernancePatchUpgrade(governance).Staking()));
|
||||||
|
|
||||||
|
// Get the small amount of TORN left
|
||||||
|
oldStaking.withdrawTorn(TORN.balanceOf(address(oldStaking)));
|
||||||
|
|
||||||
|
// And create a new staking contract
|
||||||
|
TornadoStakingRewards newStaking = TornadoStakingRewards(
|
||||||
|
patchProposalContractsFactory.createStakingRewards(address(governance), address(TORN), registry)
|
||||||
|
);
|
||||||
|
|
||||||
|
// And a new registry implementation
|
||||||
|
address newRegistryImplementationAddress = patchProposalContractsFactory.createRegistryContract(
|
||||||
|
address(TORN),
|
||||||
|
address(governance),
|
||||||
|
ensAddress,
|
||||||
|
address(newStaking),
|
||||||
|
feeManagerAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upgrade the registry proxy
|
||||||
|
Proxy(registry).upgradeTo(newRegistryImplementationAddress);
|
||||||
|
|
||||||
|
// Now upgrade the governance to the latest stuff
|
||||||
|
LoopbackProxy(payable(governance)).upgradeTo(address(new GovernancePatchUpgrade(address(newStaking), gasComp, vault)));
|
||||||
|
}
|
||||||
|
}
|
384
src/v4-patch/RelayerRegistry.sol
Normal file
384
src/v4-patch/RelayerRegistry.sol
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
|
||||||
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol";
|
||||||
|
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
|
||||||
|
import { EnsResolve } from "torn-token/contracts/ENS.sol";
|
||||||
|
import { TORN } from "torn-token/contracts/TORN.sol";
|
||||||
|
import { TornadoStakingRewards } from "./TornadoStakingRewards.sol";
|
||||||
|
|
||||||
|
interface ITornadoInstance {
|
||||||
|
function token() external view returns (address);
|
||||||
|
|
||||||
|
function denomination() external view returns (uint256);
|
||||||
|
|
||||||
|
function deposit(bytes32 commitment) external payable;
|
||||||
|
|
||||||
|
function withdraw(
|
||||||
|
bytes calldata proof,
|
||||||
|
bytes32 root,
|
||||||
|
bytes32 nullifierHash,
|
||||||
|
address payable recipient,
|
||||||
|
address payable relayer,
|
||||||
|
uint256 fee,
|
||||||
|
uint256 refund
|
||||||
|
) external payable;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IENS {
|
||||||
|
function owner(bytes32 node) external view returns (address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @dev Solidity implementation of the ENS namehash algorithm.
|
||||||
|
*
|
||||||
|
* Warning! Does not normalize or validate names before hashing.
|
||||||
|
* Original version can be found here https://github.com/JonahGroendal/ens-namehash/
|
||||||
|
*/
|
||||||
|
library ENSNamehash {
|
||||||
|
function namehash(bytes memory domain) internal pure returns (bytes32) {
|
||||||
|
return namehash(domain, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function namehash(bytes memory domain, uint256 i) internal pure returns (bytes32) {
|
||||||
|
if (domain.length <= i) return 0x0000000000000000000000000000000000000000000000000000000000000000;
|
||||||
|
|
||||||
|
uint256 len = labelLength(domain, i);
|
||||||
|
|
||||||
|
return keccak256(abi.encodePacked(namehash(domain, i + len + 1), keccak(domain, i, len)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelLength(bytes memory domain, uint256 i) private pure returns (uint256) {
|
||||||
|
uint256 len;
|
||||||
|
while (i + len != domain.length && domain[i + len] != 0x2e) {
|
||||||
|
len++;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keccak(
|
||||||
|
bytes memory data,
|
||||||
|
uint256 offset,
|
||||||
|
uint256 len
|
||||||
|
) private pure returns (bytes32 ret) {
|
||||||
|
require(offset + len <= data.length);
|
||||||
|
assembly {
|
||||||
|
ret := keccak256(add(add(data, 32), offset), len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFeeManager {
|
||||||
|
function instanceFeeWithUpdate(ITornadoInstance _instance) external returns (uint160);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RelayerState {
|
||||||
|
uint256 balance;
|
||||||
|
bytes32 ensHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Registry contract, one of the main contracts of this protocol upgrade.
|
||||||
|
* The contract should store relayers' addresses and data attributed to the
|
||||||
|
* master address of the relayer. This data includes the relayers stake and
|
||||||
|
* his ensHash.
|
||||||
|
* A relayers master address has a number of subaddresses called "workers",
|
||||||
|
* these are all addresses which burn stake in communication with the proxy.
|
||||||
|
* If a relayer is not registered, he is not displayed on the frontend.
|
||||||
|
* @dev CONTRACT RISKS:
|
||||||
|
* - if setter functions are compromised, relayer metadata would be at risk, including the noted amount of his balance
|
||||||
|
* - if burn function is compromised, relayers run the risk of being unable to handle withdrawals
|
||||||
|
* - the above risk also applies to the nullify balance function
|
||||||
|
* */
|
||||||
|
contract RelayerRegistry is Initializable, EnsResolve {
|
||||||
|
using SafeMath for uint256;
|
||||||
|
using SafeERC20 for TORN;
|
||||||
|
using ENSNamehash for bytes;
|
||||||
|
|
||||||
|
TORN public immutable torn;
|
||||||
|
address public immutable governance;
|
||||||
|
IENS public immutable ens;
|
||||||
|
TornadoStakingRewards public immutable staking;
|
||||||
|
IFeeManager public immutable feeManager;
|
||||||
|
|
||||||
|
address public tornadoRouter;
|
||||||
|
uint256 public minStakeAmount;
|
||||||
|
|
||||||
|
mapping(address => RelayerState) public relayers;
|
||||||
|
mapping(address => address) public workers;
|
||||||
|
|
||||||
|
event RelayerBalanceNullified(address relayer);
|
||||||
|
event WorkerRegistered(address relayer, address worker);
|
||||||
|
event WorkerUnregistered(address relayer, address worker);
|
||||||
|
event StakeAddedToRelayer(address relayer, uint256 amountStakeAdded);
|
||||||
|
event StakeBurned(address relayer, uint256 amountBurned);
|
||||||
|
event MinimumStakeAmount(uint256 minStakeAmount);
|
||||||
|
event RouterRegistered(address tornadoRouter);
|
||||||
|
event RelayerRegistered(bytes32 relayer, string ensName, address relayerAddress, uint256 stakedAmount);
|
||||||
|
|
||||||
|
modifier onlyGovernance() {
|
||||||
|
require(msg.sender == governance, "only governance");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier onlyTornadoRouter() {
|
||||||
|
require(msg.sender == tornadoRouter, "only proxy");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier onlyRelayer(address sender, address relayer) {
|
||||||
|
require(workers[sender] == relayer, "only relayer");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
address _torn,
|
||||||
|
address _governance,
|
||||||
|
address _ens,
|
||||||
|
address _staking,
|
||||||
|
address _feeManager
|
||||||
|
) public {
|
||||||
|
torn = TORN(_torn);
|
||||||
|
governance = _governance;
|
||||||
|
ens = IENS(_ens);
|
||||||
|
staking = TornadoStakingRewards(_staking);
|
||||||
|
feeManager = IFeeManager(_feeManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice initialize function for upgradeability
|
||||||
|
* @dev this contract will be deployed behind a proxy and should not assign values at logic address,
|
||||||
|
* params left out because self explainable
|
||||||
|
* */
|
||||||
|
function initialize(bytes32 _tornadoRouter) external initializer {
|
||||||
|
tornadoRouter = resolve(_tornadoRouter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should register a master address and optionally a set of workeres for a relayer + metadata
|
||||||
|
* @dev Relayer can't steal other relayers workers since they are registered, and a wallet (msg.sender check) can always unregister itself
|
||||||
|
* @param ensName ens name of the relayer
|
||||||
|
* @param stake the initial amount of stake in TORN the relayer is depositing
|
||||||
|
* */
|
||||||
|
function register(
|
||||||
|
string calldata ensName,
|
||||||
|
uint256 stake,
|
||||||
|
address[] calldata workersToRegister
|
||||||
|
) external {
|
||||||
|
_register(msg.sender, ensName, stake, workersToRegister);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Register function equivalent with permit-approval instead of regular approve.
|
||||||
|
* */
|
||||||
|
function registerPermit(
|
||||||
|
string calldata ensName,
|
||||||
|
uint256 stake,
|
||||||
|
address[] calldata workersToRegister,
|
||||||
|
address relayer,
|
||||||
|
uint256 deadline,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) external {
|
||||||
|
torn.permit(relayer, address(this), stake, deadline, v, r, s);
|
||||||
|
_register(relayer, ensName, stake, workersToRegister);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _register(
|
||||||
|
address relayer,
|
||||||
|
string calldata ensName,
|
||||||
|
uint256 stake,
|
||||||
|
address[] calldata workersToRegister
|
||||||
|
) internal {
|
||||||
|
bytes32 ensHash = bytes(ensName).namehash();
|
||||||
|
require(relayer == ens.owner(ensHash), "only ens owner");
|
||||||
|
require(workers[relayer] == address(0), "cant register again");
|
||||||
|
RelayerState storage metadata = relayers[relayer];
|
||||||
|
|
||||||
|
require(metadata.ensHash == bytes32(0), "registered already");
|
||||||
|
require(stake >= minStakeAmount, "!min_stake");
|
||||||
|
|
||||||
|
torn.safeTransferFrom(relayer, address(staking), stake);
|
||||||
|
emit StakeAddedToRelayer(relayer, stake);
|
||||||
|
|
||||||
|
metadata.balance = stake;
|
||||||
|
metadata.ensHash = ensHash;
|
||||||
|
workers[relayer] = relayer;
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < workersToRegister.length; i++) {
|
||||||
|
address worker = workersToRegister[i];
|
||||||
|
_registerWorker(relayer, worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit RelayerRegistered(ensHash, ensName, relayer, stake);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow relayers to register more workeres
|
||||||
|
* @param relayer Relayer which should send message from any worker which is already registered
|
||||||
|
* @param worker Address to register
|
||||||
|
* */
|
||||||
|
function registerWorker(address relayer, address worker) external onlyRelayer(msg.sender, relayer) {
|
||||||
|
_registerWorker(relayer, worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _registerWorker(address relayer, address worker) internal {
|
||||||
|
require(workers[worker] == address(0), "can't steal an address");
|
||||||
|
workers[worker] = relayer;
|
||||||
|
emit WorkerRegistered(relayer, worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow anybody to unregister an address they own
|
||||||
|
* @dev designed this way as to allow someone to unregister themselves in case a relayer misbehaves
|
||||||
|
* - this should be followed by an action like burning relayer stake
|
||||||
|
* - there was an option of allowing the sender to burn relayer stake in case of malicious behaviour, this feature was not included in the end
|
||||||
|
* - reverts if trying to unregister master, otherwise contract would break. in general, there should be no reason to unregister master at all
|
||||||
|
* */
|
||||||
|
function unregisterWorker(address worker) external {
|
||||||
|
if (worker != msg.sender) require(workers[worker] == msg.sender, "only owner of worker");
|
||||||
|
require(workers[worker] != worker, "cant unregister master");
|
||||||
|
emit WorkerUnregistered(workers[worker], worker);
|
||||||
|
workers[worker] = address(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow anybody to stake to a relayer more TORN
|
||||||
|
* @param relayer Relayer main address to stake to
|
||||||
|
* @param stake Stake to be added to relayer
|
||||||
|
* */
|
||||||
|
function stakeToRelayer(address relayer, uint256 stake) external {
|
||||||
|
_stakeToRelayer(msg.sender, relayer, stake);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev stakeToRelayer function equivalent with permit-approval instead of regular approve.
|
||||||
|
* @param staker address from that stake is paid
|
||||||
|
* */
|
||||||
|
function stakeToRelayerPermit(
|
||||||
|
address relayer,
|
||||||
|
uint256 stake,
|
||||||
|
address staker,
|
||||||
|
uint256 deadline,
|
||||||
|
uint8 v,
|
||||||
|
bytes32 r,
|
||||||
|
bytes32 s
|
||||||
|
) external {
|
||||||
|
torn.permit(staker, address(this), stake, deadline, v, r, s);
|
||||||
|
_stakeToRelayer(staker, relayer, stake);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stakeToRelayer(
|
||||||
|
address staker,
|
||||||
|
address relayer,
|
||||||
|
uint256 stake
|
||||||
|
) internal {
|
||||||
|
require(workers[relayer] == relayer, "!registered");
|
||||||
|
torn.safeTransferFrom(staker, address(staking), stake);
|
||||||
|
relayers[relayer].balance = stake.add(relayers[relayer].balance);
|
||||||
|
emit StakeAddedToRelayer(relayer, stake);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should burn some relayer stake on withdraw and notify staking of this
|
||||||
|
* @dev IMPORTANT FUNCTION:
|
||||||
|
* - This should be only called by the tornado proxy
|
||||||
|
* - Should revert if relayer does not call proxy from valid worker
|
||||||
|
* - Should not overflow
|
||||||
|
* - Should underflow and revert (SafeMath) on not enough stake (balance)
|
||||||
|
* @param sender worker to check sender == relayer
|
||||||
|
* @param relayer address of relayer who's stake is being burned
|
||||||
|
* @param pool instance to get fee for
|
||||||
|
* */
|
||||||
|
function burn(
|
||||||
|
address sender,
|
||||||
|
address relayer,
|
||||||
|
ITornadoInstance pool
|
||||||
|
) external onlyTornadoRouter {
|
||||||
|
address masterAddress = workers[sender];
|
||||||
|
if (masterAddress == address(0)) {
|
||||||
|
require(workers[relayer] == address(0), "Only custom relayer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require(masterAddress == relayer, "only relayer");
|
||||||
|
uint256 toBurn = feeManager.instanceFeeWithUpdate(pool);
|
||||||
|
relayers[relayer].balance = relayers[relayer].balance.sub(toBurn);
|
||||||
|
staking.addBurnRewards(toBurn);
|
||||||
|
emit StakeBurned(relayer, toBurn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow governance to set the minimum stake amount
|
||||||
|
* @param minAmount new minimum stake amount
|
||||||
|
* */
|
||||||
|
function setMinStakeAmount(uint256 minAmount) external onlyGovernance {
|
||||||
|
minStakeAmount = minAmount;
|
||||||
|
emit MinimumStakeAmount(minAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow governance to set a new tornado proxy address
|
||||||
|
* @param tornadoRouterAddress address of the new proxy
|
||||||
|
* */
|
||||||
|
function setTornadoRouter(address tornadoRouterAddress) external onlyGovernance {
|
||||||
|
tornadoRouter = tornadoRouterAddress;
|
||||||
|
emit RouterRegistered(tornadoRouterAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow governance to nullify a relayers balance
|
||||||
|
* @dev IMPORTANT FUNCTION:
|
||||||
|
* - Should nullify the balance
|
||||||
|
* - Adding nullified balance as rewards was refactored to allow for the flexibility of these funds (for gov to operate with them)
|
||||||
|
* @param relayer address of relayer who's balance is to nullify
|
||||||
|
* */
|
||||||
|
function nullifyBalance(address relayer) external onlyGovernance {
|
||||||
|
address masterAddress = workers[relayer];
|
||||||
|
require(relayer == masterAddress, "must be master");
|
||||||
|
relayers[masterAddress].balance = 0;
|
||||||
|
emit RelayerBalanceNullified(relayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should check if a worker is associated with a relayer
|
||||||
|
* @param toResolve address to check
|
||||||
|
* @return true if is associated
|
||||||
|
* */
|
||||||
|
function isRelayer(address toResolve) external view returns (bool) {
|
||||||
|
return workers[toResolve] != address(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should check if a worker is registered to the relayer stated
|
||||||
|
* @param relayer relayer to check
|
||||||
|
* @param toResolve address to check
|
||||||
|
* @return true if registered
|
||||||
|
* */
|
||||||
|
function isRelayerRegistered(address relayer, address toResolve) external view returns (bool) {
|
||||||
|
return workers[toResolve] == relayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should get a relayers ensHash
|
||||||
|
* @param relayer address to fetch for
|
||||||
|
* @return relayer's ensHash
|
||||||
|
* */
|
||||||
|
function getRelayerEnsHash(address relayer) external view returns (bytes32) {
|
||||||
|
return relayers[workers[relayer]].ensHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should get a relayers balance
|
||||||
|
* @param relayer relayer who's balance is to fetch
|
||||||
|
* @return relayer's balance
|
||||||
|
* */
|
||||||
|
function getRelayerBalance(address relayer) external view returns (uint256) {
|
||||||
|
return relayers[workers[relayer]].balance;
|
||||||
|
}
|
||||||
|
}
|
143
src/v4-patch/TornadoStakingRewards.sol
Normal file
143
src/v4-patch/TornadoStakingRewards.sol
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
|
||||||
|
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
|
||||||
|
import { Initializable } from "@openzeppelin/contracts/proxy/Initializable.sol";
|
||||||
|
import { EnsResolve } from "torn-token/contracts/ENS.sol";
|
||||||
|
|
||||||
|
interface ITornadoVault {
|
||||||
|
function withdrawTorn(address recipient, uint256 amount) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITornadoGovernance {
|
||||||
|
function lockedBalance(address account) external view returns (uint256);
|
||||||
|
|
||||||
|
function userVault() external view returns (ITornadoVault);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This is the staking contract of the governance staking upgrade.
|
||||||
|
* This contract should hold the staked funds which are received upon relayer registration,
|
||||||
|
* and properly attribute rewards to addresses without security issues.
|
||||||
|
* @dev CONTRACT RISKS:
|
||||||
|
* - Relayer staked TORN at risk if contract is compromised.
|
||||||
|
* */
|
||||||
|
contract TornadoStakingRewards is Initializable, EnsResolve {
|
||||||
|
using SafeMath for uint256;
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
/// @notice 1e25
|
||||||
|
uint256 public immutable ratioConstant;
|
||||||
|
ITornadoGovernance public immutable Governance;
|
||||||
|
IERC20 public immutable torn;
|
||||||
|
address public immutable relayerRegistry;
|
||||||
|
|
||||||
|
/// @notice the sum torn_burned_i/locked_amount_i*coefficient where i is incremented at each burn
|
||||||
|
uint256 public accumulatedRewardPerTorn;
|
||||||
|
/// @notice notes down accumulatedRewardPerTorn for an address on a lock/unlock/claim
|
||||||
|
mapping(address => uint256) public accumulatedRewardRateOnLastUpdate;
|
||||||
|
/// @notice notes down how much an account may claim
|
||||||
|
mapping(address => uint256) public accumulatedRewards;
|
||||||
|
|
||||||
|
event RewardsUpdated(address indexed account, uint256 rewards);
|
||||||
|
event RewardsClaimed(address indexed account, uint256 rewardsClaimed);
|
||||||
|
|
||||||
|
modifier onlyGovernance() {
|
||||||
|
require(msg.sender == address(Governance), "only governance");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minor code change here we won't resolve the registry by ENS
|
||||||
|
constructor(
|
||||||
|
address governanceAddress,
|
||||||
|
address tornAddress,
|
||||||
|
address _relayerRegistry
|
||||||
|
) public {
|
||||||
|
Governance = ITornadoGovernance(governanceAddress);
|
||||||
|
torn = IERC20(tornAddress);
|
||||||
|
relayerRegistry = _relayerRegistry;
|
||||||
|
ratioConstant = IERC20(tornAddress).totalSupply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should safely send a user his rewards.
|
||||||
|
* @dev IMPORTANT FUNCTION:
|
||||||
|
* We know that rewards are going to be updated every time someone locks or unlocks
|
||||||
|
* so we know that this function can't be used to falsely increase the amount of
|
||||||
|
* lockedTorn by locking in governance and subsequently calling it.
|
||||||
|
* - set rewards to 0 greedily
|
||||||
|
*/
|
||||||
|
function getReward() external {
|
||||||
|
uint256 rewards = _updateReward(msg.sender, Governance.lockedBalance(msg.sender));
|
||||||
|
rewards = rewards.add(accumulatedRewards[msg.sender]);
|
||||||
|
accumulatedRewards[msg.sender] = 0;
|
||||||
|
torn.safeTransfer(msg.sender, rewards);
|
||||||
|
emit RewardsClaimed(msg.sender, rewards);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should increment the proper amount of rewards per torn for the contract
|
||||||
|
* @dev IMPORTANT FUNCTION:
|
||||||
|
* - calculation must not overflow with extreme values
|
||||||
|
* (amount <= 1e25) * 1e25 / (balance of vault <= 1e25) -> (extreme values)
|
||||||
|
* @param amount amount to add to the rewards
|
||||||
|
*/
|
||||||
|
function addBurnRewards(uint256 amount) external {
|
||||||
|
require(msg.sender == address(Governance) || msg.sender == relayerRegistry, "unauthorized");
|
||||||
|
accumulatedRewardPerTorn = accumulatedRewardPerTorn.add(
|
||||||
|
amount.mul(ratioConstant).div(torn.balanceOf(address(Governance.userVault())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow governance to properly update the accumulated rewards rate for an account
|
||||||
|
* @param account address of account to update data for
|
||||||
|
* @param amountLockedBeforehand the balance locked beforehand in the governance contract
|
||||||
|
* */
|
||||||
|
function updateRewardsOnLockedBalanceChange(address account, uint256 amountLockedBeforehand) external onlyGovernance {
|
||||||
|
uint256 claimed = _updateReward(account, amountLockedBeforehand);
|
||||||
|
accumulatedRewards[account] = accumulatedRewards[account].add(claimed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should allow governance rescue tokens from the staking rewards contract
|
||||||
|
* */
|
||||||
|
function withdrawTorn(uint256 amount) external onlyGovernance {
|
||||||
|
if (amount == type(uint256).max) amount = torn.balanceOf(address(this));
|
||||||
|
torn.safeTransfer(address(Governance), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should calculated the proper amount of rewards attributed to user since the last update
|
||||||
|
* @dev IMPORTANT FUNCTION:
|
||||||
|
* - calculation must not overflow with extreme values
|
||||||
|
* (accumulatedReward <= 1e25) * (lockedBeforehand <= 1e25) / 1e25
|
||||||
|
* - result may go to 0, since this implies on 1 TORN locked => accumulatedReward <= 1e7, meaning a very small reward
|
||||||
|
* @param account address of account to calculate rewards for
|
||||||
|
* @param amountLockedBeforehand the balance locked beforehand in the governance contract
|
||||||
|
* @return claimed the rewards attributed to user since the last update
|
||||||
|
*/
|
||||||
|
function _updateReward(address account, uint256 amountLockedBeforehand) private returns (uint256 claimed) {
|
||||||
|
if (amountLockedBeforehand != 0)
|
||||||
|
claimed = (accumulatedRewardPerTorn.sub(accumulatedRewardRateOnLastUpdate[account])).mul(amountLockedBeforehand).div(
|
||||||
|
ratioConstant
|
||||||
|
);
|
||||||
|
accumulatedRewardRateOnLastUpdate[account] = accumulatedRewardPerTorn;
|
||||||
|
emit RewardsUpdated(account, claimed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice This function should show a user his rewards.
|
||||||
|
* @param account address of account to calculate rewards for
|
||||||
|
*/
|
||||||
|
function checkReward(address account) external view returns (uint256 rewards) {
|
||||||
|
uint256 amountLocked = Governance.lockedBalance(account);
|
||||||
|
if (amountLocked != 0)
|
||||||
|
rewards = (accumulatedRewardPerTorn.sub(accumulatedRewardRateOnLastUpdate[account])).mul(amountLocked).div(ratioConstant);
|
||||||
|
rewards = rewards.add(accumulatedRewards[account]);
|
||||||
|
}
|
||||||
|
}
|
13
src/v4-patch/metamorphic/IMetamorphicContractFactory.sol
Normal file
13
src/v4-patch/metamorphic/IMetamorphicContractFactory.sol
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity 0.6.12;
|
||||||
|
|
||||||
|
interface IMetamorphicContractFactory {
|
||||||
|
function findMetamorphicContractAddress(bytes32 salt) external view returns (address metamorphicContractAddress);
|
||||||
|
|
||||||
|
function deployMetamorphicContractFromExistingImplementation(
|
||||||
|
bytes32 salt,
|
||||||
|
address implementationContract,
|
||||||
|
bytes calldata metamorphicContractInitializationCalldata
|
||||||
|
) external payable returns (address metamorphicContractAddress);
|
||||||
|
}
|
32
src/v4-patch/mock/MockProposals.sol
Normal file
32
src/v4-patch/mock/MockProposals.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
|
||||||
|
contract InitialProposal {
|
||||||
|
event MockExecuted(uint256 num);
|
||||||
|
|
||||||
|
function executeProposal() external virtual {
|
||||||
|
emit MockExecuted(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emergencyStop() public {
|
||||||
|
selfdestruct(payable(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contract MaliciousProposal is InitialProposal {
|
||||||
|
address public immutable deployer;
|
||||||
|
|
||||||
|
constructor() public {
|
||||||
|
deployer = msg.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeProposal() external virtual override {
|
||||||
|
IERC20 torn = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C);
|
||||||
|
uint256 bal = torn.balanceOf(address(this));
|
||||||
|
torn.transfer(deployer, bal);
|
||||||
|
}
|
||||||
|
}
|
103
test/MockProposal.sol
Normal file
103
test/MockProposal.sol
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.6.12;
|
||||||
|
|
||||||
|
import {Parameters} from "@proprietary/Parameters.sol";
|
||||||
|
import {IGovernance} from "@interfaces/IGovernance.sol";
|
||||||
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
|
||||||
|
import {Test} from "@forge-std/Test.sol";
|
||||||
|
|
||||||
|
contract MockProposal is Parameters, Test {
|
||||||
|
IGovernance internal governance = IGovernance(_governanceAddress);
|
||||||
|
|
||||||
|
uint256 public constant TEST_PRIVATE_KEY_ONE = 0x66ddbd7cbe4a566df405f6ded0b908c669f88cdb1656380c050e3a457bd21df0;
|
||||||
|
uint256 public constant TEST_PRIVATE_KEY_TWO = 0xa4c8c98120e77741a87a116074a2df4ddb20d1149069290fd4a3d7ee65c55064;
|
||||||
|
address public constant TEST_ADDRESS_ONE = 0x118251976c65AFAf291f5255450ddb5b6A4d8B88;
|
||||||
|
address public constant TEST_ADDRESS_TWO = 0x63aE7d90Eb37ca39FC62dD9991DbEfeE70673a20;
|
||||||
|
|
||||||
|
uint256 public constant PROPOSAL_VOTING_DURATION = 5 days;
|
||||||
|
uint256 public constant PROPOSAL_LOCKED_DURATION = 2 days;
|
||||||
|
uint256 public constant PROPOSAL_DURATION = PROPOSAL_VOTING_DURATION + PROPOSAL_LOCKED_DURATION;
|
||||||
|
uint256 public constant PROPOSAL_EXECUTION_MAX_DURATION = 3 days;
|
||||||
|
uint256 public constant PROPOSAL_THRESHOLD = 25000 ether;
|
||||||
|
string public constant PROPOSAL_DESCRIPTION = "{title:'Some proposal',description:''}";
|
||||||
|
|
||||||
|
address public constant VERIFIER_ADDRESS = 0x77777FeDdddFfC19Ff86DB637967013e6C6A116C;
|
||||||
|
|
||||||
|
bytes32 public constant PERMIT_TYPEHASH =
|
||||||
|
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
|
||||||
|
|
||||||
|
bytes32 public constant EIP712_DOMAIN = keccak256(
|
||||||
|
abi.encode(
|
||||||
|
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
|
||||||
|
keccak256(bytes("TornadoCash")),
|
||||||
|
keccak256(bytes("1")),
|
||||||
|
1,
|
||||||
|
VERIFIER_ADDRESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
uint16 public constant PERMIT_FUNC_SELECTOR = uint16(0x1901);
|
||||||
|
|
||||||
|
function executeProposal(address proposalAddress) public {
|
||||||
|
uint256 proposalId = voteAndCreateProposal(proposalAddress);
|
||||||
|
|
||||||
|
governance.execute(proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitUntilExecutable() internal {
|
||||||
|
vm.warp(block.timestamp + PROPOSAL_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
function voteAndCreateProposal(address proposalAddress) public returns (uint256) {
|
||||||
|
retrieveAndLockBalance(TEST_PRIVATE_KEY_ONE, TEST_ADDRESS_ONE, PROPOSAL_THRESHOLD);
|
||||||
|
retrieveAndLockBalance(TEST_PRIVATE_KEY_TWO, TEST_ADDRESS_TWO, 1 ether);
|
||||||
|
|
||||||
|
/* ----------PROPOSER------------ */
|
||||||
|
vm.startPrank(TEST_ADDRESS_ONE);
|
||||||
|
|
||||||
|
uint256 proposalId = IGovernance(_governanceAddress).propose(proposalAddress, PROPOSAL_DESCRIPTION);
|
||||||
|
|
||||||
|
// TIME-TRAVEL
|
||||||
|
vm.warp(block.timestamp + 6 hours);
|
||||||
|
|
||||||
|
IGovernance(_governanceAddress).castVote(proposalId, true);
|
||||||
|
|
||||||
|
vm.stopPrank();
|
||||||
|
/* ------------------------------ */
|
||||||
|
|
||||||
|
/* -------------VOTER-------------*/
|
||||||
|
vm.startPrank(TEST_ADDRESS_TWO);
|
||||||
|
IGovernance(_governanceAddress).castVote(proposalId, true);
|
||||||
|
vm.stopPrank();
|
||||||
|
/* ------------------------------ */
|
||||||
|
|
||||||
|
return proposalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function retrieveAndLockBalance(uint256 privateKey, address voter, uint256 amount) internal {
|
||||||
|
uint256 lockTimestamp = block.timestamp + PROPOSAL_DURATION;
|
||||||
|
|
||||||
|
bytes32 messageHash = keccak256(
|
||||||
|
abi.encodePacked(
|
||||||
|
PERMIT_FUNC_SELECTOR,
|
||||||
|
EIP712_DOMAIN,
|
||||||
|
keccak256(abi.encode(PERMIT_TYPEHASH, voter, _governanceAddress, amount, 0, lockTimestamp))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* ----------GOVERNANCE------- */
|
||||||
|
vm.startPrank(_governanceAddress);
|
||||||
|
IERC20(_tokenAddress).transfer(voter, amount);
|
||||||
|
vm.stopPrank();
|
||||||
|
/* ----------------------------*/
|
||||||
|
|
||||||
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash);
|
||||||
|
|
||||||
|
/* ----------VOTER------------ */
|
||||||
|
vm.startPrank(voter);
|
||||||
|
IGovernance(_governanceAddress).lock(voter, amount, lockTimestamp, v, r, s);
|
||||||
|
vm.stopPrank();
|
||||||
|
/* ----------------------------*/
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user