├── .gitattributes ├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── moloch.sol ├── src ├── SelloutDao.sol └── SelloutDao.t.sol ├── test.sh └── ui ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.jsx ├── App.test.js ├── Proposal.jsx ├── abi │ ├── moloch.json │ └── sellout.json ├── index.css ├── index.js ├── moloch.png └── serviceWorker.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ds-test"] 2 | path = lib/ds-test 3 | url = https://github.com/dapphub/ds-test 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all :; dapp build 2 | clean :; dapp clean 3 | test :; dapp test 4 | deploy :; dapp create OpenMoloch 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SelloutDAO 2 | 3 | What is is? 4 | 5 | A way to sell your voting power on a Moloch-like DAO to the highest bidder of course! 6 | 7 | ## Installation 8 | 9 | Have `dapptools` (https://dapp.tools) 10 | 11 | `dapp build` 12 | 13 | `dapp create SelloutDao [address_of_dao]` 14 | 15 | Then set that newly created contract to be your delegate voting key on MolochDAO, MetaCartelDAO, YangDAO, OrochiDAO, or any Moloch-Like DAO. (will you two stop saying dao so much??) 16 | 17 | ## UI 18 | 19 | `cd ui` 20 | 21 | `npm install` 22 | 23 | `npm start` -------------------------------------------------------------------------------- /moloch.sol: -------------------------------------------------------------------------------- 1 | // Goals 2 | // - Defensibility -> Kick out malicious members via forceRagequit 3 | // - Separation of Wealth and Power -> voting / loot tokens - grant pool can't be claimed (controlled by separate contract?) 4 | // - batch proposals -> 1 month between proposal batches, 2 week voting period, 2 week grace period 5 | // - better spam protection -> exponential increase in deposit for same member / option to claim deposit 6 | // - replacing members? 7 | // - hasn't been discussed 8 | // - accountability to stakeholders 9 | // - some kind of siganlling 10 | 11 | 12 | pragma solidity ^0.5.3; 13 | 14 | import "./oz/SafeMath.sol"; 15 | import "./oz/IERC20.sol"; 16 | import "./GuildBank.sol"; 17 | 18 | contract Moloch { 19 | using SafeMath for uint256; 20 | 21 | /*************** 22 | GLOBAL CONSTANTS 23 | ***************/ 24 | uint256 public periodDuration; // default = 17280 = 4.8 hours in seconds (5 periods per day) 25 | uint256 public votingPeriodLength; // default = 35 periods (7 days) 26 | uint256 public gracePeriodLength; // default = 35 periods (7 days) 27 | uint256 public abortWindow; // default = 5 periods (1 day) 28 | uint256 public proposalDeposit; // default = 10 ETH (~$1,000 worth of ETH at contract deployment) 29 | uint256 public dilutionBound; // default = 3 - maximum multiplier a YES voter will be obligated to pay in case of mass ragequit 30 | uint256 public processingReward; // default = 0.1 - amount of ETH to give to whoever processes a proposal 31 | uint256 public summoningTime; // needed to determine the current period 32 | 33 | IERC20 public approvedToken; // approved token contract reference; default = wETH 34 | GuildBank public guildBank; // guild bank contract reference 35 | 36 | // HARD-CODED LIMITS 37 | // These numbers are quite arbitrary; they are small enough to avoid overflows when doing calculations 38 | // with periods or shares, yet big enough to not limit reasonable use cases. 39 | uint256 constant MAX_VOTING_PERIOD_LENGTH = 10**18; // maximum length of voting period 40 | uint256 constant MAX_GRACE_PERIOD_LENGTH = 10**18; // maximum length of grace period 41 | uint256 constant MAX_DILUTION_BOUND = 10**18; // maximum dilution bound 42 | uint256 constant MAX_NUMBER_OF_SHARES = 10**18; // maximum number of shares that can be minted 43 | 44 | /*************** 45 | EVENTS 46 | ***************/ 47 | event SubmitProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, address indexed applicant, uint256 tokenTribute, uint256 sharesRequested); 48 | event SubmitVote(uint256 indexed proposalIndex, address indexed delegateKey, address indexed memberAddress, uint8 uintVote); 49 | event ProcessProposal(uint256 indexed proposalIndex, address indexed applicant, address indexed memberAddress, uint256 tokenTribute, uint256 sharesRequested, bool didPass); 50 | event Ragequit(address indexed memberAddress, uint256 sharesToBurn); 51 | event Abort(uint256 indexed proposalIndex, address applicantAddress); 52 | event UpdateDelegateKey(address indexed memberAddress, address newDelegateKey); 53 | event SummonComplete(address indexed summoner, uint256 shares); 54 | 55 | /****************** 56 | INTERNAL ACCOUNTING 57 | ******************/ 58 | uint256 public totalShares = 0; // total shares across all members 59 | uint256 public totalSharesRequested = 0; // total shares that have been requested in unprocessed proposals 60 | 61 | enum Vote { 62 | Null, // default value, counted as abstention 63 | Yes, 64 | No 65 | } 66 | 67 | struct Member { 68 | address delegateKey; // the key responsible for submitting proposals and voting - defaults to member address unless updated 69 | uint256 shares; // the # of shares assigned to this member 70 | bool exists; // always true once a member has been created 71 | uint256 highestIndexYesVote; // highest proposal index # on which the member voted YES 72 | } 73 | 74 | struct Proposal { 75 | address proposer; // the member who submitted the proposal 76 | address applicant; // the applicant who wishes to become a member - this key will be used for withdrawals 77 | uint256 sharesRequested; // the # of shares the applicant is requesting 78 | uint256 startingPeriod; // the period in which voting can start for this proposal 79 | uint256 yesVotes; // the total number of YES votes for this proposal 80 | uint256 noVotes; // the total number of NO votes for this proposal 81 | bool processed; // true only if the proposal has been processed 82 | bool didPass; // true only if the proposal passed 83 | bool aborted; // true only if applicant calls "abort" fn before end of voting period 84 | uint256 tokenTribute; // amount of tokens offered as tribute 85 | string details; // proposal details - could be IPFS hash, plaintext, or JSON 86 | uint256 maxTotalSharesAtYesVote; // the maximum # of total shares encountered at a yes vote on this proposal 87 | mapping (address => Vote) votesByMember; // the votes on this proposal by each member 88 | } 89 | 90 | mapping (address => Member) public members; 91 | mapping (address => address) public memberAddressByDelegateKey; 92 | Proposal[] public proposalQueue; 93 | 94 | /******** 95 | MODIFIERS 96 | ********/ 97 | modifier onlyMember { 98 | require(members[msg.sender].shares > 0, "Moloch::onlyMember - not a member"); 99 | _; 100 | } 101 | 102 | modifier onlyDelegate { 103 | require(members[memberAddressByDelegateKey[msg.sender]].shares > 0, "Moloch::onlyDelegate - not a delegate"); 104 | _; 105 | } 106 | 107 | /******** 108 | FUNCTIONS 109 | ********/ 110 | constructor( 111 | address summoner, 112 | address _approvedToken, 113 | uint256 _periodDuration, 114 | uint256 _votingPeriodLength, 115 | uint256 _gracePeriodLength, 116 | uint256 _abortWindow, 117 | uint256 _proposalDeposit, 118 | uint256 _dilutionBound, 119 | uint256 _processingReward 120 | ) public { 121 | require(summoner != address(0), "Moloch::constructor - summoner cannot be 0"); 122 | require(_approvedToken != address(0), "Moloch::constructor - _approvedToken cannot be 0"); 123 | require(_periodDuration > 0, "Moloch::constructor - _periodDuration cannot be 0"); 124 | require(_votingPeriodLength > 0, "Moloch::constructor - _votingPeriodLength cannot be 0"); 125 | require(_votingPeriodLength <= MAX_VOTING_PERIOD_LENGTH, "Moloch::constructor - _votingPeriodLength exceeds limit"); 126 | require(_gracePeriodLength <= MAX_GRACE_PERIOD_LENGTH, "Moloch::constructor - _gracePeriodLength exceeds limit"); 127 | require(_abortWindow > 0, "Moloch::constructor - _abortWindow cannot be 0"); 128 | require(_abortWindow <= _votingPeriodLength, "Moloch::constructor - _abortWindow must be smaller than or equal to _votingPeriodLength"); 129 | require(_dilutionBound > 0, "Moloch::constructor - _dilutionBound cannot be 0"); 130 | require(_dilutionBound <= MAX_DILUTION_BOUND, "Moloch::constructor - _dilutionBound exceeds limit"); 131 | require(_proposalDeposit >= _processingReward, "Moloch::constructor - _proposalDeposit cannot be smaller than _processingReward"); 132 | 133 | approvedToken = IERC20(_approvedToken); 134 | 135 | guildBank = new GuildBank(_approvedToken); 136 | 137 | periodDuration = _periodDuration; 138 | votingPeriodLength = _votingPeriodLength; 139 | gracePeriodLength = _gracePeriodLength; 140 | abortWindow = _abortWindow; 141 | proposalDeposit = _proposalDeposit; 142 | dilutionBound = _dilutionBound; 143 | processingReward = _processingReward; 144 | 145 | summoningTime = now; 146 | 147 | members[summoner] = Member(summoner, 1, true, 0); 148 | memberAddressByDelegateKey[summoner] = summoner; 149 | totalShares = 1; 150 | 151 | emit SummonComplete(summoner, 1); 152 | } 153 | 154 | /***************** 155 | PROPOSAL FUNCTIONS 156 | *****************/ 157 | 158 | function submitProposal( 159 | address applicant, 160 | uint256 tokenTribute, 161 | uint256 sharesRequested, 162 | string memory details 163 | ) 164 | public 165 | onlyDelegate 166 | { 167 | require(applicant != address(0), "Moloch::submitProposal - applicant cannot be 0"); 168 | 169 | // Make sure we won't run into overflows when doing calculations with shares. 170 | // Note that totalShares + totalSharesRequested + sharesRequested is an upper bound 171 | // on the number of shares that can exist until this proposal has been processed. 172 | require(totalShares.add(totalSharesRequested).add(sharesRequested) <= MAX_NUMBER_OF_SHARES, "Moloch::submitProposal - too many shares requested"); 173 | 174 | totalSharesRequested = totalSharesRequested.add(sharesRequested); 175 | 176 | address memberAddress = memberAddressByDelegateKey[msg.sender]; 177 | 178 | // collect proposal deposit from proposer and store it in the Moloch until the proposal is processed 179 | require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed"); 180 | 181 | // collect tribute from applicant and store it in the Moloch until the proposal is processed 182 | require(approvedToken.transferFrom(applicant, address(this), tokenTribute), "Moloch::submitProposal - tribute token transfer failed"); 183 | 184 | // compute startingPeriod for proposal 185 | uint256 startingPeriod = max( 186 | getCurrentPeriod(), 187 | proposalQueue.length == 0 ? 0 : proposalQueue[proposalQueue.length.sub(1)].startingPeriod 188 | ).add(1); 189 | 190 | // create proposal ... 191 | Proposal memory proposal = Proposal({ 192 | proposer: memberAddress, 193 | applicant: applicant, 194 | sharesRequested: sharesRequested, 195 | startingPeriod: startingPeriod, 196 | yesVotes: 0, 197 | noVotes: 0, 198 | processed: false, 199 | didPass: false, 200 | aborted: false, 201 | tokenTribute: tokenTribute, 202 | details: details, 203 | maxTotalSharesAtYesVote: 0 204 | }); 205 | 206 | // ... and append it to the queue 207 | proposalQueue.push(proposal); 208 | 209 | uint256 proposalIndex = proposalQueue.length.sub(1); 210 | emit SubmitProposal(proposalIndex, msg.sender, memberAddress, applicant, tokenTribute, sharesRequested); 211 | } 212 | 213 | function submitVote(uint256 proposalIndex, uint8 uintVote) public onlyDelegate { 214 | address memberAddress = memberAddressByDelegateKey[msg.sender]; 215 | Member storage member = members[memberAddress]; 216 | 217 | require(proposalIndex < proposalQueue.length, "Moloch::submitVote - proposal does not exist"); 218 | Proposal storage proposal = proposalQueue[proposalIndex]; 219 | 220 | require(uintVote < 3, "Moloch::submitVote - uintVote must be less than 3"); 221 | Vote vote = Vote(uintVote); 222 | 223 | require(getCurrentPeriod() >= proposal.startingPeriod, "Moloch::submitVote - voting period has not started"); 224 | require(!hasVotingPeriodExpired(proposal.startingPeriod), "Moloch::submitVote - proposal voting period has expired"); 225 | require(proposal.votesByMember[memberAddress] == Vote.Null, "Moloch::submitVote - member has already voted on this proposal"); 226 | require(vote == Vote.Yes || vote == Vote.No, "Moloch::submitVote - vote must be either Yes or No"); 227 | require(!proposal.aborted, "Moloch::submitVote - proposal has been aborted"); 228 | 229 | // store vote 230 | proposal.votesByMember[memberAddress] = vote; 231 | 232 | // count vote 233 | if (vote == Vote.Yes) { 234 | proposal.yesVotes = proposal.yesVotes.add(member.shares); 235 | 236 | // set highest index (latest) yes vote - must be processed for member to ragequit 237 | if (proposalIndex > member.highestIndexYesVote) { 238 | member.highestIndexYesVote = proposalIndex; 239 | } 240 | 241 | // set maximum of total shares encountered at a yes vote - used to bound dilution for yes voters 242 | if (totalShares > proposal.maxTotalSharesAtYesVote) { 243 | proposal.maxTotalSharesAtYesVote = totalShares; 244 | } 245 | 246 | } else if (vote == Vote.No) { 247 | proposal.noVotes = proposal.noVotes.add(member.shares); 248 | } 249 | 250 | emit SubmitVote(proposalIndex, msg.sender, memberAddress, uintVote); 251 | } 252 | 253 | function processProposal(uint256 proposalIndex) public { 254 | require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist"); 255 | Proposal storage proposal = proposalQueue[proposalIndex]; 256 | 257 | require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed"); 258 | require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed"); 259 | require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed"); 260 | 261 | proposal.processed = true; 262 | totalSharesRequested = totalSharesRequested.sub(proposal.sharesRequested); 263 | 264 | bool didPass = proposal.yesVotes > proposal.noVotes; 265 | 266 | // Make the proposal fail if the dilutionBound is exceeded 267 | if (totalShares.mul(dilutionBound) < proposal.maxTotalSharesAtYesVote) { 268 | didPass = false; 269 | } 270 | 271 | // PROPOSAL PASSED 272 | if (didPass && !proposal.aborted) { 273 | 274 | proposal.didPass = true; 275 | 276 | // if the applicant is already a member, add to their existing shares 277 | if (members[proposal.applicant].exists) { 278 | members[proposal.applicant].shares = members[proposal.applicant].shares.add(proposal.sharesRequested); 279 | 280 | // the applicant is a new member, create a new record for them 281 | } else { 282 | // if the applicant address is already taken by a member's delegateKey, reset it to their member address 283 | if (members[memberAddressByDelegateKey[proposal.applicant]].exists) { 284 | address memberToOverride = memberAddressByDelegateKey[proposal.applicant]; 285 | memberAddressByDelegateKey[memberToOverride] = memberToOverride; 286 | members[memberToOverride].delegateKey = memberToOverride; 287 | } 288 | 289 | // use applicant address as delegateKey by default 290 | members[proposal.applicant] = Member(proposal.applicant, proposal.sharesRequested, true, 0); 291 | memberAddressByDelegateKey[proposal.applicant] = proposal.applicant; 292 | } 293 | 294 | // mint new shares 295 | totalShares = totalShares.add(proposal.sharesRequested); 296 | 297 | // transfer tokens to guild bank 298 | require( 299 | approvedToken.transfer(address(guildBank), proposal.tokenTribute), 300 | "Moloch::processProposal - token transfer to guild bank failed" 301 | ); 302 | 303 | // PROPOSAL FAILED OR ABORTED 304 | } else { 305 | // return all tokens to the applicant 306 | require( 307 | approvedToken.transfer(proposal.applicant, proposal.tokenTribute), 308 | "Moloch::processProposal - failing vote token transfer failed" 309 | ); 310 | } 311 | 312 | // send msg.sender the processingReward 313 | require( 314 | approvedToken.transfer(msg.sender, processingReward), 315 | "Moloch::processProposal - failed to send processing reward to msg.sender" 316 | ); 317 | 318 | // return deposit to proposer (subtract processing reward) 319 | require( 320 | approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)), 321 | "Moloch::processProposal - failed to return proposal deposit to proposer" 322 | ); 323 | 324 | emit ProcessProposal( 325 | proposalIndex, 326 | proposal.applicant, 327 | proposal.proposer, 328 | proposal.tokenTribute, 329 | proposal.sharesRequested, 330 | didPass 331 | ); 332 | } 333 | 334 | function ragequit(uint256 sharesToBurn) public onlyMember { 335 | uint256 initialTotalShares = totalShares; 336 | 337 | Member storage member = members[msg.sender]; 338 | 339 | require(member.shares >= sharesToBurn, "Moloch::ragequit - insufficient shares"); 340 | 341 | require(canRagequit(member.highestIndexYesVote), "Moloch::ragequit - cant ragequit until highest index proposal member voted YES on is processed"); 342 | 343 | // burn shares 344 | member.shares = member.shares.sub(sharesToBurn); 345 | totalShares = totalShares.sub(sharesToBurn); 346 | 347 | // instruct guildBank to transfer fair share of tokens to the ragequitter 348 | require( 349 | guildBank.withdraw(msg.sender, sharesToBurn, initialTotalShares), 350 | "Moloch::ragequit - withdrawal of tokens from guildBank failed" 351 | ); 352 | 353 | emit Ragequit(msg.sender, sharesToBurn); 354 | } 355 | 356 | function abort(uint256 proposalIndex) public { 357 | require(proposalIndex < proposalQueue.length, "Moloch::abort - proposal does not exist"); 358 | Proposal storage proposal = proposalQueue[proposalIndex]; 359 | 360 | require(msg.sender == proposal.applicant, "Moloch::abort - msg.sender must be applicant"); 361 | require(getCurrentPeriod() < proposal.startingPeriod.add(abortWindow), "Moloch::abort - abort window must not have passed"); 362 | require(!proposal.aborted, "Moloch::abort - proposal must not have already been aborted"); 363 | 364 | uint256 tokensToAbort = proposal.tokenTribute; 365 | proposal.tokenTribute = 0; 366 | proposal.aborted = true; 367 | 368 | // return all tokens to the applicant 369 | require( 370 | approvedToken.transfer(proposal.applicant, tokensToAbort), 371 | "Moloch::processProposal - failed to return tribute to applicant" 372 | ); 373 | 374 | emit Abort(proposalIndex, msg.sender); 375 | } 376 | 377 | function updateDelegateKey(address newDelegateKey) public onlyMember { 378 | require(newDelegateKey != address(0), "Moloch::updateDelegateKey - newDelegateKey cannot be 0"); 379 | 380 | // skip checks if member is setting the delegate key to their member address 381 | if (newDelegateKey != msg.sender) { 382 | require(!members[newDelegateKey].exists, "Moloch::updateDelegateKey - cant overwrite existing members"); 383 | require(!members[memberAddressByDelegateKey[newDelegateKey]].exists, "Moloch::updateDelegateKey - cant overwrite existing delegate keys"); 384 | } 385 | 386 | Member storage member = members[msg.sender]; 387 | memberAddressByDelegateKey[member.delegateKey] = address(0); 388 | memberAddressByDelegateKey[newDelegateKey] = msg.sender; 389 | member.delegateKey = newDelegateKey; 390 | 391 | emit UpdateDelegateKey(msg.sender, newDelegateKey); 392 | } 393 | 394 | /*************** 395 | GETTER FUNCTIONS 396 | ***************/ 397 | 398 | function max(uint256 x, uint256 y) internal pure returns (uint256) { 399 | return x >= y ? x : y; 400 | } 401 | 402 | function getCurrentPeriod() public view returns (uint256) { 403 | return now.sub(summoningTime).div(periodDuration); 404 | } 405 | 406 | function getProposalQueueLength() public view returns (uint256) { 407 | return proposalQueue.length; 408 | } 409 | 410 | // can only ragequit if the latest proposal you voted YES on has been processed 411 | function canRagequit(uint256 highestIndexYesVote) public view returns (bool) { 412 | require(highestIndexYesVote < proposalQueue.length, "Moloch::canRagequit - proposal does not exist"); 413 | return proposalQueue[highestIndexYesVote].processed; 414 | } 415 | 416 | function hasVotingPeriodExpired(uint256 startingPeriod) public view returns (bool) { 417 | return getCurrentPeriod() >= startingPeriod.add(votingPeriodLength); 418 | } 419 | 420 | function getMemberProposalVote(address memberAddress, uint256 proposalIndex) public view returns (Vote) { 421 | require(members[memberAddress].exists, "Moloch::getMemberProposalVote - member doesn't exist"); 422 | require(proposalIndex < proposalQueue.length, "Moloch::getMemberProposalVote - proposal doesn't exist"); 423 | return proposalQueue[proposalIndex].votesByMember[memberAddress]; 424 | } 425 | } -------------------------------------------------------------------------------- /src/SelloutDao.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.10; 2 | 3 | contract MolochLike { 4 | function updateDelegateKey(address) external; 5 | function submitVote(uint256, uint8) external; 6 | function submitProposal(address, uint256, uint256, string calldata) external; 7 | function processProposal(uint256) external; 8 | function getProposalQueueLength() external view returns (uint256); 9 | function getMemberProposalVote(address, uint256) external view returns (uint256); 10 | function proposalDeposit() external view returns (uint256); 11 | function periodDuration() external view returns (uint256); 12 | function approvedToken() external view returns (address); 13 | } 14 | 15 | contract GemLike { 16 | function approve(address, uint256) external returns (bool); 17 | function transfer(address, uint256) external returns (bool); 18 | function balanceOf(address) external view returns (uint256); 19 | } 20 | 21 | contract WethLike is GemLike { 22 | function deposit() external payable; 23 | } 24 | 25 | contract SelloutDao { 26 | address public owner; 27 | MolochLike public dao; 28 | GemLike public gem; 29 | address public hat; 30 | bool public sold; 31 | uint256 public prop; 32 | bool public voted; 33 | 34 | modifier auth() { 35 | require(msg.sender == owner, "nope"); 36 | _; 37 | } 38 | 39 | modifier only_hat() { 40 | require(msg.sender == hat, "nope"); 41 | _; 42 | } 43 | 44 | constructor(MolochLike dao_) public { 45 | owner = msg.sender; 46 | dao = dao_; 47 | gem = GemLike(dao.approvedToken()); 48 | } 49 | 50 | function () external payable { 51 | buy(); 52 | } 53 | 54 | function buy() public payable { 55 | require(!sold, "already sold"); 56 | require(msg.value >= 0.5 ether, "need to send at least 0.5 eth"); 57 | sold = true; 58 | hat = msg.sender; 59 | } 60 | 61 | function make(address who, uint256 tribute, uint256 shares, string calldata text) external only_hat { 62 | require(prop == 0, "can only create one proposal"); 63 | gem.approve(address(dao), dao.proposalDeposit()); 64 | dao.submitProposal(who, tribute, shares, text); 65 | prop = dao.getProposalQueueLength() - 1; 66 | } 67 | 68 | function vote(uint8 val) external only_hat { 69 | dao.submitVote(prop, val); 70 | } 71 | 72 | function take() external auth { 73 | msg.sender.transfer(address(this).balance); 74 | gem.transfer(msg.sender, gem.balanceOf(address(this))); 75 | } 76 | } 77 | 78 | // contract OpenMolochFactory { 79 | 80 | // } 81 | -------------------------------------------------------------------------------- /src/SelloutDao.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.10; 2 | 3 | import "ds-test/test.sol"; 4 | 5 | import "./SelloutDao.sol"; 6 | 7 | contract Hevm { 8 | function warp(uint256) public; 9 | } 10 | 11 | contract SelloutDaoTest is DSTest { 12 | MolochLike dao; 13 | SelloutDao om; 14 | WethLike weth; 15 | GemLike gem; 16 | Hevm hevm; 17 | 18 | function () external payable { 19 | 20 | } 21 | 22 | function setUp() public { 23 | hevm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); 24 | dao = MolochLike(0x1fd169A4f5c59ACf79d0Fd5d91D1201EF1Bce9f1); 25 | weth = WethLike(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 26 | gem = GemLike(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 27 | } 28 | 29 | function test_after() public { 30 | 31 | om = SelloutDao(0x829fE69F1feA3305C1aa0C1873b22835b87200d6); 32 | 33 | uint256 initialEth = address(this).balance; 34 | uint256 initialWeth = weth.balanceOf(address(this)); 35 | uint256 deposit = 10 ether; 36 | uint256 tribute = 100 ether; 37 | 38 | assertEq(weth.balanceOf(address(this)), initialWeth); 39 | assertEq(weth.balanceOf(address(om)), 0); 40 | 41 | emit log_named_uint('balance', initialWeth); 42 | 43 | weth.transfer(address(om), deposit); 44 | 45 | assertEq(weth.balanceOf(address(this)), initialWeth - deposit); 46 | assertEq(weth.balanceOf(address(om)), deposit); 47 | 48 | emit log_named_uint('balance - deposit', initialWeth - deposit); 49 | 50 | address(om).call.value(1 ether).gas(100000)(""); 51 | 52 | assertEq(address(this).balance, initialEth - 1 ether); 53 | assertEq(address(om).balance, 1 ether); 54 | 55 | assertTrue(om.sold()); 56 | assertEq(om.hat(), address(this)); 57 | 58 | weth.approve(address(dao), tribute); 59 | 60 | om.make(address(this), tribute, 100, "Made via SelloutDAO"); 61 | 62 | emit log_named_uint('balance - deposit - tribute', initialWeth - deposit - tribute); 63 | 64 | uint256 prop = om.prop(); 65 | 66 | emit log_named_uint('prop', prop); 67 | 68 | hevm.warp(now + dao.periodDuration()); 69 | 70 | om.vote(1); 71 | assertEq(dao.getMemberProposalVote(0xcd16CBdA54af2556EBB6df4FBFd178e63c33FD89, prop), 1); 72 | 73 | hevm.warp(now + (dao.periodDuration() * 70)); 74 | 75 | dao.processProposal(86); 76 | dao.processProposal(87); 77 | dao.processProposal(88); 78 | dao.processProposal(89); 79 | dao.processProposal(90); 80 | dao.processProposal(91); 81 | dao.processProposal(92); 82 | dao.processProposal(93); 83 | dao.processProposal(94); 84 | dao.processProposal(95); 85 | dao.processProposal(prop); 86 | 87 | assertEq(weth.balanceOf(0xcd16CBdA54af2556EBB6df4FBFd178e63c33FD89), 0); 88 | 89 | om.take(); 90 | 91 | emit log_named_uint('om', weth.balanceOf(address(om))); 92 | 93 | assertEq(weth.balanceOf(address(om)), 0); 94 | assertEq(weth.balanceOf(address(this)), initialWeth - tribute - deposit + 1.1 ether); 95 | 96 | assertEq(weth.balanceOf(0xcd16CBdA54af2556EBB6df4FBFd178e63c33FD89), 20 ether - 0.2 ether); 97 | } 98 | 99 | function test_basic_sanity() public { 100 | om = new SelloutDao(dao); 101 | 102 | // hand over control to the Sellout proxy 103 | dao.updateDelegateKey(address(om)); 104 | 105 | assertTrue(!om.sold()); 106 | 107 | address payable omg = address(om); 108 | 109 | assertEq(address(omg).balance, 0); 110 | 111 | assertEq(om.hat(), address(0x0)); 112 | 113 | omg.call.value(1 ether).gas(100000)(""); 114 | 115 | assertEq(address(omg).balance, 1 ether); 116 | 117 | assertTrue(om.sold()); 118 | assertEq(om.hat(), address(this)); 119 | 120 | assertEq(weth.balanceOf(address(this)), 0); 121 | 122 | weth.deposit.value(100 ether)(); 123 | weth.transfer(address(om), 15 ether); 124 | 125 | assertEq(weth.balanceOf(address(om)), 15 ether); 126 | 127 | om.make(0xcd16CBdA54af2556EBB6df4FBFd178e63c33FD89, 0, 1, "Hola"); 128 | 129 | uint256 prop = om.prop(); 130 | assertEq(prop, 96); 131 | 132 | assertEq(weth.balanceOf(address(om)), 5 ether); 133 | 134 | // warp one period duration 135 | hevm.warp(now + dao.periodDuration()); 136 | 137 | assertEq(dao.getMemberProposalVote(0xcd16CBdA54af2556EBB6df4FBFd178e63c33FD89, prop), 0); 138 | om.vote(1); 139 | assertEq(dao.getMemberProposalVote(0xcd16CBdA54af2556EBB6df4FBFd178e63c33FD89, prop), 1); 140 | 141 | om.take(); 142 | 143 | assertEq(address(om).balance, 0); 144 | assertEq(weth.balanceOf(address(om)), 0); 145 | assertEq(weth.balanceOf(address(this)), 90 ether); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | dapp build 4 | 5 | . ~/load_mainnet 6 | 7 | export MOLOCH_KEY=0xcd16CBdA54af2556EBB6df4FBFd178e63c33FD89 8 | export MOLOCH_DELEGATE_KEY=0x72BA1965320ab5352FD6D68235Cc3C5306a6FFA2 9 | 10 | 11 | DAPP_TEST_TIMESTAMP=$(seth block latest timestamp) 12 | DAPP_TEST_NUMBER=$(seth block latest number) 13 | DAPP_TEST_ADDRESS=$MOLOCH_DELEGATE_KEY 14 | 15 | export DAPP_TEST_TIMESTAMP DAPP_TEST_NUMBER DAPP_TEST_ADDRESS 16 | 17 | export LANG=C.UTF-8 18 | 19 | hevm dapp-test --verbose 2 --rpc="$ETH_RPC_URL" --json-file=out/dapp.sol.json --match after --debug 20 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "homepage": "https://selloutdao.com", 5 | "private": true, 6 | "dependencies": { 7 | "ethers": "^4.0.36", 8 | "gh-pages": "^2.1.1", 9 | "qrcode.react": "^0.9.3", 10 | "react": "^16.9.0", 11 | "react-dom": "^16.9.0", 12 | "react-scripts": "3.1.1" 13 | }, 14 | "scripts": { 15 | "predeploy": "npm run build", 16 | "deploy": "gh-pages -d build", 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanexcool/sellout-dao/b07438f6556d00128db4dacb98a14b7814b118d1/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | SelloutDAO! 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanexcool/sellout-dao/b07438f6556d00128db4dacb98a14b7814b118d1/ui/public/logo192.png -------------------------------------------------------------------------------- /ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanexcool/sellout-dao/b07438f6556d00128db4dacb98a14b7814b118d1/ui/public/logo512.png -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /ui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 10s linear; 7 | height: 10vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ethers } from 'ethers'; 3 | import logo from './moloch.png'; 4 | import './App.css'; 5 | 6 | window.ethers = ethers 7 | const molochAbi = require('./abi/moloch.json') 8 | const selloutAbi = require('./abi/sellout.json') 9 | 10 | class App extends React.Component { 11 | state = { 12 | sold: false, 13 | hat: null 14 | } 15 | 16 | async componentDidMount() { 17 | // let provider = ethers.getDefaultProvider('homestead'); 18 | let provider = new ethers.providers.Web3Provider(window.web3.currentProvider); 19 | 20 | let moloch = new ethers.Contract('0x1fd169A4f5c59ACf79d0Fd5d91D1201EF1Bce9f1', molochAbi, provider); 21 | window.moloch = moloch 22 | let sellout = new ethers.Contract('0x829fE69F1feA3305C1aa0C1873b22835b87200d6', selloutAbi, provider); 23 | window.sellout = sellout 24 | 25 | let loop = async () => { 26 | let sold = await sellout.sold() 27 | let hat = await sellout.hat() 28 | this.setState({sold, hat}) 29 | } 30 | 31 | setInterval(loop, 10000); 32 | } 33 | 34 | render() { 35 | return ( 36 |
37 |
38 | logo 39 |

40 | Sell your voting power!
41 | Works on any Moloch-like DAO 42 |

43 |

SOLD to 0x2AF412...

44 | 50 | Read the story 51 | 52 |

53 | The initial trial of the SelloutDAO was a success! Stay tuned for version 2 ;) 54 |

55 | 61 | Source Code 62 | 63 |

Made by Mariano Conti (@nanexcool) for @ETHBerlin

64 |
65 |
66 | ); 67 | 68 | } 69 | } 70 | 71 | export default App; 72 | -------------------------------------------------------------------------------- /ui/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /ui/src/Proposal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ethers } from 'ethers'; 3 | 4 | class Proposal extends React.Component { 5 | 6 | constructor(props) { 7 | super() 8 | this.provider = props.provider 9 | } 10 | 11 | componentDidMount() { 12 | this.provider.getBalance('0x1fd169A4f5c59ACf79d0Fd5d91D1201EF1Bce9f1').then((balance) => { 13 | let etherString = ethers.utils.formatEther(balance); 14 | console.log("Balance: " + etherString); 15 | }); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |

Proposal

22 |
23 | ) 24 | } 25 | } 26 | 27 | export default Proposal 28 | -------------------------------------------------------------------------------- /ui/src/abi/moloch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "processingReward", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "uint256" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": true, 18 | "inputs": [ 19 | { 20 | "name": "memberAddress", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "proposalIndex", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "getMemberProposalVote", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "uint8" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "view", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "getCurrentPeriod", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": true, 55 | "inputs": [ 56 | { 57 | "name": "", 58 | "type": "address" 59 | } 60 | ], 61 | "name": "members", 62 | "outputs": [ 63 | { 64 | "name": "delegateKey", 65 | "type": "address" 66 | }, 67 | { 68 | "name": "shares", 69 | "type": "uint256" 70 | }, 71 | { 72 | "name": "exists", 73 | "type": "bool" 74 | }, 75 | { 76 | "name": "highestIndexYesVote", 77 | "type": "uint256" 78 | } 79 | ], 80 | "payable": false, 81 | "stateMutability": "view", 82 | "type": "function" 83 | }, 84 | { 85 | "constant": true, 86 | "inputs": [], 87 | "name": "totalSharesRequested", 88 | "outputs": [ 89 | { 90 | "name": "", 91 | "type": "uint256" 92 | } 93 | ], 94 | "payable": false, 95 | "stateMutability": "view", 96 | "type": "function" 97 | }, 98 | { 99 | "constant": false, 100 | "inputs": [ 101 | { 102 | "name": "newDelegateKey", 103 | "type": "address" 104 | } 105 | ], 106 | "name": "updateDelegateKey", 107 | "outputs": [], 108 | "payable": false, 109 | "stateMutability": "nonpayable", 110 | "type": "function" 111 | }, 112 | { 113 | "constant": true, 114 | "inputs": [], 115 | "name": "totalShares", 116 | "outputs": [ 117 | { 118 | "name": "", 119 | "type": "uint256" 120 | } 121 | ], 122 | "payable": false, 123 | "stateMutability": "view", 124 | "type": "function" 125 | }, 126 | { 127 | "constant": true, 128 | "inputs": [ 129 | { 130 | "name": "", 131 | "type": "uint256" 132 | } 133 | ], 134 | "name": "proposalQueue", 135 | "outputs": [ 136 | { 137 | "name": "proposer", 138 | "type": "address" 139 | }, 140 | { 141 | "name": "applicant", 142 | "type": "address" 143 | }, 144 | { 145 | "name": "sharesRequested", 146 | "type": "uint256" 147 | }, 148 | { 149 | "name": "startingPeriod", 150 | "type": "uint256" 151 | }, 152 | { 153 | "name": "yesVotes", 154 | "type": "uint256" 155 | }, 156 | { 157 | "name": "noVotes", 158 | "type": "uint256" 159 | }, 160 | { 161 | "name": "processed", 162 | "type": "bool" 163 | }, 164 | { 165 | "name": "didPass", 166 | "type": "bool" 167 | }, 168 | { 169 | "name": "aborted", 170 | "type": "bool" 171 | }, 172 | { 173 | "name": "tokenTribute", 174 | "type": "uint256" 175 | }, 176 | { 177 | "name": "details", 178 | "type": "string" 179 | }, 180 | { 181 | "name": "maxTotalSharesAtYesVote", 182 | "type": "uint256" 183 | } 184 | ], 185 | "payable": false, 186 | "stateMutability": "view", 187 | "type": "function" 188 | }, 189 | { 190 | "constant": true, 191 | "inputs": [ 192 | { 193 | "name": "", 194 | "type": "address" 195 | } 196 | ], 197 | "name": "memberAddressByDelegateKey", 198 | "outputs": [ 199 | { 200 | "name": "", 201 | "type": "address" 202 | } 203 | ], 204 | "payable": false, 205 | "stateMutability": "view", 206 | "type": "function" 207 | }, 208 | { 209 | "constant": true, 210 | "inputs": [], 211 | "name": "gracePeriodLength", 212 | "outputs": [ 213 | { 214 | "name": "", 215 | "type": "uint256" 216 | } 217 | ], 218 | "payable": false, 219 | "stateMutability": "view", 220 | "type": "function" 221 | }, 222 | { 223 | "constant": true, 224 | "inputs": [], 225 | "name": "abortWindow", 226 | "outputs": [ 227 | { 228 | "name": "", 229 | "type": "uint256" 230 | } 231 | ], 232 | "payable": false, 233 | "stateMutability": "view", 234 | "type": "function" 235 | }, 236 | { 237 | "constant": true, 238 | "inputs": [], 239 | "name": "getProposalQueueLength", 240 | "outputs": [ 241 | { 242 | "name": "", 243 | "type": "uint256" 244 | } 245 | ], 246 | "payable": false, 247 | "stateMutability": "view", 248 | "type": "function" 249 | }, 250 | { 251 | "constant": true, 252 | "inputs": [], 253 | "name": "summoningTime", 254 | "outputs": [ 255 | { 256 | "name": "", 257 | "type": "uint256" 258 | } 259 | ], 260 | "payable": false, 261 | "stateMutability": "view", 262 | "type": "function" 263 | }, 264 | { 265 | "constant": true, 266 | "inputs": [], 267 | "name": "votingPeriodLength", 268 | "outputs": [ 269 | { 270 | "name": "", 271 | "type": "uint256" 272 | } 273 | ], 274 | "payable": false, 275 | "stateMutability": "view", 276 | "type": "function" 277 | }, 278 | { 279 | "constant": false, 280 | "inputs": [ 281 | { 282 | "name": "sharesToBurn", 283 | "type": "uint256" 284 | } 285 | ], 286 | "name": "ragequit", 287 | "outputs": [], 288 | "payable": false, 289 | "stateMutability": "nonpayable", 290 | "type": "function" 291 | }, 292 | { 293 | "constant": true, 294 | "inputs": [], 295 | "name": "proposalDeposit", 296 | "outputs": [ 297 | { 298 | "name": "", 299 | "type": "uint256" 300 | } 301 | ], 302 | "payable": false, 303 | "stateMutability": "view", 304 | "type": "function" 305 | }, 306 | { 307 | "constant": true, 308 | "inputs": [ 309 | { 310 | "name": "startingPeriod", 311 | "type": "uint256" 312 | } 313 | ], 314 | "name": "hasVotingPeriodExpired", 315 | "outputs": [ 316 | { 317 | "name": "", 318 | "type": "bool" 319 | } 320 | ], 321 | "payable": false, 322 | "stateMutability": "view", 323 | "type": "function" 324 | }, 325 | { 326 | "constant": false, 327 | "inputs": [ 328 | { 329 | "name": "applicant", 330 | "type": "address" 331 | }, 332 | { 333 | "name": "tokenTribute", 334 | "type": "uint256" 335 | }, 336 | { 337 | "name": "sharesRequested", 338 | "type": "uint256" 339 | }, 340 | { 341 | "name": "details", 342 | "type": "string" 343 | } 344 | ], 345 | "name": "submitProposal", 346 | "outputs": [], 347 | "payable": false, 348 | "stateMutability": "nonpayable", 349 | "type": "function" 350 | }, 351 | { 352 | "constant": false, 353 | "inputs": [ 354 | { 355 | "name": "proposalIndex", 356 | "type": "uint256" 357 | }, 358 | { 359 | "name": "uintVote", 360 | "type": "uint8" 361 | } 362 | ], 363 | "name": "submitVote", 364 | "outputs": [], 365 | "payable": false, 366 | "stateMutability": "nonpayable", 367 | "type": "function" 368 | }, 369 | { 370 | "constant": true, 371 | "inputs": [ 372 | { 373 | "name": "highestIndexYesVote", 374 | "type": "uint256" 375 | } 376 | ], 377 | "name": "canRagequit", 378 | "outputs": [ 379 | { 380 | "name": "", 381 | "type": "bool" 382 | } 383 | ], 384 | "payable": false, 385 | "stateMutability": "view", 386 | "type": "function" 387 | }, 388 | { 389 | "constant": true, 390 | "inputs": [], 391 | "name": "guildBank", 392 | "outputs": [ 393 | { 394 | "name": "", 395 | "type": "address" 396 | } 397 | ], 398 | "payable": false, 399 | "stateMutability": "view", 400 | "type": "function" 401 | }, 402 | { 403 | "constant": true, 404 | "inputs": [], 405 | "name": "dilutionBound", 406 | "outputs": [ 407 | { 408 | "name": "", 409 | "type": "uint256" 410 | } 411 | ], 412 | "payable": false, 413 | "stateMutability": "view", 414 | "type": "function" 415 | }, 416 | { 417 | "constant": true, 418 | "inputs": [], 419 | "name": "periodDuration", 420 | "outputs": [ 421 | { 422 | "name": "", 423 | "type": "uint256" 424 | } 425 | ], 426 | "payable": false, 427 | "stateMutability": "view", 428 | "type": "function" 429 | }, 430 | { 431 | "constant": true, 432 | "inputs": [], 433 | "name": "approvedToken", 434 | "outputs": [ 435 | { 436 | "name": "", 437 | "type": "address" 438 | } 439 | ], 440 | "payable": false, 441 | "stateMutability": "view", 442 | "type": "function" 443 | }, 444 | { 445 | "constant": false, 446 | "inputs": [ 447 | { 448 | "name": "proposalIndex", 449 | "type": "uint256" 450 | } 451 | ], 452 | "name": "abort", 453 | "outputs": [], 454 | "payable": false, 455 | "stateMutability": "nonpayable", 456 | "type": "function" 457 | }, 458 | { 459 | "constant": false, 460 | "inputs": [ 461 | { 462 | "name": "proposalIndex", 463 | "type": "uint256" 464 | } 465 | ], 466 | "name": "processProposal", 467 | "outputs": [], 468 | "payable": false, 469 | "stateMutability": "nonpayable", 470 | "type": "function" 471 | }, 472 | { 473 | "inputs": [ 474 | { 475 | "name": "summoner", 476 | "type": "address" 477 | }, 478 | { 479 | "name": "_approvedToken", 480 | "type": "address" 481 | }, 482 | { 483 | "name": "_periodDuration", 484 | "type": "uint256" 485 | }, 486 | { 487 | "name": "_votingPeriodLength", 488 | "type": "uint256" 489 | }, 490 | { 491 | "name": "_gracePeriodLength", 492 | "type": "uint256" 493 | }, 494 | { 495 | "name": "_abortWindow", 496 | "type": "uint256" 497 | }, 498 | { 499 | "name": "_proposalDeposit", 500 | "type": "uint256" 501 | }, 502 | { 503 | "name": "_dilutionBound", 504 | "type": "uint256" 505 | }, 506 | { 507 | "name": "_processingReward", 508 | "type": "uint256" 509 | } 510 | ], 511 | "payable": false, 512 | "stateMutability": "nonpayable", 513 | "type": "constructor" 514 | }, 515 | { 516 | "anonymous": false, 517 | "inputs": [ 518 | { 519 | "indexed": false, 520 | "name": "proposalIndex", 521 | "type": "uint256" 522 | }, 523 | { 524 | "indexed": true, 525 | "name": "delegateKey", 526 | "type": "address" 527 | }, 528 | { 529 | "indexed": true, 530 | "name": "memberAddress", 531 | "type": "address" 532 | }, 533 | { 534 | "indexed": true, 535 | "name": "applicant", 536 | "type": "address" 537 | }, 538 | { 539 | "indexed": false, 540 | "name": "tokenTribute", 541 | "type": "uint256" 542 | }, 543 | { 544 | "indexed": false, 545 | "name": "sharesRequested", 546 | "type": "uint256" 547 | } 548 | ], 549 | "name": "SubmitProposal", 550 | "type": "event" 551 | }, 552 | { 553 | "anonymous": false, 554 | "inputs": [ 555 | { 556 | "indexed": true, 557 | "name": "proposalIndex", 558 | "type": "uint256" 559 | }, 560 | { 561 | "indexed": true, 562 | "name": "delegateKey", 563 | "type": "address" 564 | }, 565 | { 566 | "indexed": true, 567 | "name": "memberAddress", 568 | "type": "address" 569 | }, 570 | { 571 | "indexed": false, 572 | "name": "uintVote", 573 | "type": "uint8" 574 | } 575 | ], 576 | "name": "SubmitVote", 577 | "type": "event" 578 | }, 579 | { 580 | "anonymous": false, 581 | "inputs": [ 582 | { 583 | "indexed": true, 584 | "name": "proposalIndex", 585 | "type": "uint256" 586 | }, 587 | { 588 | "indexed": true, 589 | "name": "applicant", 590 | "type": "address" 591 | }, 592 | { 593 | "indexed": true, 594 | "name": "memberAddress", 595 | "type": "address" 596 | }, 597 | { 598 | "indexed": false, 599 | "name": "tokenTribute", 600 | "type": "uint256" 601 | }, 602 | { 603 | "indexed": false, 604 | "name": "sharesRequested", 605 | "type": "uint256" 606 | }, 607 | { 608 | "indexed": false, 609 | "name": "didPass", 610 | "type": "bool" 611 | } 612 | ], 613 | "name": "ProcessProposal", 614 | "type": "event" 615 | }, 616 | { 617 | "anonymous": false, 618 | "inputs": [ 619 | { 620 | "indexed": true, 621 | "name": "memberAddress", 622 | "type": "address" 623 | }, 624 | { 625 | "indexed": false, 626 | "name": "sharesToBurn", 627 | "type": "uint256" 628 | } 629 | ], 630 | "name": "Ragequit", 631 | "type": "event" 632 | }, 633 | { 634 | "anonymous": false, 635 | "inputs": [ 636 | { 637 | "indexed": true, 638 | "name": "proposalIndex", 639 | "type": "uint256" 640 | }, 641 | { 642 | "indexed": false, 643 | "name": "applicantAddress", 644 | "type": "address" 645 | } 646 | ], 647 | "name": "Abort", 648 | "type": "event" 649 | }, 650 | { 651 | "anonymous": false, 652 | "inputs": [ 653 | { 654 | "indexed": true, 655 | "name": "memberAddress", 656 | "type": "address" 657 | }, 658 | { 659 | "indexed": false, 660 | "name": "newDelegateKey", 661 | "type": "address" 662 | } 663 | ], 664 | "name": "UpdateDelegateKey", 665 | "type": "event" 666 | }, 667 | { 668 | "anonymous": false, 669 | "inputs": [ 670 | { 671 | "indexed": true, 672 | "name": "summoner", 673 | "type": "address" 674 | }, 675 | { 676 | "indexed": false, 677 | "name": "shares", 678 | "type": "uint256" 679 | } 680 | ], 681 | "name": "SummonComplete", 682 | "type": "event" 683 | } 684 | ] -------------------------------------------------------------------------------- /ui/src/abi/sellout.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"sold","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"take","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"voted","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"dao","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"buy","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"who","type":"address"},{"name":"tribute","type":"uint256"},{"name":"shares","type":"uint256"},{"name":"text","type":"string"}],"name":"make","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"val","type":"uint8"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"prop","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"hat","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"dao_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"}] -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /ui/src/moloch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanexcool/sellout-dao/b07438f6556d00128db4dacb98a14b7814b118d1/ui/src/moloch.png -------------------------------------------------------------------------------- /ui/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------