291 lines
9.9 KiB
Solidity
291 lines
9.9 KiB
Solidity
// 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;
|
|
}
|
|
}
|