Created
September 21, 2021 06:24
Star
You must be signed in to star a gist
Moloch on BentoBox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0-or-later | |
pragma solidity 0.6.12; | |
/* | |
███ ███ ██████ ██████ ██ ██ ██ | |
████ ████ ██ ██ ██ ██ ██ ██ | |
██ ████ ██ ██ ██ ██ ███████ ██ | |
██ ██ ██ ██ ██ ██ ██ ██ ██ | |
██ ██ ██████ ██████ ██ ██ ██ | |
🍣 👹 🍣 👹 🍣 👹 🍣 👹 🍣 👹 🍣 | |
*/ | |
/// @notice Minimal BentoBox vault interface. | |
/// @dev `token` is aliased as `address` from `IERC20` for simplicity. | |
interface IBentoBoxMinimal { | |
/// @notice Deposit an amount of `token` represented in either `amount` or `share`. | |
/// @param token_ The ERC-20 token to deposit. | |
/// @param from which account to pull the tokens. | |
/// @param to which account to push the tokens. | |
/// @param amount Token amount in native representation to deposit. | |
/// @param share Token amount represented in shares to deposit. Takes precedence over `amount`. | |
/// @return amountOut The amount deposited. | |
/// @return shareOut The deposited amount repesented in shares. | |
function deposit( | |
address token_, | |
address from, | |
address to, | |
uint256 amount, | |
uint256 share | |
) external payable returns (uint256 amountOut, uint256 shareOut); | |
/// @notice Withdraws an amount of `token` from a user account. | |
/// @param token_ The ERC-20 token to withdraw. | |
/// @param from which user to pull the tokens. | |
/// @param to which user to push the tokens. | |
/// @param amount of tokens. Either one of `amount` or `share` needs to be supplied. | |
/// @param share Like above, but `share` takes precedence over `amount`. | |
function withdraw( | |
address token_, | |
address from, | |
address to, | |
uint256 amount, | |
uint256 share | |
) external returns (uint256 amountOut, uint256 shareOut); | |
} | |
/// @dev Brief interface for moloch dao v2. | |
interface IMOLOCHMinimal { | |
function submitProposal( | |
address applicant, | |
uint256 sharesRequested, | |
uint256 lootRequested, | |
uint256 tributeOffered, | |
address tributeToken, | |
uint256 paymentRequested, | |
address paymentToken, | |
string calldata details | |
) external returns (uint256); | |
function getProposalFlags(uint256 proposalId) external view returns (bool[6] memory); | |
function withdrawBalance(address token, uint256 amount) external; | |
} | |
/// @notice A library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math). | |
library SafeMath { | |
function add(uint x, uint y) internal pure returns (uint z) { | |
require((z = x + y) >= x, 'SafeMath:Add-Over'); | |
} | |
function sub(uint x, uint y) internal pure returns (uint z) { | |
require((z = x - y) <= x, 'SafeMath:Sub-Over'); | |
} | |
function mul(uint x, uint y) internal pure returns (uint z) { | |
require(y == 0 || (z = x * y) / y == x, 'SafeMath:Mul-Over'); | |
} | |
} | |
/// @dev Low-level caller, ETH holder, separate bank for moloch dao v2 - based on raid guild `Minion`. | |
contract SushiMinion { | |
address immutable depositToken; | |
IMOLOCHMinimal public moloch; // parent moloch contract reference | |
uint256 status = 1; | |
mapping(uint256 => Action) public actions; // proposalId => Action | |
modifier nonReentrant() { // reentrancy guard | |
require(status == 1, "reentrant"); | |
status = 2; | |
_; | |
status = 1; | |
} | |
struct Action { | |
uint256 value; | |
address to; | |
address proposer; | |
bool executed; | |
bytes data; | |
} | |
event ProposeAction(uint256 proposalId, address proposer); | |
event ExecuteAction(uint256 proposalId, address executor); | |
constructor(address _depositToken, IMOLOCHMinimal _moloch) public { | |
depositToken = _depositToken; | |
moloch = _moloch; | |
} | |
function doWithdraw(address token, uint256 amount) external nonReentrant { | |
moloch.withdrawBalance(token, amount); // withdraw funds from parent moloch into minion | |
} | |
function proposeAction( | |
address actionTo, | |
uint256 actionValue, | |
bytes calldata actionData, | |
string calldata details | |
) external nonReentrant returns (uint256) { | |
// No calls to zero address allows us to check that proxy submitted | |
// the proposal without getting the proposal struct from parent moloch | |
require(actionTo != address(0), "invalid actionTo"); | |
uint256 proposalId = moloch.submitProposal( | |
address(this), | |
0, | |
0, | |
0, | |
depositToken, | |
0, | |
depositToken, | |
details | |
); | |
Action memory action = Action({ | |
value: actionValue, | |
to: actionTo, | |
proposer: msg.sender, | |
executed: false, | |
data: actionData | |
}); | |
actions[proposalId] = action; | |
emit ProposeAction(proposalId, msg.sender); | |
return proposalId; | |
} | |
function executeAction(uint256 proposalId) external nonReentrant returns (bytes memory) { | |
Action memory action = actions[proposalId]; | |
bool[6] memory flags = moloch.getProposalFlags(proposalId); | |
require(action.to != address(0), "invalid proposalId"); | |
require(!action.executed, "action executed"); | |
require(address(this).balance >= action.value, "insufficient ETH"); | |
require(flags[2], "proposal not passed"); | |
// execute call | |
actions[proposalId].executed = true; | |
(bool success, bytes memory retData) = action.to.call{value: action.value}(action.data); | |
require(success, "call failure"); | |
emit ExecuteAction(proposalId, msg.sender); | |
return retData; | |
} | |
receive() external payable {} | |
} | |
/*============================ | |
WELCOME TO THE PARTY (飲み会)! | |
============================*/ | |
/// @dev MolochV2 with BentoBox. | |
contract Mochi { | |
using SafeMath for uint256; | |
/*************** | |
GLOBAL CONSTANTS | |
***************/ | |
IBentoBoxMinimal constant bento = IBentoBoxMinimal(0xF5BCE5077908a1b7370B9ae04AdC565EBd643966); // BENTO vault contract (multinet) | |
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 immutable summoningTime; // needed to determine the current period | |
address payable public immutable sushiMinion; // sushi minion contract reference | |
address immutable depositToken; // deposit token contract reference; default = SUSHI | |
// 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 = type(uint256).max; // 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 | |
uint256 status = 1; | |
// *************** | |
// EVENTS | |
// *************** | |
event SummonComplete(address[] indexed summoner, address[] tokens, uint256 summoningTime, uint256 periodDuration, uint256 votingPeriodLength, uint256 gracePeriodLength, uint256 proposalDeposit, uint256 dilutionBound, uint256 processingReward); | |
event MakeDeposit(address indexed memberAddress, uint256 tributeOffered, uint256 shares); | |
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); | |
event InternalTransfer(address indexed from, address indexed to, address token, uint256 amount); | |
// ******************* | |
// INTERNAL ACCOUNTING | |
// ******************* | |
uint256 public proposalCount; // total proposals submitted | |
uint256 public totalShares; // total shares across all members | |
uint256 public totalLoot; // total loot across all members | |
uint256 public totalGuildBankTokens; // 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] | |
modifier nonReentrant() { // reentrancy guard | |
require(status == 1, "reentrant"); | |
status = 2; | |
_; | |
status = 1; | |
} | |
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"); | |
_; | |
} | |
constructor( | |
address[] memory _summoner, | |
address[] memory _approvedTokens, | |
uint256 _periodDuration, | |
uint256 _votingPeriodLength, | |
uint256 _gracePeriodLength, | |
uint256 _proposalDeposit, | |
uint256 _dilutionBound, | |
uint256 _processingReward, | |
uint256[] memory _summonerShares | |
) public { | |
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"); | |
// NOTE: move event up here, avoid stack too deep if too many approved tokens | |
emit SummonComplete(_summoner, _approvedTokens, block.timestamp, _periodDuration, _votingPeriodLength, _gracePeriodLength, _proposalDeposit, _dilutionBound, _processingReward); | |
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]); | |
} | |
SushiMinion minion = new SushiMinion(_approvedTokens[0], IMOLOCHMinimal(address(this))); // summon sushi minion contract | |
sushiMinion = address(minion); // record minion reference | |
depositToken = _approvedTokens[0]; | |
periodDuration = _periodDuration; | |
votingPeriodLength = _votingPeriodLength; | |
gracePeriodLength = _gracePeriodLength; | |
proposalDeposit = _proposalDeposit; | |
dilutionBound = _dilutionBound; | |
processingReward = _processingReward; | |
summoningTime = block.timestamp; | |
} | |
/***************** | |
PROPOSAL FUNCTIONS | |
*****************/ | |
function submitProposal( | |
address applicant, | |
uint256 sharesRequested, | |
uint256 lootRequested, | |
uint256 tributeOffered, | |
address tributeToken, | |
uint256 paymentRequested, | |
address paymentToken, | |
string memory details | |
) external nonReentrant returns (uint256 proposalId) { | |
require(sharesRequested == 0 || lootRequested == 0, 'must request shares or loot, but not both'); | |
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 BentoBox until the proposal is processed | |
_safeTransferFrom(tributeToken, msg.sender, address(bento), tributeOffered); | |
(, uint256 shares) = bento.deposit(tributeToken, address(bento), address(this), tributeOffered, 0); | |
bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick] | |
_submitProposal(applicant, shares, shares, tributeOffered, tributeToken, paymentRequested, paymentToken, details, flags); | |
return proposalCount - 1; // return proposalId - contracts calling submit might want it | |
} | |
function submitWhitelistProposal(address tokenToWhitelist, string memory details) external 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) external 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 | |
) private { | |
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) external nonReentrant onlyDelegate { | |
// collect proposal deposit from sponsor and store it in the BentoBox until the proposal is processed | |
_safeTransferFrom(depositToken, msg.sender, address(bento), proposalDeposit); | |
(, uint256 shares) = bento.deposit(depositToken, address(bento), address(this), proposalDeposit, 0); | |
unsafeAddToBalance(ESCROW, depositToken, shares); | |
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) external 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) external 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) external 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) external 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) private view 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) private 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) private { | |
unsafeInternalTransfer(ESCROW, msg.sender, depositToken, processingReward); | |
unsafeInternalTransfer(ESCROW, sponsor, depositToken, proposalDeposit.sub(processingReward)); | |
} | |
function ragequit(uint256 sharesToBurn, uint256 lootToBurn) external nonReentrant onlyMember { | |
_ragequit(msg.sender, sharesToBurn, lootToBurn); | |
} | |
function _ragequit(address memberAddress, uint256 sharesToBurn, uint256 lootToBurn) private { | |
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) external 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) external nonReentrant { | |
_withdrawBalance(token, amount); | |
} | |
function withdrawBalances(address[] memory tokens, uint256[] memory amounts, bool max) external 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) private { | |
require(userTokenBalances[msg.sender][token] >= amount, "insufficient balance"); | |
unsafeSubtractFromBalance(msg.sender, token, amount); | |
bento.withdraw(token, address(this), msg.sender, 0, amount); | |
emit Withdraw(msg.sender, token, amount); | |
} | |
// allows guild bank users to make internal token transfers among accounts | |
function internalTransfer(address to, address token, uint256 amount) external { | |
require(userTokenBalances[msg.sender][token] >= amount, "insufficient amount"); | |
unsafeInternalTransfer(msg.sender, to, token, amount); | |
emit InternalTransfer(msg.sender, to, token, amount); | |
} | |
function collectTokens(address token) external onlyDelegate nonReentrant { | |
uint256 amountToCollect = balanceOfThis(token).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'); | |
_safeTransfer(token, address(bento), amountToCollect); | |
(, uint256 shares) = bento.deposit(token, address(bento), address(this), amountToCollect, 0); | |
unsafeAddToBalance(GUILD, token, shares); | |
emit TokensCollected(token, shares); | |
} | |
// NOTE: requires that delegate key which sent the original proposal cancels, msg.sender == proposal.proposer | |
function cancelProposal(uint256 proposalId) external 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) external 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) private pure returns (uint256) { | |
return x >= y ? x : y; | |
} | |
function getCurrentPeriod() public view returns (uint256) { | |
return block.timestamp.sub(summoningTime) / 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) private { | |
userTokenBalances[user][token] += amount; | |
userTokenBalances[TOTAL][token] += amount; | |
} | |
function unsafeSubtractFromBalance(address user, address token, uint256 amount) private { | |
userTokenBalances[user][token] -= amount; | |
userTokenBalances[TOTAL][token] -= amount; | |
} | |
function unsafeInternalTransfer(address from, address to, address token, uint256 amount) private { | |
unsafeSubtractFromBalance(from, token, amount); | |
unsafeAddToBalance(to, token, amount); | |
} | |
function fairShare(uint256 balance, uint256 shares, uint256 totalSharesAndLoot) private pure returns (uint256) { | |
require(totalSharesAndLoot != 0); | |
if (balance == 0) { return 0; } | |
uint256 prod = balance * shares; | |
if (prod / balance == shares) { // no overflow in multiplication above? | |
return prod / totalSharesAndLoot; | |
} | |
return (balance / totalSharesAndLoot) * shares; | |
} | |
/// @notice Provides gas-optimized balance check on this contract to avoid redundant extcodesize check in addition to returndatasize check. | |
/// @param token Address of ERC-20 token. | |
/// @return balance Token amount held by this contract. | |
function balanceOfThis(address token) private view returns (uint256 balance) { | |
(bool success, bytes memory data) = token.staticcall(abi.encodeWithSelector(0x70a08231, address(this))); // @dev balanceOf(address). | |
require(success && data.length >= 32, "BALANCE_OF_FAILED"); | |
balance = abi.decode(data, (uint256)); | |
} | |
/// @notice Provides 'safe' {transfer} for tokens that do not consistently return 'true/false'. | |
function _safeTransfer(address token, address to, uint amount) private { | |
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount)); /*'transfer(address,uint)'*/ | |
require(success && (data.length == 0 || abi.decode(data, (bool))),'transfer failed'); /*checks success & allows non-conforming transfers*/ | |
} | |
/// @notice Provides 'safe' {transferFrom} for tokens that do not consistently return 'true/false'. | |
function _safeTransferFrom(address token, address from, address to, uint amount) private { | |
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, amount)); /*'transferFrom(address,address,uint)'*/ | |
require(success && (data.length == 0 || abi.decode(data, (bool))),'transferFrom failed'); /*checks success & allows non-conforming transfers*/ | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment