Contract Source Code:
File 1 of 1 : Moloch
pragma solidity ^0.5.3;
contract Moloch {
using SafeMath for uint256;
/***************
GLOBAL CONSTANTS
***************/
uint256 public periodDuration; // default = 17280 = 4.8 hours in seconds (5 periods per day)
uint256 public votingPeriodLength; // default = 35 periods (7 days)
uint256 public gracePeriodLength; // default = 35 periods (7 days)
uint256 public abortWindow; // default = 5 periods (1 day)
uint256 public proposalDeposit; // default = 10 ETH (~$1,000 worth of ETH at contract deployment)
uint256 public dilutionBound; // default = 3 - maximum multiplier a YES voter will be obligated to pay in case of mass ragequit
uint256 public processingReward; // default = 0.1 - amount of ETH to give to whoever processes a proposal
uint256 public summoningTime; // needed to determine the current period
string public summoningStamp; // string stamp recording details of Moloch Summoning
IERC20 public approvedToken; // approved token contract reference; default = wETH
GuildBank public guildBank; // guild bank contract reference
// HARD-CODED LIMITS
// These numbers are quite arbitrary; they are small enough to avoid overflows when doing calculations
// with periods or shares, yet big enough to not limit reasonable use cases.
uint256 constant MAX_VOTING_PERIOD_LENGTH = 10**18; // maximum length of voting period
uint256 constant MAX_GRACE_PERIOD_LENGTH = 10**18; // maximum length of grace period
uint256 constant MAX_DILUTION_BOUND = 10**18; // maximum dilution bound
uint256 constant MAX_NUMBER_OF_SHARES = 10**18; // maximum number of shares that can be minted
/***************
EVENTS
***************/
event SubmitProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, address indexed applicant, uint256 tokenTribute, uint256 sharesRequested);
event SubmitVote(uint256 indexed proposalIndex, address indexed delegateKey, address indexed memberAddress, uint8 uintVote);
event ProcessProposal(uint256 indexed proposalIndex, address indexed applicant, address indexed memberAddress, uint256 tokenTribute, uint256 sharesRequested, bool didPass);
event Ragequit(address indexed memberAddress, uint256 sharesToBurn);
event Abort(uint256 indexed proposalIndex, address applicantAddress);
event UpdateDelegateKey(address indexed memberAddress, address newDelegateKey);
event SummonComplete(address indexed summoner, uint256 shares);
/******************
INTERNAL ACCOUNTING
******************/
uint256 public totalShares = 0; // total shares across all members
uint256 public totalSharesRequested = 0; // total shares that have been requested in unprocessed proposals
enum Vote {
Null, // default value, counted as abstention
Yes,
No
}
struct Member {
address delegateKey; // the key responsible for submitting proposals and voting - defaults to member address unless updated
uint256 shares; // the # of shares assigned to this member
bool exists; // always true once a member has been created
uint256 highestIndexYesVote; // highest proposal index # on which the member voted YES
}
struct Proposal {
address proposer; // the member who submitted the proposal
address applicant; // the applicant who wishes to become a member - this key will be used for withdrawals
uint256 sharesRequested; // the # of shares the applicant is requesting
uint256 startingPeriod; // the period in which voting can start for this proposal
uint256 yesVotes; // the total number of YES votes for this proposal
uint256 noVotes; // the total number of NO votes for this proposal
bool processed; // true only if the proposal has been processed
bool didPass; // true only if the proposal passed
bool aborted; // true only if applicant calls "abort" fn before end of voting period
uint256 tokenTribute; // amount of tokens offered as tribute
string details; // proposal details - could be IPFS hash, plaintext, or JSON
uint256 maxTotalSharesAtYesVote; // the maximum # of total shares encountered at a yes vote on this proposal
mapping (address => Vote) votesByMember; // the votes on this proposal by each member
}
mapping (address => Member) public members;
mapping (address => address) public memberAddressByDelegateKey;
Proposal[] public proposalQueue;
/********
MODIFIERS
********/
modifier onlyMember {
require(members[msg.sender].shares > 0, "Moloch::onlyMember - not a member");
_;
}
modifier onlyDelegate {
require(members[memberAddressByDelegateKey[msg.sender]].shares > 0, "Moloch::onlyDelegate - not a delegate");
_;
}
/********
FUNCTIONS
********/
constructor(
address summoner,
address _approvedToken,
uint256 _periodDuration,
uint256 _votingPeriodLength,
uint256 _gracePeriodLength,
uint256 _abortWindow,
uint256 _proposalDeposit,
uint256 _dilutionBound,
uint256 _processingReward,
string memory _summoningStamp
) public {
require(summoner != address(0), "Moloch::constructor - summoner cannot be 0");
require(_approvedToken != address(0), "Moloch::constructor - _approvedToken cannot be 0");
require(_periodDuration > 0, "Moloch::constructor - _periodDuration cannot be 0");
require(_votingPeriodLength > 0, "Moloch::constructor - _votingPeriodLength cannot be 0");
require(_votingPeriodLength <= MAX_VOTING_PERIOD_LENGTH, "Moloch::constructor - _votingPeriodLength exceeds limit");
require(_gracePeriodLength <= MAX_GRACE_PERIOD_LENGTH, "Moloch::constructor - _gracePeriodLength exceeds limit");
require(_abortWindow > 0, "Moloch::constructor - _abortWindow cannot be 0");
require(_abortWindow <= _votingPeriodLength, "Moloch::constructor - _abortWindow must be smaller than or equal to _votingPeriodLength");
require(_dilutionBound > 0, "Moloch::constructor - _dilutionBound cannot be 0");
require(_dilutionBound <= MAX_DILUTION_BOUND, "Moloch::constructor - _dilutionBound exceeds limit");
require(_proposalDeposit >= _processingReward, "Moloch::constructor - _proposalDeposit cannot be smaller than _processingReward");
approvedToken = IERC20(_approvedToken);
guildBank = new GuildBank(_approvedToken);
periodDuration = _periodDuration;
votingPeriodLength = _votingPeriodLength;
gracePeriodLength = _gracePeriodLength;
abortWindow = _abortWindow;
proposalDeposit = _proposalDeposit;
dilutionBound = _dilutionBound;
processingReward = _processingReward;
summoningStamp = _summoningStamp;
summoningTime = now;
members[summoner] = Member(summoner, 1, true, 0);
memberAddressByDelegateKey[summoner] = summoner;
totalShares = 1;
emit SummonComplete(summoner, 1);
}
/*****************
PROPOSAL FUNCTIONS
*****************/
function submitProposal(
address applicant,
uint256 tokenTribute,
uint256 sharesRequested,
string memory details
)
public
onlyDelegate
{
require(applicant != address(0), "Moloch::submitProposal - applicant cannot be 0");
// Make sure we won't run into overflows when doing calculations with shares.
// Note that totalShares + totalSharesRequested + sharesRequested is an upper bound
// on the number of shares that can exist until this proposal has been processed.
require(totalShares.add(totalSharesRequested).add(sharesRequested) <= MAX_NUMBER_OF_SHARES, "Moloch::submitProposal - too many shares requested");
totalSharesRequested = totalSharesRequested.add(sharesRequested);
address memberAddress = memberAddressByDelegateKey[msg.sender];
// collect proposal deposit from proposer and store it in the Moloch until the proposal is processed
require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed");
// collect tribute from applicant and store it in the Moloch until the proposal is processed
require(approvedToken.transferFrom(applicant, address(this), tokenTribute), "Moloch::submitProposal - tribute token transfer failed");
// compute startingPeriod for proposal
uint256 startingPeriod = max(
getCurrentPeriod(),
proposalQueue.length == 0 ? 0 : proposalQueue[proposalQueue.length.sub(1)].startingPeriod
).add(1);
// create proposal ...
Proposal memory proposal = Proposal({
proposer: memberAddress,
applicant: applicant,
sharesRequested: sharesRequested,
startingPeriod: startingPeriod,
yesVotes: 0,
noVotes: 0,
processed: false,
didPass: false,
aborted: false,
tokenTribute: tokenTribute,
details: details,
maxTotalSharesAtYesVote: 0
});
// ... and append it to the queue
proposalQueue.push(proposal);
uint256 proposalIndex = proposalQueue.length.sub(1);
emit SubmitProposal(proposalIndex, msg.sender, memberAddress, applicant, tokenTribute, sharesRequested);
}
function submitVote(uint256 proposalIndex, uint8 uintVote) public onlyDelegate {
address memberAddress = memberAddressByDelegateKey[msg.sender];
Member storage member = members[memberAddress];
require(proposalIndex < proposalQueue.length, "Moloch::submitVote - proposal does not exist");
Proposal storage proposal = proposalQueue[proposalIndex];
require(uintVote < 3, "Moloch::submitVote - uintVote must be less than 3");
Vote vote = Vote(uintVote);
require(getCurrentPeriod() >= proposal.startingPeriod, "Moloch::submitVote - voting period has not started");
require(!hasVotingPeriodExpired(proposal.startingPeriod), "Moloch::submitVote - proposal voting period has expired");
require(proposal.votesByMember[memberAddress] == Vote.Null, "Moloch::submitVote - member has already voted on this proposal");
require(vote == Vote.Yes || vote == Vote.No, "Moloch::submitVote - vote must be either Yes or No");
require(!proposal.aborted, "Moloch::submitVote - proposal has been aborted");
// store vote
proposal.votesByMember[memberAddress] = vote;
// count vote
if (vote == Vote.Yes) {
proposal.yesVotes = proposal.yesVotes.add(member.shares);
// set highest index (latest) yes vote - must be processed for member to ragequit
if (proposalIndex > member.highestIndexYesVote) {
member.highestIndexYesVote = proposalIndex;
}
// set maximum of total shares encountered at a yes vote - used to bound dilution for yes voters
if (totalShares > proposal.maxTotalSharesAtYesVote) {
proposal.maxTotalSharesAtYesVote = totalShares;
}
} else if (vote == Vote.No) {
proposal.noVotes = proposal.noVotes.add(member.shares);
}
emit SubmitVote(proposalIndex, msg.sender, memberAddress, uintVote);
}
function processProposal(uint256 proposalIndex) public {
require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist");
Proposal storage proposal = proposalQueue[proposalIndex];
require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed");
require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed");
require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed");
proposal.processed = true;
totalSharesRequested = totalSharesRequested.sub(proposal.sharesRequested);
bool didPass = proposal.yesVotes > proposal.noVotes;
// Make the proposal fail if the dilutionBound is exceeded
if (totalShares.mul(dilutionBound) < proposal.maxTotalSharesAtYesVote) {
didPass = false;
}
// PROPOSAL PASSED
if (didPass && !proposal.aborted) {
proposal.didPass = true;
// if the applicant is already a member, add to their existing shares
if (members[proposal.applicant].exists) {
members[proposal.applicant].shares = members[proposal.applicant].shares.add(proposal.sharesRequested);
// the applicant is a new member, create a new record for them
} else {
// if the applicant address is already taken by a member's delegateKey, reset it to their member address
if (members[memberAddressByDelegateKey[proposal.applicant]].exists) {
address memberToOverride = memberAddressByDelegateKey[proposal.applicant];
memberAddressByDelegateKey[memberToOverride] = memberToOverride;
members[memberToOverride].delegateKey = memberToOverride;
}
// use applicant address as delegateKey by default
members[proposal.applicant] = Member(proposal.applicant, proposal.sharesRequested, true, 0);
memberAddressByDelegateKey[proposal.applicant] = proposal.applicant;
}
// mint new shares
totalShares = totalShares.add(proposal.sharesRequested);
// transfer tokens to guild bank
require(
approvedToken.transfer(address(guildBank), proposal.tokenTribute),
"Moloch::processProposal - token transfer to guild bank failed"
);
// PROPOSAL FAILED OR ABORTED
} else {
// return all tokens to the applicant
require(
approvedToken.transfer(proposal.applicant, proposal.tokenTribute),
"Moloch::processProposal - failing vote token transfer failed"
);
}
// send msg.sender the processingReward
require(
approvedToken.transfer(msg.sender, processingReward),
"Moloch::processProposal - failed to send processing reward to msg.sender"
);
// return deposit to proposer (subtract processing reward)
require(
approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)),
"Moloch::processProposal - failed to return proposal deposit to proposer"
);
emit ProcessProposal(
proposalIndex,
proposal.applicant,
proposal.proposer,
proposal.tokenTribute,
proposal.sharesRequested,
didPass
);
}
function ragequit(uint256 sharesToBurn) public onlyMember {
uint256 initialTotalShares = totalShares;
Member storage member = members[msg.sender];
require(member.shares >= sharesToBurn, "Moloch::ragequit - insufficient shares");
require(canRagequit(member.highestIndexYesVote), "Moloch::ragequit - cant ragequit until highest index proposal member voted YES on is processed");
// burn shares
member.shares = member.shares.sub(sharesToBurn);
totalShares = totalShares.sub(sharesToBurn);
// instruct guildBank to transfer fair share of tokens to the ragequitter
require(
guildBank.withdraw(msg.sender, sharesToBurn, initialTotalShares),
"Moloch::ragequit - withdrawal of tokens from guildBank failed"
);
emit Ragequit(msg.sender, sharesToBurn);
}
function abort(uint256 proposalIndex) public {
require(proposalIndex < proposalQueue.length, "Moloch::abort - proposal does not exist");
Proposal storage proposal = proposalQueue[proposalIndex];
require(msg.sender == proposal.applicant, "Moloch::abort - msg.sender must be applicant");
require(getCurrentPeriod() < proposal.startingPeriod.add(abortWindow), "Moloch::abort - abort window must not have passed");
require(!proposal.aborted, "Moloch::abort - proposal must not have already been aborted");
uint256 tokensToAbort = proposal.tokenTribute;
proposal.tokenTribute = 0;
proposal.aborted = true;
// return all tokens to the applicant
require(
approvedToken.transfer(proposal.applicant, tokensToAbort),
"Moloch::processProposal - failed to return tribute to applicant"
);
emit Abort(proposalIndex, msg.sender);
}
function updateDelegateKey(address newDelegateKey) public onlyMember {
require(newDelegateKey != address(0), "Moloch::updateDelegateKey - newDelegateKey cannot be 0");
// skip checks if member is setting the delegate key to their member address
if (newDelegateKey != msg.sender) {
require(!members[newDelegateKey].exists, "Moloch::updateDelegateKey - cant overwrite existing members");
require(!members[memberAddressByDelegateKey[newDelegateKey]].exists, "Moloch::updateDelegateKey - cant overwrite existing delegate keys");
}
Member storage member = members[msg.sender];
memberAddressByDelegateKey[member.delegateKey] = address(0);
memberAddressByDelegateKey[newDelegateKey] = msg.sender;
member.delegateKey = newDelegateKey;
emit UpdateDelegateKey(msg.sender, newDelegateKey);
}
/***************
GETTER FUNCTIONS
***************/
function max(uint256 x, uint256 y) internal pure returns (uint256) {
return x >= y ? x : y;
}
function getCurrentPeriod() public view returns (uint256) {
return now.sub(summoningTime).div(periodDuration);
}
function getProposalQueueLength() public view returns (uint256) {
return proposalQueue.length;
}
// can only ragequit if the latest proposal you voted YES on has been processed
function canRagequit(uint256 highestIndexYesVote) public view returns (bool) {
require(highestIndexYesVote < proposalQueue.length, "Moloch::canRagequit - proposal does not exist");
return proposalQueue[highestIndexYesVote].processed;
}
function hasVotingPeriodExpired(uint256 startingPeriod) public view returns (bool) {
return getCurrentPeriod() >= startingPeriod.add(votingPeriodLength);
}
function getMemberProposalVote(address memberAddress, uint256 proposalIndex) public view returns (Vote) {
require(members[memberAddress].exists, "Moloch::getMemberProposalVote - member doesn't exist");
require(proposalIndex < proposalQueue.length, "Moloch::getMemberProposalVote - proposal doesn't exist");
return proposalQueue[proposalIndex].votesByMember[memberAddress];
}
}
interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address who) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract Ownable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor () internal {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
/**
* @return the address of the owner.
*/
function owner() public view returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner());
_;
}
/**
* @return true if `msg.sender` is the owner of the contract.
*/
function isOwner() public view returns (bool) {
return msg.sender == _owner;
}
/**
* @dev Allows the current owner to relinquish control of the contract.
* @notice Renouncing to ownership will leave the contract without an owner.
* It will not be possible to call the functions with the `onlyOwner`
* modifier anymore.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
/**
* @dev Transfers control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0));
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
contract GuildBank is Ownable {
using SafeMath for uint256;
IERC20 public approvedToken; // approved token contract reference
event Withdrawal(address indexed receiver, uint256 amount);
constructor(address approvedTokenAddress) public {
approvedToken = IERC20(approvedTokenAddress);
}
function withdraw(address receiver, uint256 shares, uint256 totalShares) public onlyOwner returns (bool) {
uint256 amount = approvedToken.balanceOf(address(this)).mul(shares).div(totalShares);
emit Withdrawal(receiver, amount);
return approvedToken.transfer(receiver, amount);
}
}
library SafeMath {
/**
* @dev Multiplies two unsigned integers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/**
* @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// Solidity only automatically asserts when dividing by 0
require(b > 0);
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/**
* @dev Adds two unsigned integers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/**
* @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}