diff --git a/contracts/moloch/MolochSummoner.sol b/contracts/moloch/MolochSummoner.sol new file mode 100644 index 0000000..3e5869e --- /dev/null +++ b/contracts/moloch/MolochSummoner.sol @@ -0,0 +1,896 @@ +pragma solidity 0.5.3; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + constructor () internal { + _status = _NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and make it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + // On the first call to nonReentrant, _notEntered will be true + require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + + _; + + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } +} + +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); +} + +library SafeMath { + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0) { + return 0; + } + + uint256 c = a * b; + require(c / a == b); + + return c; + } + + function div(uint256 a, uint256 b) internal pure returns (uint256) { + + require(b > 0); + uint256 c = a / b; + + return c; + } + + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + require(b <= a); + uint256 c = a - b; + + return c; + } + + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a); + + return c; + } +} + +contract Moloch is ReentrancyGuard { + 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 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 + bool private initialized; // internally tracks deployment under eip-1167 proxy pattern + + address public depositToken; // deposit token contract reference; default = wETH + + // 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_AND_LOOT = 10**18; // maximum number of shares that can be minted + uint256 constant MAX_TOKEN_WHITELIST_COUNT = 400; // maximum number of whitelisted tokens + uint256 constant MAX_TOKEN_GUILDBANK_COUNT = 200; // maximum number of tokens with non-zero balance in guildbank + + // *************** + // EVENTS + // *************** + event SubmitProposal(address indexed applicant, uint256 sharesRequested, uint256 lootRequested, uint256 tributeOffered, address tributeToken, uint256 paymentRequested, address paymentToken, string details, bool[6] flags, uint256 proposalId, address indexed delegateKey, address indexed memberAddress); + event SponsorProposal(address indexed delegateKey, address indexed memberAddress, uint256 proposalId, uint256 proposalIndex, uint256 startingPeriod); + event SubmitVote(uint256 proposalId, uint256 indexed proposalIndex, address indexed delegateKey, address indexed memberAddress, uint8 uintVote); + event ProcessProposal(uint256 indexed proposalIndex, uint256 indexed proposalId, bool didPass); + event ProcessWhitelistProposal(uint256 indexed proposalIndex, uint256 indexed proposalId, bool didPass); + event ProcessGuildKickProposal(uint256 indexed proposalIndex, uint256 indexed proposalId, bool didPass); + event Ragequit(address indexed memberAddress, uint256 sharesToBurn, uint256 lootToBurn); + event TokensCollected(address indexed token, uint256 amountToCollect); + event CancelProposal(uint256 indexed proposalId, address applicantAddress); + event UpdateDelegateKey(address indexed memberAddress, address newDelegateKey); + event Withdraw(address indexed memberAddress, address token, uint256 amount); + + // ******************* + // INTERNAL ACCOUNTING + // ******************* + uint256 public proposalCount = 0; // total proposals submitted + uint256 public totalShares = 0; // total shares across all members + uint256 public totalLoot = 0; // total loot across all members + + uint256 public totalGuildBankTokens = 0; // total tokens with non-zero balance in guild bank + + address public constant GUILD = address(0xdead); + address public constant ESCROW = address(0xbeef); + address public constant TOTAL = address(0xbabe); + mapping (address => mapping(address => uint256)) public userTokenBalances; // userTokenBalances[userAddress][tokenAddress] + + 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 voting shares assigned to this member + uint256 loot; // the loot amount available to this member (combined with shares on ragequit) + bool exists; // always true once a member has been created + uint256 highestIndexYesVote; // highest proposal index # on which the member voted YES + uint256 jailed; // set to proposalIndex of a passing guild kick proposal for this member, prevents voting on and sponsoring proposals + } + + struct Proposal { + address applicant; // the applicant who wishes to become a member - this key will be used for withdrawals (doubles as guild kick target for gkick proposals) + address proposer; // the account that submitted the proposal (can be non-member) + address sponsor; // the member that sponsored the proposal (moving it into the queue) + uint256 sharesRequested; // the # of shares the applicant is requesting + uint256 lootRequested; // the amount of loot the applicant is requesting + uint256 tributeOffered; // amount of tokens offered as tribute + address tributeToken; // tribute token contract reference + uint256 paymentRequested; // amount of tokens requested as payment + address paymentToken; // payment token contract reference + 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[6] flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick] + string details; // proposal details - could be IPFS hash, plaintext, or JSON + uint256 maxTotalSharesAndLootAtYesVote; // 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 => bool) public tokenWhitelist; + address[] public approvedTokens; + + mapping(address => bool) public proposedToWhitelist; + mapping(address => bool) public proposedToKick; + + mapping(address => Member) public members; + mapping(address => address) public memberAddressByDelegateKey; + + mapping(uint256 => Proposal) public proposals; + + uint256[] public proposalQueue; + + modifier onlyMember { + require(members[msg.sender].shares > 0 || members[msg.sender].loot > 0, "not a member"); + _; + } + + modifier onlyShareholder { + require(members[msg.sender].shares > 0, "not a shareholder"); + _; + } + + modifier onlyDelegate { + require(members[memberAddressByDelegateKey[msg.sender]].shares > 0, "not a delegate"); + _; + } + + function init( + address[] calldata _summoner, + address[] calldata _approvedTokens, + uint256 _periodDuration, + uint256 _votingPeriodLength, + uint256 _gracePeriodLength, + uint256 _proposalDeposit, + uint256 _dilutionBound, + uint256 _processingReward, + uint256[] calldata _summonerShares + ) external { + require(!initialized, "initialized"); + require(_summoner.length == _summonerShares.length, "summoner length mismatches summonerShares"); + require(_periodDuration > 0, "_periodDuration cannot be 0"); + require(_votingPeriodLength > 0, "_votingPeriodLength cannot be 0"); + require(_votingPeriodLength <= MAX_VOTING_PERIOD_LENGTH, "_votingPeriodLength exceeds limit"); + require(_gracePeriodLength <= MAX_GRACE_PERIOD_LENGTH, "_gracePeriodLength exceeds limit"); + require(_dilutionBound > 0, "_dilutionBound cannot be 0"); + require(_dilutionBound <= MAX_DILUTION_BOUND, "_dilutionBound exceeds limit"); + require(_approvedTokens.length > 0, "need at least one approved token"); + require(_approvedTokens.length <= MAX_TOKEN_WHITELIST_COUNT, "too many tokens"); + require(_proposalDeposit >= _processingReward, "_proposalDeposit cannot be smaller than _processingReward"); + + depositToken = _approvedTokens[0]; + + for (uint256 i = 0; i < _summoner.length; i++) { + require(_summoner[i] != address(0), "summoner cannot be 0"); + members[_summoner[i]] = Member(_summoner[i], _summonerShares[i], 0, true, 0, 0); + memberAddressByDelegateKey[_summoner[i]] = _summoner[i]; + totalShares = totalShares.add(_summonerShares[i]); + } + + require(totalShares <= MAX_NUMBER_OF_SHARES_AND_LOOT, "too many shares requested"); + + for (uint256 i = 0; i < _approvedTokens.length; i++) { + require(_approvedTokens[i] != address(0), "_approvedToken cannot be 0"); + require(!tokenWhitelist[_approvedTokens[i]], "duplicate approved token"); + tokenWhitelist[_approvedTokens[i]] = true; + approvedTokens.push(_approvedTokens[i]); + } + + periodDuration = _periodDuration; + votingPeriodLength = _votingPeriodLength; + gracePeriodLength = _gracePeriodLength; + proposalDeposit = _proposalDeposit; + dilutionBound = _dilutionBound; + processingReward = _processingReward; + summoningTime = now; + initialized = true; + } + + /***************** + PROPOSAL FUNCTIONS + *****************/ + function submitProposal( + address applicant, + uint256 sharesRequested, + uint256 lootRequested, + uint256 tributeOffered, + address tributeToken, + uint256 paymentRequested, + address paymentToken, + string memory details + ) public nonReentrant returns (uint256 proposalId) { + require(sharesRequested.add(lootRequested) <= MAX_NUMBER_OF_SHARES_AND_LOOT, "too many shares requested"); + require(tokenWhitelist[tributeToken], "tributeToken is not whitelisted"); + require(tokenWhitelist[paymentToken], "payment is not whitelisted"); + require(applicant != address(0), "applicant cannot be 0"); + require(applicant != GUILD && applicant != ESCROW && applicant != TOTAL, "applicant address cannot be reserved"); + require(members[applicant].jailed == 0, "proposal applicant must not be jailed"); + + if (tributeOffered > 0 && userTokenBalances[GUILD][tributeToken] == 0) { + require(totalGuildBankTokens < MAX_TOKEN_GUILDBANK_COUNT, 'cannot submit more tribute proposals for new tokens - guildbank is full'); + } + + // collect tribute from proposer and store it in the Moloch until the proposal is processed + require(IERC20(tributeToken).transferFrom(msg.sender, address(this), tributeOffered), "tribute token transfer failed"); + unsafeAddToBalance(ESCROW, tributeToken, tributeOffered); + + bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick] + + _submitProposal(applicant, sharesRequested, lootRequested, tributeOffered, tributeToken, paymentRequested, paymentToken, details, flags); + return proposalCount - 1; // return proposalId - contracts calling submit might want it + } + + function submitWhitelistProposal(address tokenToWhitelist, string memory details) public nonReentrant returns (uint256 proposalId) { + require(tokenToWhitelist != address(0), "must provide token address"); + require(!tokenWhitelist[tokenToWhitelist], "cannot already have whitelisted the token"); + require(approvedTokens.length < MAX_TOKEN_WHITELIST_COUNT, "cannot submit more whitelist proposals"); + + bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick] + flags[4] = true; // whitelist + + _submitProposal(address(0), 0, 0, 0, tokenToWhitelist, 0, address(0), details, flags); + return proposalCount - 1; + } + + function submitGuildKickProposal(address memberToKick, string memory details) public nonReentrant returns (uint256 proposalId) { + Member memory member = members[memberToKick]; + + require(member.shares > 0 || member.loot > 0, "member must have at least one share or one loot"); + require(members[memberToKick].jailed == 0, "member must not already be jailed"); + + bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick] + flags[5] = true; // guild kick + + _submitProposal(memberToKick, 0, 0, 0, address(0), 0, address(0), details, flags); + return proposalCount - 1; + } + + function _submitProposal( + address applicant, + uint256 sharesRequested, + uint256 lootRequested, + uint256 tributeOffered, + address tributeToken, + uint256 paymentRequested, + address paymentToken, + string memory details, + bool[6] memory flags + ) internal { + Proposal memory proposal = Proposal({ + applicant : applicant, + proposer : msg.sender, + sponsor : address(0), + sharesRequested : sharesRequested, + lootRequested : lootRequested, + tributeOffered : tributeOffered, + tributeToken : tributeToken, + paymentRequested : paymentRequested, + paymentToken : paymentToken, + startingPeriod : 0, + yesVotes : 0, + noVotes : 0, + flags : flags, + details : details, + maxTotalSharesAndLootAtYesVote : 0 + }); + + proposals[proposalCount] = proposal; + address memberAddress = memberAddressByDelegateKey[msg.sender]; + // NOTE: argument order matters, avoid stack too deep + emit SubmitProposal(applicant, sharesRequested, lootRequested, tributeOffered, tributeToken, paymentRequested, paymentToken, details, flags, proposalCount, msg.sender, memberAddress); + proposalCount += 1; + } + + function sponsorProposal(uint256 proposalId) public nonReentrant onlyDelegate { + // collect proposal deposit from sponsor and store it in the Moloch until the proposal is processed + require(IERC20(depositToken).transferFrom(msg.sender, address(this), proposalDeposit), "proposal deposit token transfer failed"); + unsafeAddToBalance(ESCROW, depositToken, proposalDeposit); + + Proposal storage proposal = proposals[proposalId]; + + require(proposal.proposer != address(0), 'proposal must have been proposed'); + require(!proposal.flags[0], "proposal has already been sponsored"); + require(!proposal.flags[3], "proposal has been cancelled"); + require(members[proposal.applicant].jailed == 0, "proposal applicant must not be jailed"); + + if (proposal.tributeOffered > 0 && userTokenBalances[GUILD][proposal.tributeToken] == 0) { + require(totalGuildBankTokens < MAX_TOKEN_GUILDBANK_COUNT, 'cannot sponsor more tribute proposals for new tokens - guildbank is full'); + } + + // whitelist proposal + if (proposal.flags[4]) { + require(!tokenWhitelist[address(proposal.tributeToken)], "cannot already have whitelisted the token"); + require(!proposedToWhitelist[address(proposal.tributeToken)], 'already proposed to whitelist'); + require(approvedTokens.length < MAX_TOKEN_WHITELIST_COUNT, "cannot sponsor more whitelist proposals"); + proposedToWhitelist[address(proposal.tributeToken)] = true; + + // guild kick proposal + } else if (proposal.flags[5]) { + require(!proposedToKick[proposal.applicant], 'already proposed to kick'); + proposedToKick[proposal.applicant] = true; + } + + // compute startingPeriod for proposal + uint256 startingPeriod = max( + getCurrentPeriod(), + proposalQueue.length == 0 ? 0 : proposals[proposalQueue[proposalQueue.length.sub(1)]].startingPeriod + ).add(1); + + proposal.startingPeriod = startingPeriod; + + address memberAddress = memberAddressByDelegateKey[msg.sender]; + proposal.sponsor = memberAddress; + + proposal.flags[0] = true; // sponsored + + // append proposal to the queue + proposalQueue.push(proposalId); + + emit SponsorProposal(msg.sender, memberAddress, proposalId, proposalQueue.length.sub(1), startingPeriod); + } + + // NOTE: In MolochV2 proposalIndex !== proposalId + function submitVote(uint256 proposalIndex, uint8 uintVote) public nonReentrant onlyDelegate { + address memberAddress = memberAddressByDelegateKey[msg.sender]; + Member storage member = members[memberAddress]; + + require(proposalIndex < proposalQueue.length, "proposal does not exist"); + Proposal storage proposal = proposals[proposalQueue[proposalIndex]]; + + require(uintVote < 3, "must be less than 3"); + Vote vote = Vote(uintVote); + + require(getCurrentPeriod() >= proposal.startingPeriod, "voting period has not started"); + require(!hasVotingPeriodExpired(proposal.startingPeriod), "proposal voting period has expired"); + require(proposal.votesByMember[memberAddress] == Vote.Null, "member has already voted"); + require(vote == Vote.Yes || vote == Vote.No, "vote must be either Yes or No"); + + proposal.votesByMember[memberAddress] = 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.add(totalLoot) > proposal.maxTotalSharesAndLootAtYesVote) { + proposal.maxTotalSharesAndLootAtYesVote = totalShares.add(totalLoot); + } + + } else if (vote == Vote.No) { + proposal.noVotes = proposal.noVotes.add(member.shares); + } + + // NOTE: subgraph indexes by proposalId not proposalIndex since proposalIndex isn't set untill it's been sponsored but proposal is created on submission + emit SubmitVote(proposalQueue[proposalIndex], proposalIndex, msg.sender, memberAddress, uintVote); + } + + function processProposal(uint256 proposalIndex) public nonReentrant { + _validateProposalForProcessing(proposalIndex); + + uint256 proposalId = proposalQueue[proposalIndex]; + Proposal storage proposal = proposals[proposalId]; + + require(!proposal.flags[4] && !proposal.flags[5], "must be a standard proposal"); + + proposal.flags[1] = true; // processed + + bool didPass = _didPass(proposalIndex); + + // Make the proposal fail if the new total number of shares and loot exceeds the limit + if (totalShares.add(totalLoot).add(proposal.sharesRequested).add(proposal.lootRequested) > MAX_NUMBER_OF_SHARES_AND_LOOT) { + didPass = false; + } + + // Make the proposal fail if it is requesting more tokens as payment than the available guild bank balance + if (proposal.paymentRequested > userTokenBalances[GUILD][proposal.paymentToken]) { + didPass = false; + } + + // Make the proposal fail if it would result in too many tokens with non-zero balance in guild bank + if (proposal.tributeOffered > 0 && userTokenBalances[GUILD][proposal.tributeToken] == 0 && totalGuildBankTokens >= MAX_TOKEN_GUILDBANK_COUNT) { + didPass = false; + } + + // PROPOSAL PASSED + if (didPass) { + proposal.flags[2] = true; // didPass + + // if the applicant is already a member, add to their existing shares & loot + if (members[proposal.applicant].exists) { + members[proposal.applicant].shares = members[proposal.applicant].shares.add(proposal.sharesRequested); + members[proposal.applicant].loot = members[proposal.applicant].loot.add(proposal.lootRequested); + + // 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, proposal.lootRequested, true, 0, 0); + memberAddressByDelegateKey[proposal.applicant] = proposal.applicant; + } + + // mint new shares & loot + totalShares = totalShares.add(proposal.sharesRequested); + totalLoot = totalLoot.add(proposal.lootRequested); + + // if the proposal tribute is the first tokens of its kind to make it into the guild bank, increment total guild bank tokens + if (userTokenBalances[GUILD][proposal.tributeToken] == 0 && proposal.tributeOffered > 0) { + totalGuildBankTokens += 1; + } + + unsafeInternalTransfer(ESCROW, GUILD, proposal.tributeToken, proposal.tributeOffered); + unsafeInternalTransfer(GUILD, proposal.applicant, proposal.paymentToken, proposal.paymentRequested); + + // if the proposal spends 100% of guild bank balance for a token, decrement total guild bank tokens + if (userTokenBalances[GUILD][proposal.paymentToken] == 0 && proposal.paymentRequested > 0) { + totalGuildBankTokens -= 1; + } + + // PROPOSAL FAILED + } else { + // return all tokens to the proposer (not the applicant, because funds come from proposer) + unsafeInternalTransfer(ESCROW, proposal.proposer, proposal.tributeToken, proposal.tributeOffered); + } + + _returnDeposit(proposal.sponsor); + + emit ProcessProposal(proposalIndex, proposalId, didPass); + } + + function processWhitelistProposal(uint256 proposalIndex) public nonReentrant { + _validateProposalForProcessing(proposalIndex); + + uint256 proposalId = proposalQueue[proposalIndex]; + Proposal storage proposal = proposals[proposalId]; + + require(proposal.flags[4], "must be a whitelist proposal"); + + proposal.flags[1] = true; // processed + + bool didPass = _didPass(proposalIndex); + + if (approvedTokens.length >= MAX_TOKEN_WHITELIST_COUNT) { + didPass = false; + } + + if (didPass) { + proposal.flags[2] = true; // didPass + + tokenWhitelist[address(proposal.tributeToken)] = true; + approvedTokens.push(proposal.tributeToken); + } + + proposedToWhitelist[address(proposal.tributeToken)] = false; + + _returnDeposit(proposal.sponsor); + + emit ProcessWhitelistProposal(proposalIndex, proposalId, didPass); + } + + function processGuildKickProposal(uint256 proposalIndex) public nonReentrant { + _validateProposalForProcessing(proposalIndex); + + uint256 proposalId = proposalQueue[proposalIndex]; + Proposal storage proposal = proposals[proposalId]; + + require(proposal.flags[5], "must be a guild kick proposal"); + + proposal.flags[1] = true; // processed + + bool didPass = _didPass(proposalIndex); + + if (didPass) { + proposal.flags[2] = true; // didPass + Member storage member = members[proposal.applicant]; + member.jailed = proposalIndex; + + // transfer shares to loot + member.loot = member.loot.add(member.shares); + totalShares = totalShares.sub(member.shares); + totalLoot = totalLoot.add(member.shares); + member.shares = 0; // revoke all shares + } + + proposedToKick[proposal.applicant] = false; + + _returnDeposit(proposal.sponsor); + + emit ProcessGuildKickProposal(proposalIndex, proposalId, didPass); + } + + function _didPass(uint256 proposalIndex) internal returns (bool didPass) { + Proposal memory proposal = proposals[proposalQueue[proposalIndex]]; + + didPass = proposal.yesVotes > proposal.noVotes; + + // Make the proposal fail if the dilutionBound is exceeded + if ((totalShares.add(totalLoot)).mul(dilutionBound) < proposal.maxTotalSharesAndLootAtYesVote) { + didPass = false; + } + + // Make the proposal fail if the applicant is jailed + // - for standard proposals, we don't want the applicant to get any shares/loot/payment + // - for guild kick proposals, we should never be able to propose to kick a jailed member (or have two kick proposals active), so it doesn't matter + if (members[proposal.applicant].jailed != 0) { + didPass = false; + } + + return didPass; + } + + function _validateProposalForProcessing(uint256 proposalIndex) internal view { + require(proposalIndex < proposalQueue.length, "proposal does not exist"); + Proposal memory proposal = proposals[proposalQueue[proposalIndex]]; + + require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "proposal is not ready to be processed"); + require(proposal.flags[1] == false, "proposal has already been processed"); + require(proposalIndex == 0 || proposals[proposalQueue[proposalIndex.sub(1)]].flags[1], "previous proposal must be processed"); + } + + function _returnDeposit(address sponsor) internal { + unsafeInternalTransfer(ESCROW, msg.sender, depositToken, processingReward); + unsafeInternalTransfer(ESCROW, sponsor, depositToken, proposalDeposit.sub(processingReward)); + } + + function ragequit(uint256 sharesToBurn, uint256 lootToBurn) public nonReentrant onlyMember { + _ragequit(msg.sender, sharesToBurn, lootToBurn); + } + + function _ragequit(address memberAddress, uint256 sharesToBurn, uint256 lootToBurn) internal { + uint256 initialTotalSharesAndLoot = totalShares.add(totalLoot); + + Member storage member = members[memberAddress]; + + require(member.shares >= sharesToBurn, "insufficient shares"); + require(member.loot >= lootToBurn, "insufficient loot"); + + require(canRagequit(member.highestIndexYesVote), "cannot ragequit until highest index proposal member voted YES on is processed"); + + uint256 sharesAndLootToBurn = sharesToBurn.add(lootToBurn); + + // burn shares and loot + member.shares = member.shares.sub(sharesToBurn); + member.loot = member.loot.sub(lootToBurn); + totalShares = totalShares.sub(sharesToBurn); + totalLoot = totalLoot.sub(lootToBurn); + + for (uint256 i = 0; i < approvedTokens.length; i++) { + uint256 amountToRagequit = fairShare(userTokenBalances[GUILD][approvedTokens[i]], sharesAndLootToBurn, initialTotalSharesAndLoot); + if (amountToRagequit > 0) { // gas optimization to allow a higher maximum token limit + // deliberately not using safemath here to keep overflows from preventing the function execution (which would break ragekicks) + // if a token overflows, it is because the supply was artificially inflated to oblivion, so we probably don't care about it anyways + userTokenBalances[GUILD][approvedTokens[i]] -= amountToRagequit; + userTokenBalances[memberAddress][approvedTokens[i]] += amountToRagequit; + } + } + + emit Ragequit(msg.sender, sharesToBurn, lootToBurn); + } + + function ragekick(address memberToKick) public nonReentrant { + Member storage member = members[memberToKick]; + + require(member.jailed != 0, "member must be in jail"); + require(member.loot > 0, "member must have some loot"); // note - should be impossible for jailed member to have shares + require(canRagequit(member.highestIndexYesVote), "cannot ragequit until highest index proposal member voted YES on is processed"); + + _ragequit(memberToKick, 0, member.loot); + } + + function withdrawBalance(address token, uint256 amount) public nonReentrant { + _withdrawBalance(token, amount); + } + + function withdrawBalances(address[] memory tokens, uint256[] memory amounts, bool max) public nonReentrant { + require(tokens.length == amounts.length, "tokens and amounts arrays must be matching lengths"); + + for (uint256 i=0; i < tokens.length; i++) { + uint256 withdrawAmount = amounts[i]; + if (max) { // withdraw the maximum balance + withdrawAmount = userTokenBalances[msg.sender][tokens[i]]; + } + + _withdrawBalance(tokens[i], withdrawAmount); + } + } + + function _withdrawBalance(address token, uint256 amount) internal { + require(userTokenBalances[msg.sender][token] >= amount, "insufficient balance"); + unsafeSubtractFromBalance(msg.sender, token, amount); + require(IERC20(token).transfer(msg.sender, amount), "transfer failed"); + emit Withdraw(msg.sender, token, amount); + } + + function collectTokens(address token) public onlyDelegate nonReentrant { + uint256 amountToCollect = IERC20(token).balanceOf(address(this)).sub(userTokenBalances[TOTAL][token]); + // only collect if 1) there are tokens to collect 2) token is whitelisted 3) token has non-zero balance + require(amountToCollect > 0, 'no tokens to collect'); + require(tokenWhitelist[token], 'token to collect must be whitelisted'); + require(userTokenBalances[GUILD][token] > 0, 'token to collect must have non-zero guild bank balance'); + + unsafeAddToBalance(GUILD, token, amountToCollect); + emit TokensCollected(token, amountToCollect); + } + + // NOTE: requires that delegate key which sent the original proposal cancels, msg.sender == proposal.proposer + function cancelProposal(uint256 proposalId) public nonReentrant { + Proposal storage proposal = proposals[proposalId]; + require(!proposal.flags[0], "proposal has already been sponsored"); + require(!proposal.flags[3], "proposal has already been cancelled"); + require(msg.sender == proposal.proposer, "solely the proposer can cancel"); + + proposal.flags[3] = true; // cancelled + + unsafeInternalTransfer(ESCROW, proposal.proposer, proposal.tributeToken, proposal.tributeOffered); + emit CancelProposal(proposalId, msg.sender); + } + + function updateDelegateKey(address newDelegateKey) public nonReentrant onlyShareholder { + require(newDelegateKey != address(0), "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, "cannot overwrite existing members"); + require(!members[memberAddressByDelegateKey[newDelegateKey]].exists, "cannot 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); + } + + // 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, "proposal does not exist"); + return proposals[proposalQueue[highestIndexYesVote]].flags[1]; + } + + function hasVotingPeriodExpired(uint256 startingPeriod) public view returns (bool) { + return getCurrentPeriod() >= startingPeriod.add(votingPeriodLength); + } + + /*************** + 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; + } + + function getProposalFlags(uint256 proposalId) public view returns (bool[6] memory) { + return proposals[proposalId].flags; + } + + function getUserTokenBalance(address user, address token) public view returns (uint256) { + return userTokenBalances[user][token]; + } + + function getMemberProposalVote(address memberAddress, uint256 proposalIndex) public view returns (Vote) { + require(members[memberAddress].exists, "member does not exist"); + require(proposalIndex < proposalQueue.length, "proposal does not exist"); + return proposals[proposalQueue[proposalIndex]].votesByMember[memberAddress]; + } + + function getTokenCount() public view returns (uint256) { + return approvedTokens.length; + } + + /*************** + HELPER FUNCTIONS + ***************/ + function unsafeAddToBalance(address user, address token, uint256 amount) internal { + userTokenBalances[user][token] += amount; + userTokenBalances[TOTAL][token] += amount; + } + + function unsafeSubtractFromBalance(address user, address token, uint256 amount) internal { + userTokenBalances[user][token] -= amount; + userTokenBalances[TOTAL][token] -= amount; + } + + function unsafeInternalTransfer(address from, address to, address token, uint256 amount) internal { + unsafeSubtractFromBalance(from, token, amount); + unsafeAddToBalance(to, token, amount); + } + + function fairShare(uint256 balance, uint256 shares, uint256 totalShares) internal pure returns (uint256) { + require(totalShares != 0); + + if (balance == 0) { return 0; } + + uint256 prod = balance * shares; + + if (prod / balance == shares) { // no overflow in multiplication above? + return prod / totalShares; + } + + return (balance / totalShares) * shares; + } +} + +/* +The MIT License (MIT) +Copyright (c) 2018 Murray Software, LLC. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +contract CloneFactory { // implementation of eip-1167 - see https://eips.ethereum.org/EIPS/eip-1167 + function createClone(address target) internal returns (address result) { + bytes20 targetBytes = bytes20(target); + assembly { + let clone := mload(0x40) + mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(clone, 0x14), targetBytes) + mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + result := create(0, clone, 0x37) + } + } +} + +contract MolochSummoner is CloneFactory { + address public template; + + constructor(address _template) public { + template = _template; + } + + event SummonComplete(address indexed moloch, address[] summoner, address[] tokens, uint256 summoningTime, uint256 periodDuration, uint256 votingPeriodLength, uint256 gracePeriodLength, uint256 proposalDeposit, uint256 dilutionBound, uint256 processingReward, uint256[] summonerShares); + + function summonMoloch( + address[] memory _summoner, + address[] memory _approvedTokens, + uint256 _periodDuration, + uint256 _votingPeriodLength, + uint256 _gracePeriodLength, + uint256 _proposalDeposit, + uint256 _dilutionBound, + uint256 _processingReward, + uint256[] memory _summonerShares + ) public returns (address) { + Moloch baal = Moloch(createClone(template)); + + baal.init( + _summoner, + _approvedTokens, + _periodDuration, + _votingPeriodLength, + _gracePeriodLength, + _proposalDeposit, + _dilutionBound, + _processingReward, + _summonerShares + ); + + emit SummonComplete(address(baal), _summoner, _approvedTokens, now, _periodDuration, _votingPeriodLength, _gracePeriodLength, _proposalDeposit, _dilutionBound, _processingReward, _summonerShares); + + return address(baal); + } +}