├── .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 |
40 | Sell your voting power!
41 | Works on any Moloch-like DAO
42 |
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 |