From 3ca687f7f1ea522b55b346309704f1fdfcdd3d5e Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Wed, 22 Apr 2020 14:18:12 -0400 Subject: [PATCH 01/13] Test setup allowing the submission of a Moloch proposal --- test-environment.config.js | 2 +- test/dai.json | 339 +++++++++++++++++++++++++++++++++++++ test/endaoment-test.js | 22 ++- test/helpers.js | 19 +++ 4 files changed, 377 insertions(+), 5 deletions(-) create mode 100644 test/dai.json create mode 100644 test/helpers.js diff --git a/test-environment.config.js b/test-environment.config.js index b102b27..51dd011 100644 --- a/test-environment.config.js +++ b/test-environment.config.js @@ -4,7 +4,7 @@ const infura_key = process.env.INFURA_ID; module.exports = { node: { fork: `https://mainnet.infura.io/v3/${infura_key}`, - unlocked_accounts: [], + unlocked_accounts: [process.env.DAI_FUNDER], allowUnlimitedContractSize: true, // TODO remove the need for this }, }; diff --git a/test/dai.json b/test/dai.json new file mode 100644 index 0000000..2528490 --- /dev/null +++ b/test/dai.json @@ -0,0 +1,339 @@ +{ + "abi": [ + { + "inputs": [ + { "internalType": "uint256", "name": "chainId_", "type": "uint256" } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "guy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": true, + "inputs": [ + { + "indexed": true, + "internalType": "bytes4", + "name": "sig", + "type": "bytes4" + }, + { + "indexed": true, + "internalType": "address", + "name": "usr", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg1", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "arg2", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "LogNote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wad", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": true, + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "burn", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "guy", "type": "address" } + ], + "name": "deny", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "mint", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "src", "type": "address" }, + { "internalType": "address", "name": "dst", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "move", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "holder", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "expiry", "type": "uint256" }, + { "internalType": "bool", "name": "allowed", "type": "bool" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "pull", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "usr", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "push", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "guy", "type": "address" } + ], + "name": "rely", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "dst", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { "internalType": "address", "name": "src", "type": "address" }, + { "internalType": "address", "name": "dst", "type": "address" }, + { "internalType": "uint256", "name": "wad", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "version", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "wards", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ] + } diff --git a/test/endaoment-test.js b/test/endaoment-test.js index 07f24f0..c919685 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -1,14 +1,16 @@ -const { accounts, contract } = require('@openzeppelin/test-environment'); +require('dotenv').config(); +const { accounts, contract, web3 } = require('@openzeppelin/test-environment'); const { expect } = require('chai'); const Moloch = contract.fromArtifact('Moloch'); +const { toWeiDai, stealDai, approveDai } = require('./helpers'); describe('Moloch', () => { - const [ summoner ] = accounts; + const [ summoner, member1 ] = accounts; before(async () => { this.instance = await Moloch.new( summoner, - "0x6B175474E89094C44Da98b954EedeAC495271d0F", // address _approvedToken (DAI address) + process.env.DAI_ADDR, // address _approvedToken (DAI address) 17280, // uint256 _periodDuration 35, // uint256 _votingPeriodLength -- 35 periods? 35, // uint256 _gracePeriodLength -- 35 periods? @@ -18,11 +20,23 @@ describe('Moloch', () => { "1000000000000000000", // uint256 _processingReward -- 1 Dai "COVID-19 Relief", "Donate funds to selected organizations helping with COVID-19 relief", - {from: summoner}); + {from: summoner} + ); + + await stealDai(100, summoner); + await stealDai(200, member1); + await approveDai(summoner, this.instance.address); + await approveDai(member1, this.instance.address); }); it('should see the deployed Moloch contract', async () => { expect(this.instance.address.startsWith('0x')).to.be.true; expect(this.instance.address.length).to.equal(42); }); + + it('should allow a membership proposal', async () => { + await this.instance.submitProposal(member1, toWeiDai(200), 200, "member1", {from: summoner}); + const proposal = await this.instance.proposalQueue(0); + expect(proposal.details).to.equal("member1"); + }); }); diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 0000000..43403a3 --- /dev/null +++ b/test/helpers.js @@ -0,0 +1,19 @@ +const { web3 } = require('@openzeppelin/test-environment'); +const daiAbi = require('./dai.json').abi; + +const daiFunder = process.env.DAI_FUNDER; +const daiAddress = process.env.DAI_ADDR; + +exports.toWeiDai = (dai) => { + return web3.utils.toWei(dai.toString(), 'ether'); +}; + +exports.stealDai = async (amount, receiver) => { + const daiContract = new web3.eth.Contract(daiAbi, daiAddress); + await daiContract.methods.transfer(receiver, this.toWeiDai(amount)).send({from: daiFunder}); +}; + +exports.approveDai = async (holder, spender) => { + const daiContract = new web3.eth.Contract(daiAbi, daiAddress); + await daiContract.methods.approve(spender, this.toWeiDai(1000000000000)).send({from: holder}); +} From fa6cc5c25d56a81dcb82449fce0f6cff7bc55d91 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Wed, 22 Apr 2020 14:24:14 -0400 Subject: [PATCH 02/13] Set up tests to approve two membership requests to the DAO --- package-lock.json | 24 ------------------- package.json | 3 +-- test/endaoment-test.js | 53 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index c539aaa..bcf3325 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19607,30 +19607,6 @@ "mimic-fn": "^2.1.0" } }, - "openzeppelin-test-helpers": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/openzeppelin-test-helpers/-/openzeppelin-test-helpers-0.5.1.tgz", - "integrity": "sha512-xIMusuVCKXfR+qwbyiXpV7UaqaW3H/Q3axQ2nSce8o3R9TQoxrZdbKvMZdJdg6Ro4cpRDQ3yZtniz4vSIbSZHg==", - "dev": true, - "requires": { - "@truffle/contract": "^4.0.35", - "ansi-colors": "^3.2.3", - "chai-bn": "^0.2.0", - "ethjs-abi": "^0.2.1", - "lodash.flatten": "^4.4.0", - "semver": "^5.6.0", - "web3": "^1.2.1", - "web3-utils": "^1.2.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "original-require": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/original-require/-/original-require-1.0.1.tgz", diff --git a/package.json b/package.json index b93d5ea..f95de38 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "@openzeppelin/test-environment": "^0.1.4", "@openzeppelin/test-helpers": "^0.5.5", "chai": "^4.2.0", - "mocha": "^7.1.1", - "openzeppelin-test-helpers": "^0.5.1" + "mocha": "^7.1.1" } } diff --git a/test/endaoment-test.js b/test/endaoment-test.js index c919685..81ebadb 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -1,20 +1,28 @@ require('dotenv').config(); const { accounts, contract, web3 } = require('@openzeppelin/test-environment'); +const { time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Moloch = contract.fromArtifact('Moloch'); const { toWeiDai, stealDai, approveDai } = require('./helpers'); +const PERIOD_DURATION = 17280; +const VOTING_PERIODS = 35; +const GRACE_PERIODS = 35; +const ABORT_WINDOW = 5; +const VOTING_DURATION = VOTING_PERIODS * PERIOD_DURATION; +const GRACE_DURATION = GRACE_PERIODS * PERIOD_DURATION; + describe('Moloch', () => { - const [ summoner, member1 ] = accounts; + const [ summoner, member1, member2 ] = accounts; before(async () => { this.instance = await Moloch.new( summoner, - process.env.DAI_ADDR, // address _approvedToken (DAI address) - 17280, // uint256 _periodDuration - 35, // uint256 _votingPeriodLength -- 35 periods? - 35, // uint256 _gracePeriodLength -- 35 periods? - 5, // uint256 _abortWindow -- 5 periods? + process.env.DAI_ADDR, // address _approvedToken (DAI address) + PERIOD_DURATION, // uint256 _periodDuration + VOTING_PERIODS, // uint256 _votingPeriodLength -- 35 periods? + GRACE_PERIODS, // uint256 _gracePeriodLength -- 35 periods? + ABORT_WINDOW, // uint256 _abortWindow -- 5 periods? "100000000000000000000", // uint256 _proposalDeposit -- 100 Dai 3, // uint256 _dilutionBound -- 3 "1000000000000000000", // uint256 _processingReward -- 1 Dai @@ -23,10 +31,12 @@ describe('Moloch', () => { {from: summoner} ); - await stealDai(100, summoner); - await stealDai(200, member1); + await stealDai(1000, summoner); + await stealDai(20000, member1); + await stealDai(20000, member2); await approveDai(summoner, this.instance.address); await approveDai(member1, this.instance.address); + await approveDai(member2, this.instance.address); }); it('should see the deployed Moloch contract', async () => { @@ -34,9 +44,32 @@ describe('Moloch', () => { expect(this.instance.address.length).to.equal(42); }); - it('should allow a membership proposal', async () => { - await this.instance.submitProposal(member1, toWeiDai(200), 200, "member1", {from: summoner}); + it('should allow a membership proposal & vote', async () => { + await this.instance.submitProposal(member1, toWeiDai(2000), 2000, "member1", {from: summoner}); const proposal = await this.instance.proposalQueue(0); expect(proposal.details).to.equal("member1"); + + await time.increase(PERIOD_DURATION); + await this.instance.submitVote(0, 1, {from: summoner}); + await time.increase(VOTING_DURATION + GRACE_DURATION); + await this.instance.processProposal(0, {from: summoner}); + + const memberInfo = await this.instance.members(member1); + expect(memberInfo.shares.toString()).to.equal('2000'); + }); + + it('should allow another membership proposal & vote', async () => { + await this.instance.submitProposal(member2, toWeiDai(3000), 3000, "member2", {from: member1}); + const proposal = await this.instance.proposalQueue(1); + expect(proposal.details).to.equal("member2"); + + await time.increase(PERIOD_DURATION); + await this.instance.submitVote(1, 2, {from: summoner}); + await this.instance.submitVote(1, 1, {from: member1}); + await time.increase(VOTING_DURATION + GRACE_DURATION); + await this.instance.processProposal(1, {from: summoner}); + + const memberInfo = await this.instance.members(member2); + expect(memberInfo.shares.toString()).to.equal('3000'); }); }); From 4977fe3423dcb7b68d4b2c141ef4a610f4de888c Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Wed, 22 Apr 2020 22:37:34 -0400 Subject: [PATCH 03/13] Implement grant proposals Special methods for submission & processing of proposals --- contracts/Moloch.sol | 119 +++++++++++++++++++++++++++++++++++++++++-- contracts/Pool.sol | 2 +- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/contracts/Moloch.sol b/contracts/Moloch.sol index 7fd19e4..d3f1820 100644 --- a/contracts/Moloch.sol +++ b/contracts/Moloch.sol @@ -34,8 +34,10 @@ contract Moloch { EVENTS ***************/ event SubmitProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, address indexed applicant, uint256 tokenTribute, uint256 sharesRequested); + event SubmitGrantProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, address indexed applicant, uint256 tokenGrant, uint256 grantDuration); event SubmitVote(uint256 indexed proposalIndex, address indexed delegateKey, address indexed memberAddress, uint8 uintVote); event ProcessProposal(uint256 indexed proposalIndex, address indexed applicant, address indexed memberAddress, uint256 tokenTribute, uint256 sharesRequested, bool didPass); + event ProcessGrantProposal(uint256 indexed proposalIndex, address indexed applicant, address indexed memberAddress, uint256 tokenGrant, uint256 grantDuration, bool didPass); event Ragequit(address indexed memberAddress, uint256 sharesToBurn); event Abort(uint256 indexed proposalIndex, address applicantAddress); event UpdateDelegateKey(address indexed memberAddress, address newDelegateKey); @@ -53,6 +55,12 @@ contract Moloch { No } + enum ProposalKind { + Membership, + Grant, + Revocation + } + struct Member { address delegateKey; // the key responsible for submitting proposals and voting - defaults to member address unless updated uint256 shares; // the # of shares assigned to this member @@ -62,7 +70,7 @@ contract Moloch { struct Proposal { address proposer; // the member who submitted the proposal - address applicant; // the applicant who wishes to become a member - this key will be used for withdrawals + address applicant; // the applicant who wishes to become a member or receive a grant - this key will be used for withdrawals uint256 sharesRequested; // the # of shares the applicant is requesting uint256 startingPeriod; // the period in which voting can start for this proposal uint256 yesVotes; // the total number of YES votes for this proposal @@ -70,10 +78,12 @@ contract Moloch { bool processed; // true only if the proposal has been processed bool didPass; // true only if the proposal passed bool aborted; // true only if applicant calls "abort" fn before end of voting period - uint256 tokenTribute; // amount of tokens offered as tribute + uint256 tokenTribute; // amount of tokens offered as tribute or given as grant string details; // proposal details - could be IPFS hash, plaintext, or JSON uint256 maxTotalSharesAtYesVote; // the maximum # of total shares encountered at a yes vote on this proposal mapping (address => Vote) votesByMember; // the votes on this proposal by each member + ProposalKind kind; // the kind of proposal + uint256 grantDuration; // how long the grant toknes will be streamed } mapping (address => Member) public members; @@ -191,7 +201,9 @@ contract Moloch { aborted: false, tokenTribute: tokenTribute, details: details, - maxTotalSharesAtYesVote: 0 + maxTotalSharesAtYesVote: 0, + kind: ProposalKind.Membership, + grantDuration: 0 }); // ... and append it to the queue @@ -201,6 +213,53 @@ contract Moloch { emit SubmitProposal(proposalIndex, msg.sender, memberAddress, applicant, tokenTribute, sharesRequested); } + function submitGrantProposal( + address applicant, + uint256 tokenGrant, + uint256 grantDuration, + string memory details + ) + public + onlyDelegate + { + require(applicant != address(0), "Moloch::submitProposal - applicant cannot be 0"); + + address memberAddress = memberAddressByDelegateKey[msg.sender]; + + // collect proposal deposit from proposer and store it in the Moloch until the proposal is processed + require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed"); + + // compute startingPeriod for proposal + uint256 startingPeriod = max( + getCurrentPeriod(), + proposalQueue.length == 0 ? 0 : proposalQueue[proposalQueue.length.sub(1)].startingPeriod + ).add(1); + + // create proposal ... + Proposal memory proposal = Proposal({ + proposer: memberAddress, + applicant: applicant, + sharesRequested: 0, + startingPeriod: startingPeriod, + yesVotes: 0, + noVotes: 0, + processed: false, + didPass: false, + aborted: false, + tokenTribute: tokenGrant, + details: details, + maxTotalSharesAtYesVote: 0, + kind: ProposalKind.Grant, + grantDuration: grantDuration + }); + + // ... and append it to the queue + proposalQueue.push(proposal); + + uint256 proposalIndex = proposalQueue.length.sub(1); + emit SubmitGrantProposal(proposalIndex, msg.sender, memberAddress, applicant, tokenGrant, grantDuration); + } + function submitVote(uint256 proposalIndex, uint8 uintVote) public onlyDelegate { address memberAddress = memberAddressByDelegateKey[msg.sender]; Member storage member = members[memberAddress]; @@ -248,6 +307,7 @@ contract Moloch { require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed"); require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed"); require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed"); + require(proposal.kind == ProposalKind.Membership, "Endaoment::processProposal - not a membership proposal"); proposal.processed = true; totalSharesRequested = totalSharesRequested.sub(proposal.sharesRequested); @@ -322,6 +382,59 @@ contract Moloch { ); } + function processGrantProposal(uint256 proposalIndex) public { + require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist"); + Proposal storage proposal = proposalQueue[proposalIndex]; + + require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed"); + require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed"); + require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed"); + require(proposal.kind == ProposalKind.Grant, "Endaoment::processGrantProposal - not a grant proposal"); + + proposal.processed = true; + + bool didPass = proposal.yesVotes > proposal.noVotes; + + // PROPOSAL PASSED + if (didPass && !proposal.aborted) { + + proposal.didPass = true; + + // TODO: Call guildbank funciton to start the stream! + + // transfer tokens to guild bank + // require( + // approvedToken.transfer(address(guildBank), proposal.tokenTribute), + // "Moloch::processProposal - token transfer to guild bank failed" + // ); + + // PROPOSAL FAILED OR ABORTED + } else { + // TODO: anything? + } + + // send msg.sender the processingReward + require( + approvedToken.transfer(msg.sender, processingReward), + "Moloch::processProposal - failed to send processing reward to msg.sender" + ); + + // return deposit to proposer (subtract processing reward) + require( + approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)), + "Moloch::processProposal - failed to return proposal deposit to proposer" + ); + + emit ProcessProposal( + proposalIndex, + proposal.applicant, + proposal.proposer, + proposal.tokenTribute, + proposal.grantDuration, + didPass + ); + } + function ragequit(uint256 sharesToBurn) public onlyMember { uint256 initialTotalShares = totalShares; diff --git a/contracts/Pool.sol b/contracts/Pool.sol index 9486bb4..cb53779 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -127,7 +127,7 @@ contract MolochPool { while (i < toIndex) { - (, applicant, sharesRequested, , , , processed, didPass, aborted, tokenTribute, , maxTotalSharesAtYesVote) = moloch.proposalQueue(i); + (, applicant, sharesRequested, , , , processed, didPass, aborted, tokenTribute, , maxTotalSharesAtYesVote, , ) = moloch.proposalQueue(i); if (!processed) { break; } From d739676cb227d559133ac3d95a2e07a0cf33d888 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Thu, 23 Apr 2020 06:52:41 -0400 Subject: [PATCH 04/13] Checks to ensure sufficient balances for grant proposals --- contracts/Moloch.sol | 10 +++++++++- test/endaoment-test.js | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/contracts/Moloch.sol b/contracts/Moloch.sol index d3f1820..fca4b33 100644 --- a/contracts/Moloch.sol +++ b/contracts/Moloch.sol @@ -213,6 +213,12 @@ contract Moloch { emit SubmitProposal(proposalIndex, msg.sender, memberAddress, applicant, tokenTribute, sharesRequested); } + // + // when users may ragequit. + // Call guildbank functions + // Test proposals work and emit expected events + // Integrate w/ Sablier and start a stream + function submitGrantProposal( address applicant, uint256 tokenGrant, @@ -223,6 +229,7 @@ contract Moloch { onlyDelegate { require(applicant != address(0), "Moloch::submitProposal - applicant cannot be 0"); + require(tokenGrant <= approvedToken.balanceOf(address(guildBank)), "Endaoment::submitGrantProposal - grant is greater than treasury"); address memberAddress = memberAddressByDelegateKey[msg.sender]; @@ -394,9 +401,10 @@ contract Moloch { proposal.processed = true; bool didPass = proposal.yesVotes > proposal.noVotes; + bool hasFunds = (proposal.tokenTribute < approvedToken.balanceOf(address(guildBank))); // PROPOSAL PASSED - if (didPass && !proposal.aborted) { + if (didPass && !proposal.aborted && hasFunds) { proposal.didPass = true; diff --git a/test/endaoment-test.js b/test/endaoment-test.js index 81ebadb..cd0a67b 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -1,6 +1,6 @@ require('dotenv').config(); const { accounts, contract, web3 } = require('@openzeppelin/test-environment'); -const { time } = require('@openzeppelin/test-helpers'); +const { time, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Moloch = contract.fromArtifact('Moloch'); const { toWeiDai, stealDai, approveDai } = require('./helpers'); @@ -12,8 +12,10 @@ const ABORT_WINDOW = 5; const VOTING_DURATION = VOTING_PERIODS * PERIOD_DURATION; const GRACE_DURATION = GRACE_PERIODS * PERIOD_DURATION; +const GrantDuration = 30*24*60*60; + describe('Moloch', () => { - const [ summoner, member1, member2 ] = accounts; + const [ summoner, member1, member2, grantee1 ] = accounts; before(async () => { this.instance = await Moloch.new( @@ -72,4 +74,38 @@ describe('Moloch', () => { const memberInfo = await this.instance.members(member2); expect(memberInfo.shares.toString()).to.equal('3000'); }); + + it('should not allow a grant proposal for more than guildbank owns', async () => { + await expectRevert( + this.instance.submitGrantProposal(grantee1, toWeiDai(10000), + 30*24*60*60, "grantee1", {from: member2}), + "Endaoment::submitGrantProposal - grant is greater than treasury" + ); + }); + + it('should allow a grant proposal from the guildbank', async () => { + await this.instance.submitGrantProposal(grantee1, toWeiDai(1000), GrantDuration, "grantee1", {from: member2}); + const proposal = await this.instance.proposalQueue(2); + expect(proposal.details).to.equal('grantee1'); + }); + + it('should allow memebers to vote on a grant proposal', async () => { + await time.increase(PERIOD_DURATION); + + await this.instance.submitVote(2, 2, {from: member1}); + await this.instance.submitVote(2, 1, {from: member2}); + + const proposal = await this.instance.proposalQueue(2); + + expect(proposal.yesVotes.toString()).to.equal('3000'); + expect(proposal.noVotes.toString()).to.equal('2000'); + }); + + it('should process a successful grant proposal', async () => { + await time.increase(VOTING_DURATION + GRACE_DURATION); + + await this.instance.processGrantProposal(2, {from: grantee1}); + }); + + // TODO: Test proposal fails if ragequitters deplete required funds }); From 854e81c6fef824037d36c56dcc79be2fce76609b Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Thu, 23 Apr 2020 07:31:07 -0400 Subject: [PATCH 05/13] Create sablier stream in GuildBank --- contracts/GuildBank.sol | 11 +++++++++++ contracts/Moloch.sol | 1 + test/endaoment-test.js | 7 ++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/contracts/GuildBank.sol b/contracts/GuildBank.sol index ccac1b3..4b30176 100644 --- a/contracts/GuildBank.sol +++ b/contracts/GuildBank.sol @@ -4,15 +4,22 @@ import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; +interface ISablier { + function createStream(address recipient, uint256 deposit, address tokenAddress, uint256 startTime, uint256 stopTime) external returns (uint256); +} + contract GuildBank is Ownable { using SafeMath for uint256; IERC20 public approvedToken; // approved token contract reference + ISablier public sablier; event Withdrawal(address indexed receiver, uint256 amount); constructor(address approvedTokenAddress) public { approvedToken = IERC20(approvedTokenAddress); + sablier = ISablier(0xA4fc358455Febe425536fd1878bE67FfDBDEC59a); + approvedToken.approve(address(sablier), uint256(-1)); } function withdraw(address receiver, uint256 shares, uint256 totalShares) public onlyOwner returns (bool) { @@ -20,4 +27,8 @@ contract GuildBank is Ownable { emit Withdrawal(receiver, amount); return approvedToken.transfer(receiver, amount); } + + function initiateStream(address grantee, uint256 amount, uint256 duration) public { + sablier.createStream(grantee, amount, address(approvedToken), block.timestamp, block.timestamp + duration); + } } \ No newline at end of file diff --git a/contracts/Moloch.sol b/contracts/Moloch.sol index fca4b33..11103ee 100644 --- a/contracts/Moloch.sol +++ b/contracts/Moloch.sol @@ -407,6 +407,7 @@ contract Moloch { if (didPass && !proposal.aborted && hasFunds) { proposal.didPass = true; + guildBank.initiateStream(proposal.applicant, proposal.tokenTribute, proposal.grantDuration); // TODO: Call guildbank funciton to start the stream! diff --git a/test/endaoment-test.js b/test/endaoment-test.js index cd0a67b..a5f8615 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -84,7 +84,12 @@ describe('Moloch', () => { }); it('should allow a grant proposal from the guildbank', async () => { - await this.instance.submitGrantProposal(grantee1, toWeiDai(1000), GrantDuration, "grantee1", {from: member2}); + const desiredAmount = new web3.utils.BN(toWeiDai(1000)); + const duration = new web3.utils.BN(GrantDuration); + const remainder = desiredAmount.mod(duration); + const divisibleAmount = desiredAmount.sub(remainder); + + await this.instance.submitGrantProposal(grantee1, divisibleAmount, GrantDuration, "grantee1", {from: member2}); const proposal = await this.instance.proposalQueue(2); expect(proposal.details).to.equal('grantee1'); }); From cd79263195c981c6add56728d2f11213ec65471d Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Thu, 23 Apr 2020 07:46:06 -0400 Subject: [PATCH 06/13] Replace OpenZeppelin Ownable due to out of gas errors on initialize --- contracts/GuildBank.sol | 12 +++++++++--- test/endaoment-test.js | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/contracts/GuildBank.sol b/contracts/GuildBank.sol index 4b30176..e8e3306 100644 --- a/contracts/GuildBank.sol +++ b/contracts/GuildBank.sol @@ -1,6 +1,5 @@ pragma solidity ^0.5.0; -import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol"; import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; @@ -8,15 +7,17 @@ interface ISablier { function createStream(address recipient, uint256 deposit, address tokenAddress, uint256 startTime, uint256 stopTime) external returns (uint256); } -contract GuildBank is Ownable { +contract GuildBank { using SafeMath for uint256; + address public owner; IERC20 public approvedToken; // approved token contract reference ISablier public sablier; event Withdrawal(address indexed receiver, uint256 amount); constructor(address approvedTokenAddress) public { + owner = msg.sender; approvedToken = IERC20(approvedTokenAddress); sablier = ISablier(0xA4fc358455Febe425536fd1878bE67FfDBDEC59a); approvedToken.approve(address(sablier), uint256(-1)); @@ -28,7 +29,12 @@ contract GuildBank is Ownable { return approvedToken.transfer(receiver, amount); } - function initiateStream(address grantee, uint256 amount, uint256 duration) public { + function initiateStream(address grantee, uint256 amount, uint256 duration) public onlyOwner { sablier.createStream(grantee, amount, address(approvedToken), block.timestamp, block.timestamp + duration); } + + modifier onlyOwner() { + require(msg.sender == owner, "Endaoment::GuildBank - Not Owner"); + _; + } } \ No newline at end of file diff --git a/test/endaoment-test.js b/test/endaoment-test.js index a5f8615..27587df 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -3,6 +3,7 @@ const { accounts, contract, web3 } = require('@openzeppelin/test-environment'); const { time, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Moloch = contract.fromArtifact('Moloch'); +const GuildBank = contract.fromArtifact('GuildBank'); const { toWeiDai, stealDai, approveDai } = require('./helpers'); const PERIOD_DURATION = 17280; @@ -33,6 +34,9 @@ describe('Moloch', () => { {from: summoner} ); + const guildBankAddr = await this.instance.guildBank(); + this.guildBank = await GuildBank.at(guildBankAddr); + await stealDai(1000, summoner); await stealDai(20000, member1); await stealDai(20000, member2); From 54b124b1ed54a06b5640e42cae5e8a768254ccd4 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 24 Apr 2020 06:57:18 -0400 Subject: [PATCH 07/13] Store Sablier streamId for each grant created --- contracts/GuildBank.sol | 9 +++++++-- contracts/Moloch.sol | 26 +++++++++++++++++++------- test/endaoment-test.js | 3 +++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/contracts/GuildBank.sol b/contracts/GuildBank.sol index e8e3306..c2621f1 100644 --- a/contracts/GuildBank.sol +++ b/contracts/GuildBank.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; interface ISablier { function createStream(address recipient, uint256 deposit, address tokenAddress, uint256 startTime, uint256 stopTime) external returns (uint256); + function cancelStream(uint256 streamId) external returns (bool); } contract GuildBank { @@ -29,8 +30,12 @@ contract GuildBank { return approvedToken.transfer(receiver, amount); } - function initiateStream(address grantee, uint256 amount, uint256 duration) public onlyOwner { - sablier.createStream(grantee, amount, address(approvedToken), block.timestamp, block.timestamp + duration); + function initiateStream(address grantee, uint256 amount, uint256 startDate, uint256 endDate) public onlyOwner returns (uint256) { + return sablier.createStream(grantee, amount, address(approvedToken), startDate, endDate); + } + + function revokeStream(uint256 streamId) public onlyOwner { + sablier.cancelStream(streamId); } modifier onlyOwner() { diff --git a/contracts/Moloch.sol b/contracts/Moloch.sol index 11103ee..d493ea1 100644 --- a/contracts/Moloch.sol +++ b/contracts/Moloch.sol @@ -68,6 +68,13 @@ contract Moloch { uint256 highestIndexYesVote; // highest proposal index # on which the member voted YES } + struct Grant { + uint256 streamId; // Sablier Stream ID of the grant + uint256 proposalIndex; // Position of this Grant in the proposal queue + uint256 startDate; // When stream actually began + uint256 endDate; // When stream will/did end + } + struct Proposal { address proposer; // the member who submitted the proposal address applicant; // the applicant who wishes to become a member or receive a grant - this key will be used for withdrawals @@ -89,6 +96,7 @@ contract Moloch { mapping (address => Member) public members; mapping (address => address) public memberAddressByDelegateKey; Proposal[] public proposalQueue; + Grant[] public grants; /******** MODIFIERS @@ -407,15 +415,19 @@ contract Moloch { if (didPass && !proposal.aborted && hasFunds) { proposal.didPass = true; - guildBank.initiateStream(proposal.applicant, proposal.tokenTribute, proposal.grantDuration); - // TODO: Call guildbank funciton to start the stream! + uint256 start = block.timestamp; + uint256 end = block.timestamp + proposal.grantDuration; + uint256 streamId = guildBank.initiateStream(proposal.applicant, proposal.tokenTribute, start, end); - // transfer tokens to guild bank - // require( - // approvedToken.transfer(address(guildBank), proposal.tokenTribute), - // "Moloch::processProposal - token transfer to guild bank failed" - // ); + Grant memory grant = Grant({ + streamId: streamId, + proposalIndex: proposalIndex, + startDate: start, + endDate: end + }); + + grants.push(grant); // PROPOSAL FAILED OR ABORTED } else { diff --git a/test/endaoment-test.js b/test/endaoment-test.js index 27587df..9cb3c48 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -114,6 +114,9 @@ describe('Moloch', () => { await time.increase(VOTING_DURATION + GRACE_DURATION); await this.instance.processGrantProposal(2, {from: grantee1}); + const grant = await this.instance.grants(0); + + expect(grant.proposalIndex.toString()).to.equal('2'); }); // TODO: Test proposal fails if ragequitters deplete required funds From 99180b40b71966d3b5cde8c98ed3c85158e2b030 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 24 Apr 2020 08:03:30 -0400 Subject: [PATCH 08/13] Enable submission & processing of revocation proposals --- contracts/Moloch.sol | 122 ++++++++++++++++++++++++++++++++++--- contracts/Pool.sol | 2 +- test-environment.config.js | 4 ++ 3 files changed, 119 insertions(+), 9 deletions(-) diff --git a/contracts/Moloch.sol b/contracts/Moloch.sol index d493ea1..3722da6 100644 --- a/contracts/Moloch.sol +++ b/contracts/Moloch.sol @@ -10,7 +10,7 @@ contract Moloch { /*************** GLOBAL CONSTANTS ***************/ - uint256 public periodDuration; // default = 17280 = 4.8 hours in seconds (5 periods per day) + uint256 public periodDuration; // default = 17280 = 4.8 hours in seconds (5 periods per day) uint256 public votingPeriodLength; // default = 35 periods (7 days) uint256 public gracePeriodLength; // default = 35 periods (7 days) uint256 public abortWindow; // default = 5 periods (1 day) @@ -35,9 +35,11 @@ contract Moloch { ***************/ event SubmitProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, address indexed applicant, uint256 tokenTribute, uint256 sharesRequested); event SubmitGrantProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, address indexed applicant, uint256 tokenGrant, uint256 grantDuration); + event SubmitRevocationProposal(uint256 proposalIndex, address indexed delegateKey, address indexed memberAddress, uint256 grantIndex); event SubmitVote(uint256 indexed proposalIndex, address indexed delegateKey, address indexed memberAddress, uint8 uintVote); event ProcessProposal(uint256 indexed proposalIndex, address indexed applicant, address indexed memberAddress, uint256 tokenTribute, uint256 sharesRequested, bool didPass); event ProcessGrantProposal(uint256 indexed proposalIndex, address indexed applicant, address indexed memberAddress, uint256 tokenGrant, uint256 grantDuration, bool didPass); + event ProcessRevocationProposal(uint256 indexed proposalIndex, address indexed memberAddress, uint256 grantIndex, bool didPass); event Ragequit(address indexed memberAddress, uint256 sharesToBurn); event Abort(uint256 indexed proposalIndex, address applicantAddress); event UpdateDelegateKey(address indexed memberAddress, address newDelegateKey); @@ -73,6 +75,7 @@ contract Moloch { uint256 proposalIndex; // Position of this Grant in the proposal queue uint256 startDate; // When stream actually began uint256 endDate; // When stream will/did end + bool wasRevoked; } struct Proposal { @@ -90,7 +93,7 @@ contract Moloch { uint256 maxTotalSharesAtYesVote; // the maximum # of total shares encountered at a yes vote on this proposal mapping (address => Vote) votesByMember; // the votes on this proposal by each member ProposalKind kind; // the kind of proposal - uint256 grantDuration; // how long the grant toknes will be streamed + uint256 grantMeta; // The grant duration for Grant proposals; the grant index for Revocation proposals } mapping (address => Member) public members; @@ -211,7 +214,7 @@ contract Moloch { details: details, maxTotalSharesAtYesVote: 0, kind: ProposalKind.Membership, - grantDuration: 0 + grantMeta: 0 }); // ... and append it to the queue @@ -265,7 +268,7 @@ contract Moloch { details: details, maxTotalSharesAtYesVote: 0, kind: ProposalKind.Grant, - grantDuration: grantDuration + grantMeta: grantDuration }); // ... and append it to the queue @@ -275,6 +278,60 @@ contract Moloch { emit SubmitGrantProposal(proposalIndex, msg.sender, memberAddress, applicant, tokenGrant, grantDuration); } + function submitRevocationProposal( + uint256 grantIndex, + string memory details + ) + public + onlyDelegate + { + Grant storage grant = grants[grantIndex]; + + uint256 proposalProcessDate = block.timestamp + .add(periodDuration) // until voting starts + .add(votingPeriodLength.mul(periodDuration)) // voting ends + .add(gracePeriodLength.mul(periodDuration)) // grace end + .add(periodDuration.mul(2)); // cushion + + // Ensure grant is not completed & exists at index (if index is invalid, endDate will be 0 & this fails) + require(grant.endDate > proposalProcessDate, "Endaoment::submitRevocationProposal - grant distribution too close to completion"); + + address memberAddress = memberAddressByDelegateKey[msg.sender]; + + // collect proposal deposit from proposer and store it in the Moloch until the proposal is processed + require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed"); + + // compute startingPeriod for proposal + uint256 startingPeriod = max( + getCurrentPeriod(), + proposalQueue.length == 0 ? 0 : proposalQueue[proposalQueue.length.sub(1)].startingPeriod + ).add(1); + + // create proposal ... + Proposal memory proposal = Proposal({ + proposer: memberAddress, + applicant: address(0), + sharesRequested: 0, + startingPeriod: startingPeriod, + yesVotes: 0, + noVotes: 0, + processed: false, + didPass: false, + aborted: false, + tokenTribute: 0, + details: details, + maxTotalSharesAtYesVote: 0, + kind: ProposalKind.Revocation, + grantMeta: grantIndex + }); + + // ... and append it to the queue + proposalQueue.push(proposal); + + uint256 proposalIndex = proposalQueue.length.sub(1); + emit SubmitRevocationProposal(proposalIndex, msg.sender, memberAddress, grantIndex); + } + function submitVote(uint256 proposalIndex, uint8 uintVote) public onlyDelegate { address memberAddress = memberAddressByDelegateKey[msg.sender]; Member storage member = members[memberAddress]; @@ -417,14 +474,15 @@ contract Moloch { proposal.didPass = true; uint256 start = block.timestamp; - uint256 end = block.timestamp + proposal.grantDuration; + uint256 end = block.timestamp.add(proposal.grantMeta); uint256 streamId = guildBank.initiateStream(proposal.applicant, proposal.tokenTribute, start, end); Grant memory grant = Grant({ streamId: streamId, proposalIndex: proposalIndex, startDate: start, - endDate: end + endDate: end, + wasRevoked: false }); grants.push(grant); @@ -446,12 +504,60 @@ contract Moloch { "Moloch::processProposal - failed to return proposal deposit to proposer" ); - emit ProcessProposal( + emit ProcessGrantProposal( proposalIndex, proposal.applicant, proposal.proposer, proposal.tokenTribute, - proposal.grantDuration, + proposal.grantMeta, + didPass + ); + } + + function processRevocationProposal(uint256 proposalIndex) public { + require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist"); + Proposal storage proposal = proposalQueue[proposalIndex]; + + require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed"); + require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed"); + require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed"); + require(proposal.kind == ProposalKind.Revocation, "Endaoment::processRevocationProposal - not a grant proposal"); + + proposal.processed = true; + + bool didPass = proposal.yesVotes > proposal.noVotes; + + // PROPOSAL PASSED + if (didPass && !proposal.aborted) { + + proposal.didPass = true; + + Grant storage grant = grants[proposal.grantMeta]; + grant.wasRevoked = true; + + guildBank.revokeStream(grant.streamId); + + // PROPOSAL FAILED OR ABORTED + } else { + // TODO: anything? + } + + // send msg.sender the processingReward + require( + approvedToken.transfer(msg.sender, processingReward), + "Moloch::processProposal - failed to send processing reward to msg.sender" + ); + + // return deposit to proposer (subtract processing reward) + require( + approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)), + "Moloch::processProposal - failed to return proposal deposit to proposer" + ); + + emit ProcessRevocationProposal( + proposalIndex, + proposal.proposer, + proposal.grantMeta, didPass ); } diff --git a/contracts/Pool.sol b/contracts/Pool.sol index cb53779..171c23e 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -127,7 +127,7 @@ contract MolochPool { while (i < toIndex) { - (, applicant, sharesRequested, , , , processed, didPass, aborted, tokenTribute, , maxTotalSharesAtYesVote, , ) = moloch.proposalQueue(i); + (, applicant, sharesRequested, , , , processed, didPass, aborted, tokenTribute, , maxTotalSharesAtYesVote, ,) = moloch.proposalQueue(i); if (!processed) { break; } diff --git a/test-environment.config.js b/test-environment.config.js index 51dd011..1d5d922 100644 --- a/test-environment.config.js +++ b/test-environment.config.js @@ -6,5 +6,9 @@ module.exports = { fork: `https://mainnet.infura.io/v3/${infura_key}`, unlocked_accounts: [process.env.DAI_FUNDER], allowUnlimitedContractSize: true, // TODO remove the need for this + gasLimit: 200e6, + }, + contracts: { + defaultGas: 200e6, }, }; From 5434e53c2b32b221e76bda6f37011772c1151a00 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 24 Apr 2020 17:41:58 -0400 Subject: [PATCH 09/13] Tests for basic revocation proposal mechanics --- test/endaoment-test.js | 36 ++++++++++++++++++++++++++++++++++-- test/helpers.js | 6 ++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/test/endaoment-test.js b/test/endaoment-test.js index 9cb3c48..7382117 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -4,7 +4,7 @@ const { time, expectEvent, expectRevert } = require('@openzeppelin/test-helpers' const { expect } = require('chai'); const Moloch = contract.fromArtifact('Moloch'); const GuildBank = contract.fromArtifact('GuildBank'); -const { toWeiDai, stealDai, approveDai } = require('./helpers'); +const { toWeiDai, stealDai, approveDai, daiBalance } = require('./helpers'); const PERIOD_DURATION = 17280; const VOTING_PERIODS = 35; @@ -119,5 +119,37 @@ describe('Moloch', () => { expect(grant.proposalIndex.toString()).to.equal('2'); }); - // TODO: Test proposal fails if ragequitters deplete required funds + it('should allow a revocation proposal', async () => { + await this.instance.submitRevocationProposal(0, "revoke grantee1", {from: summoner}); + const proposal = await this.instance.proposalQueue(3); + expect(proposal.details).to.equal("revoke grantee1"); + }); + + it('should allow members to vote on a revocation proposal', async () => { + await time.increase(PERIOD_DURATION); + + await this.instance.submitVote(3, 2, {from: member1}); + await this.instance.submitVote(3, 1, {from: member2}); + + const proposal = await this.instance.proposalQueue(3); + + expect(proposal.yesVotes.toString()).to.equal('3000'); + expect(proposal.noVotes.toString()).to.equal('2000'); + }); + + it('should process a successful revocation proposal', async () => { + await time.increase(VOTING_DURATION + GRACE_DURATION); + + const initialGuildBalance = await daiBalance(this.guildBank.address); + + await this.instance.processRevocationProposal(3, {from: summoner}); + + const grant = await this.instance.grants(0); + const postGuildBalance = await daiBalance(this.guildBank.address); + + expect(grant.wasRevoked).to.be.true; + expect(postGuildBalance.gt(initialGuildBalance)).to.be.true; + }); + + // TODO: Test grant proposal fails if ragequitters deplete required funds }); diff --git a/test/helpers.js b/test/helpers.js index 43403a3..aaf0023 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -17,3 +17,9 @@ exports.approveDai = async (holder, spender) => { const daiContract = new web3.eth.Contract(daiAbi, daiAddress); await daiContract.methods.approve(spender, this.toWeiDai(1000000000000)).send({from: holder}); } + +exports.daiBalance = async(holder) => { + const daiContract = new web3.eth.Contract(daiAbi, daiAddress); + const stringBalance = await daiContract.methods.balanceOf(holder).call(); + return new web3.utils.BN(stringBalance); +} From 63e8ad77539d379f5fab6881e43f7daf27776882 Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 24 Apr 2020 21:47:36 -0400 Subject: [PATCH 10/13] Remove the unneeded Pool contract --- contracts/Pool.sol | 258 --------------------------------------------- 1 file changed, 258 deletions(-) delete mode 100644 contracts/Pool.sol diff --git a/contracts/Pool.sol b/contracts/Pool.sol deleted file mode 100644 index 171c23e..0000000 --- a/contracts/Pool.sol +++ /dev/null @@ -1,258 +0,0 @@ -// Pool.sol -// - mints a pool share when someone donates tokens -// - syncs with Moloch proposal queue to mint shares for grantees -// - allows donors to withdraw tokens at any time - -pragma solidity ^0.5.0; - -import "./Moloch.sol"; -import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol"; - -contract MolochPool { - using SafeMath for uint256; - - event Sync ( - uint256 currentProposalIndex - ); - - event Deposit ( - address donor, - uint256 sharesMinted, - uint256 tokensDeposited - ); - - event Withdraw ( - address donor, - uint256 sharesBurned - ); - - event KeeperWithdraw ( - address donor, - uint256 sharesBurned, - address keeper - ); - - event AddKeepers ( - address donor, - address[] addedKeepers - ); - - event RemoveKeepers ( - address donor, - address[] removedKeepers - ); - - event SharesMinted ( - uint256 sharesToMint, - address recipient, - uint256 totalPoolShares - ); - - event SharesBurned ( - uint256 sharesToBurn, - address recipient, - uint256 totalPoolShares - ); - - uint256 public totalPoolShares = 0; // the total shares outstanding of the pool - uint256 public currentProposalIndex = 0; // the moloch proposal index that this pool has been synced to - - Moloch public moloch; // moloch contract reference - IERC20 public approvedToken; // approved token contract reference (copied from moloch contract) - - bool locked; // prevent re-entrancy - - uint256 constant MAX_NUMBER_OF_SHARES = 10**30; // maximum number of shares that can be minted - - struct Donor { - uint256 shares; - mapping (address => bool) keepers; - } - - // the amount of shares each pool shareholder has - mapping (address => Donor) public donors; - - modifier active { - require(totalPoolShares > 0, "MolochPool: Not active"); - _; - } - - modifier noReentrancy() { - require(!locked, "MolochPool: Reentrant call"); - locked = true; - _; - locked = false; - } - - constructor(address _moloch) public { - moloch = Moloch(_moloch); - approvedToken = IERC20(moloch.approvedToken()); - } - - function activate(uint256 initialTokens, uint256 initialPoolShares) public noReentrancy { - require(totalPoolShares == 0, "MolochPool: Already active"); - - require( - approvedToken.transferFrom(msg.sender, address(this), initialTokens), - "MolochPool: Initial tokens transfer failed" - ); - _mintSharesForAddress(initialPoolShares, msg.sender); - } - - // updates Pool state based on Moloch proposal queue - // - we only want to mint shares for grants, which are 0 tribute - // - mints pool shares to applicants based on sharesRequested / maxTotalSharesAtYesVote - // - use maxTotalSharesAtYesVote because: - // - cant read shares at the time of proposal processing (womp womp) - // - should be close enough if grant shares are small relative to total shares, which they should be - // - protects pool contributors if many Moloch members ragequit before the proposal is processed by reducing follow on funding - // - e.g. if 50% of Moloch shares ragequit after someone voted yes, the grant proposal would get 50% less follow-on from the pool - function sync(uint256 toIndex) public active noReentrancy { - require( - toIndex <= moloch.getProposalQueueLength(), - "MolochPool: Proposal index too high" - ); - - // declare proposal params - address applicant; - uint256 sharesRequested; - bool processed; - bool didPass; - bool aborted; - uint256 tokenTribute; - uint256 maxTotalSharesAtYesVote; - - uint256 i = currentProposalIndex; - - while (i < toIndex) { - - (, applicant, sharesRequested, , , , processed, didPass, aborted, tokenTribute, , maxTotalSharesAtYesVote, ,) = moloch.proposalQueue(i); - - if (!processed) { break; } - - // passing grant proposal, mint pool shares proportionally on behalf of the applicant - if (!aborted && didPass && tokenTribute == 0 && sharesRequested > 0) { - // This can't revert: - // 1. maxTotalSharesAtYesVote > 0, otherwise nobody could have voted. - // 2. sharesRequested is <= 10**18 (see Moloch.sol:172), and - // totalPoolShares <= 10**30, so multiplying them is <= 10**48 and < 2**160 - uint256 sharesToMint = totalPoolShares.mul(sharesRequested).div(maxTotalSharesAtYesVote); // for a passing proposal, maxTotalSharesAtYesVote is > 0 - _mintSharesForAddress(sharesToMint, applicant); - } - - i++; - } - - currentProposalIndex = i; - - emit Sync(currentProposalIndex); - } - - // add tokens to the pool, mint new shares proportionally - function deposit(uint256 tokenAmount) public active noReentrancy { - - uint256 sharesToMint = totalPoolShares.mul(tokenAmount).div(approvedToken.balanceOf(address(this))); - - require( - approvedToken.transferFrom(msg.sender, address(this), tokenAmount), - "MolochPool: Deposit transfer failed" - ); - - _mintSharesForAddress(sharesToMint, msg.sender); - - emit Deposit( - msg.sender, - sharesToMint, - tokenAmount - ); - } - - // burn shares to proportionally withdraw tokens in pool - function withdraw(uint256 sharesToBurn) public active noReentrancy { - _withdraw(msg.sender, sharesToBurn); - - emit Withdraw( - msg.sender, - sharesToBurn - ); - } - - // keeper burns shares to withdraw on behalf of the donor - function keeperWithdraw(uint256 sharesToBurn, address recipient) public active noReentrancy { - require( - donors[recipient].keepers[msg.sender], - "MolochPool: Sender is not a keeper" - ); - - _withdraw(recipient, sharesToBurn); - - emit KeeperWithdraw( - recipient, - sharesToBurn, - msg.sender - ); - } - - function addKeepers(address[] calldata newKeepers) external active noReentrancy { - Donor storage donor = donors[msg.sender]; - - for (uint256 i = 0; i < newKeepers.length; i++) { - donor.keepers[newKeepers[i]] = true; - } - - emit AddKeepers(msg.sender, newKeepers); - } - - function removeKeepers(address[] calldata keepersToRemove) external active noReentrancy { - Donor storage donor = donors[msg.sender]; - - for (uint256 i = 0; i < keepersToRemove.length; i++) { - donor.keepers[keepersToRemove[i]] = false; - } - - emit RemoveKeepers(msg.sender, keepersToRemove); - } - - function _mintSharesForAddress(uint256 sharesToMint, address recipient) internal { - totalPoolShares = totalPoolShares.add(sharesToMint); - donors[recipient].shares = donors[recipient].shares.add(sharesToMint); - - require( - totalPoolShares <= MAX_NUMBER_OF_SHARES, - "MolochPool: Max number of shares exceeded" - ); - - emit SharesMinted( - sharesToMint, - recipient, - totalPoolShares - ); - } - - function _withdraw(address recipient, uint256 sharesToBurn) internal { - Donor storage donor = donors[recipient]; - - require( - donor.shares >= sharesToBurn, - "MolochPool: Not enough shares to burn" - ); - - uint256 tokensToWithdraw = approvedToken.balanceOf(address(this)).mul(sharesToBurn).div(totalPoolShares); - - totalPoolShares = totalPoolShares.sub(sharesToBurn); - donor.shares = donor.shares.sub(sharesToBurn); - - require( - approvedToken.transfer(recipient, tokensToWithdraw), - "MolochPool: Withdrawal transfer failed" - ); - - emit SharesBurned( - sharesToBurn, - recipient, - totalPoolShares - ); - } - -} \ No newline at end of file From 019217f5c5851d9e1378736d50ad499e5a66e0cd Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 24 Apr 2020 21:49:17 -0400 Subject: [PATCH 11/13] Remove empty code paths from proposal processing methods --- contracts/Moloch.sol | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/contracts/Moloch.sol b/contracts/Moloch.sol index 3722da6..5844da6 100644 --- a/contracts/Moloch.sol +++ b/contracts/Moloch.sol @@ -470,7 +470,6 @@ contract Moloch { // PROPOSAL PASSED if (didPass && !proposal.aborted && hasFunds) { - proposal.didPass = true; uint256 start = block.timestamp; @@ -486,10 +485,6 @@ contract Moloch { }); grants.push(grant); - - // PROPOSAL FAILED OR ABORTED - } else { - // TODO: anything? } // send msg.sender the processingReward @@ -529,17 +524,12 @@ contract Moloch { // PROPOSAL PASSED if (didPass && !proposal.aborted) { - proposal.didPass = true; Grant storage grant = grants[proposal.grantMeta]; grant.wasRevoked = true; guildBank.revokeStream(grant.streamId); - - // PROPOSAL FAILED OR ABORTED - } else { - // TODO: anything? } // send msg.sender the processingReward From 54e645f5c07eca02cdaf673c9c751dbba3ada0fc Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 24 Apr 2020 21:52:04 -0400 Subject: [PATCH 12/13] Rename Moloch to Endaoment * Rename contract & contract file * Replace all error messages and comments throughout the contract --- contracts/{Moloch.sol => Endaoment.sol} | 132 ++++++++++++------------ contracts/EndaomentFactory.sol | 4 +- test/endaoment-test.js | 8 +- 3 files changed, 72 insertions(+), 72 deletions(-) rename contracts/{Moloch.sol => Endaoment.sol} (78%) diff --git a/contracts/Moloch.sol b/contracts/Endaoment.sol similarity index 78% rename from contracts/Moloch.sol rename to contracts/Endaoment.sol index 5844da6..bff0433 100644 --- a/contracts/Moloch.sol +++ b/contracts/Endaoment.sol @@ -4,7 +4,7 @@ import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol"; import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol"; import "./GuildBank.sol"; -contract Moloch { +contract Endaoment { using SafeMath for uint256; /*************** @@ -105,12 +105,12 @@ contract Moloch { MODIFIERS ********/ modifier onlyMember { - require(members[msg.sender].shares > 0, "Moloch::onlyMember - not a member"); + require(members[msg.sender].shares > 0, "Endaoment::onlyMember - not a member"); _; } modifier onlyDelegate { - require(members[memberAddressByDelegateKey[msg.sender]].shares > 0, "Moloch::onlyDelegate - not a delegate"); + require(members[memberAddressByDelegateKey[msg.sender]].shares > 0, "Endaoment::onlyDelegate - not a delegate"); _; } @@ -130,17 +130,17 @@ contract Moloch { string memory _name, string memory _description ) public { - require(summoner != address(0), "Moloch::constructor - summoner cannot be 0"); - require(_approvedToken != address(0), "Moloch::constructor - _approvedToken cannot be 0"); - require(_periodDuration > 0, "Moloch::constructor - _periodDuration cannot be 0"); - require(_votingPeriodLength > 0, "Moloch::constructor - _votingPeriodLength cannot be 0"); - require(_votingPeriodLength <= MAX_VOTING_PERIOD_LENGTH, "Moloch::constructor - _votingPeriodLength exceeds limit"); - require(_gracePeriodLength <= MAX_GRACE_PERIOD_LENGTH, "Moloch::constructor - _gracePeriodLength exceeds limit"); - require(_abortWindow > 0, "Moloch::constructor - _abortWindow cannot be 0"); - require(_abortWindow <= _votingPeriodLength, "Moloch::constructor - _abortWindow must be smaller than or equal to _votingPeriodLength"); - require(_dilutionBound > 0, "Moloch::constructor - _dilutionBound cannot be 0"); - require(_dilutionBound <= MAX_DILUTION_BOUND, "Moloch::constructor - _dilutionBound exceeds limit"); - require(_proposalDeposit >= _processingReward, "Moloch::constructor - _proposalDeposit cannot be smaller than _processingReward"); + require(summoner != address(0), "Endaoment::constructor - summoner cannot be 0"); + require(_approvedToken != address(0), "Endaoment::constructor - _approvedToken cannot be 0"); + require(_periodDuration > 0, "Endaoment::constructor - _periodDuration cannot be 0"); + require(_votingPeriodLength > 0, "Endaoment::constructor - _votingPeriodLength cannot be 0"); + require(_votingPeriodLength <= MAX_VOTING_PERIOD_LENGTH, "Endaoment::constructor - _votingPeriodLength exceeds limit"); + require(_gracePeriodLength <= MAX_GRACE_PERIOD_LENGTH, "Endaoment::constructor - _gracePeriodLength exceeds limit"); + require(_abortWindow > 0, "Endaoment::constructor - _abortWindow cannot be 0"); + require(_abortWindow <= _votingPeriodLength, "Endaoment::constructor - _abortWindow must be smaller than or equal to _votingPeriodLength"); + require(_dilutionBound > 0, "Endaoment::constructor - _dilutionBound cannot be 0"); + require(_dilutionBound <= MAX_DILUTION_BOUND, "Endaoment::constructor - _dilutionBound exceeds limit"); + require(_proposalDeposit >= _processingReward, "Endaoment::constructor - _proposalDeposit cannot be smaller than _processingReward"); approvedToken = IERC20(_approvedToken); @@ -176,22 +176,22 @@ contract Moloch { public onlyDelegate { - require(applicant != address(0), "Moloch::submitProposal - applicant cannot be 0"); + require(applicant != address(0), "Endaoment::submitProposal - applicant cannot be 0"); // Make sure we won't run into overflows when doing calculations with shares. // Note that totalShares + totalSharesRequested + sharesRequested is an upper bound // on the number of shares that can exist until this proposal has been processed. - require(totalShares.add(totalSharesRequested).add(sharesRequested) <= MAX_NUMBER_OF_SHARES, "Moloch::submitProposal - too many shares requested"); + require(totalShares.add(totalSharesRequested).add(sharesRequested) <= MAX_NUMBER_OF_SHARES, "Endaoment::submitProposal - too many shares requested"); totalSharesRequested = totalSharesRequested.add(sharesRequested); address memberAddress = memberAddressByDelegateKey[msg.sender]; - // collect proposal deposit from proposer and store it in the Moloch until the proposal is processed - require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed"); + // collect proposal deposit from proposer and store it in the Endaoment until the proposal is processed + require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Endaoment::submitProposal - proposal deposit token transfer failed"); - // collect tribute from applicant and store it in the Moloch until the proposal is processed - require(approvedToken.transferFrom(applicant, address(this), tokenTribute), "Moloch::submitProposal - tribute token transfer failed"); + // collect tribute from applicant and store it in the Endaoment until the proposal is processed + require(approvedToken.transferFrom(applicant, address(this), tokenTribute), "Endaoment::submitProposal - tribute token transfer failed"); // compute startingPeriod for proposal uint256 startingPeriod = max( @@ -239,13 +239,13 @@ contract Moloch { public onlyDelegate { - require(applicant != address(0), "Moloch::submitProposal - applicant cannot be 0"); + require(applicant != address(0), "Endaoment::submitProposal - applicant cannot be 0"); require(tokenGrant <= approvedToken.balanceOf(address(guildBank)), "Endaoment::submitGrantProposal - grant is greater than treasury"); address memberAddress = memberAddressByDelegateKey[msg.sender]; - // collect proposal deposit from proposer and store it in the Moloch until the proposal is processed - require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed"); + // collect proposal deposit from proposer and store it in the Endaoment until the proposal is processed + require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Endaoment::submitProposal - proposal deposit token transfer failed"); // compute startingPeriod for proposal uint256 startingPeriod = max( @@ -298,8 +298,8 @@ contract Moloch { address memberAddress = memberAddressByDelegateKey[msg.sender]; - // collect proposal deposit from proposer and store it in the Moloch until the proposal is processed - require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Moloch::submitProposal - proposal deposit token transfer failed"); + // collect proposal deposit from proposer and store it in the Endaoment until the proposal is processed + require(approvedToken.transferFrom(msg.sender, address(this), proposalDeposit), "Endaoment::submitProposal - proposal deposit token transfer failed"); // compute startingPeriod for proposal uint256 startingPeriod = max( @@ -336,17 +336,17 @@ contract Moloch { address memberAddress = memberAddressByDelegateKey[msg.sender]; Member storage member = members[memberAddress]; - require(proposalIndex < proposalQueue.length, "Moloch::submitVote - proposal does not exist"); + require(proposalIndex < proposalQueue.length, "Endaoment::submitVote - proposal does not exist"); Proposal storage proposal = proposalQueue[proposalIndex]; - require(uintVote < 3, "Moloch::submitVote - uintVote must be less than 3"); + require(uintVote < 3, "Endaoment::submitVote - uintVote must be less than 3"); Vote vote = Vote(uintVote); - require(getCurrentPeriod() >= proposal.startingPeriod, "Moloch::submitVote - voting period has not started"); - require(!hasVotingPeriodExpired(proposal.startingPeriod), "Moloch::submitVote - proposal voting period has expired"); - require(proposal.votesByMember[memberAddress] == Vote.Null, "Moloch::submitVote - member has already voted on this proposal"); - require(vote == Vote.Yes || vote == Vote.No, "Moloch::submitVote - vote must be either Yes or No"); - require(!proposal.aborted, "Moloch::submitVote - proposal has been aborted"); + require(getCurrentPeriod() >= proposal.startingPeriod, "Endaoment::submitVote - voting period has not started"); + require(!hasVotingPeriodExpired(proposal.startingPeriod), "Endaoment::submitVote - proposal voting period has expired"); + require(proposal.votesByMember[memberAddress] == Vote.Null, "Endaoment::submitVote - member has already voted on this proposal"); + require(vote == Vote.Yes || vote == Vote.No, "Endaoment::submitVote - vote must be either Yes or No"); + require(!proposal.aborted, "Endaoment::submitVote - proposal has been aborted"); // store vote proposal.votesByMember[memberAddress] = vote; @@ -373,12 +373,12 @@ contract Moloch { } function processProposal(uint256 proposalIndex) public { - require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist"); + require(proposalIndex < proposalQueue.length, "Endaoment::processProposal - proposal does not exist"); Proposal storage proposal = proposalQueue[proposalIndex]; - require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed"); - require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed"); - require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed"); + require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Endaoment::processProposal - proposal is not ready to be processed"); + require(proposal.processed == false, "Endaoment::processProposal - proposal has already been processed"); + require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Endaoment::processProposal - previous proposal must be processed"); require(proposal.kind == ProposalKind.Membership, "Endaoment::processProposal - not a membership proposal"); proposal.processed = true; @@ -420,7 +420,7 @@ contract Moloch { // transfer tokens to guild bank require( approvedToken.transfer(address(guildBank), proposal.tokenTribute), - "Moloch::processProposal - token transfer to guild bank failed" + "Endaoment::processProposal - token transfer to guild bank failed" ); // PROPOSAL FAILED OR ABORTED @@ -428,20 +428,20 @@ contract Moloch { // return all tokens to the applicant require( approvedToken.transfer(proposal.applicant, proposal.tokenTribute), - "Moloch::processProposal - failing vote token transfer failed" + "Endaoment::processProposal - failing vote token transfer failed" ); } // send msg.sender the processingReward require( approvedToken.transfer(msg.sender, processingReward), - "Moloch::processProposal - failed to send processing reward to msg.sender" + "Endaoment::processProposal - failed to send processing reward to msg.sender" ); // return deposit to proposer (subtract processing reward) require( approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)), - "Moloch::processProposal - failed to return proposal deposit to proposer" + "Endaoment::processProposal - failed to return proposal deposit to proposer" ); emit ProcessProposal( @@ -455,12 +455,12 @@ contract Moloch { } function processGrantProposal(uint256 proposalIndex) public { - require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist"); + require(proposalIndex < proposalQueue.length, "Endaoment::processProposal - proposal does not exist"); Proposal storage proposal = proposalQueue[proposalIndex]; - require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed"); - require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed"); - require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed"); + require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Endaoment::processProposal - proposal is not ready to be processed"); + require(proposal.processed == false, "Endaoment::processProposal - proposal has already been processed"); + require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Endaoment::processProposal - previous proposal must be processed"); require(proposal.kind == ProposalKind.Grant, "Endaoment::processGrantProposal - not a grant proposal"); proposal.processed = true; @@ -490,13 +490,13 @@ contract Moloch { // send msg.sender the processingReward require( approvedToken.transfer(msg.sender, processingReward), - "Moloch::processProposal - failed to send processing reward to msg.sender" + "Endaoment::processProposal - failed to send processing reward to msg.sender" ); // return deposit to proposer (subtract processing reward) require( approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)), - "Moloch::processProposal - failed to return proposal deposit to proposer" + "Endaoment::processProposal - failed to return proposal deposit to proposer" ); emit ProcessGrantProposal( @@ -510,12 +510,12 @@ contract Moloch { } function processRevocationProposal(uint256 proposalIndex) public { - require(proposalIndex < proposalQueue.length, "Moloch::processProposal - proposal does not exist"); + require(proposalIndex < proposalQueue.length, "Endaoment::processProposal - proposal does not exist"); Proposal storage proposal = proposalQueue[proposalIndex]; - require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Moloch::processProposal - proposal is not ready to be processed"); - require(proposal.processed == false, "Moloch::processProposal - proposal has already been processed"); - require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Moloch::processProposal - previous proposal must be processed"); + require(getCurrentPeriod() >= proposal.startingPeriod.add(votingPeriodLength).add(gracePeriodLength), "Endaoment::processProposal - proposal is not ready to be processed"); + require(proposal.processed == false, "Endaoment::processProposal - proposal has already been processed"); + require(proposalIndex == 0 || proposalQueue[proposalIndex.sub(1)].processed, "Endaoment::processProposal - previous proposal must be processed"); require(proposal.kind == ProposalKind.Revocation, "Endaoment::processRevocationProposal - not a grant proposal"); proposal.processed = true; @@ -535,13 +535,13 @@ contract Moloch { // send msg.sender the processingReward require( approvedToken.transfer(msg.sender, processingReward), - "Moloch::processProposal - failed to send processing reward to msg.sender" + "Endaoment::processProposal - failed to send processing reward to msg.sender" ); // return deposit to proposer (subtract processing reward) require( approvedToken.transfer(proposal.proposer, proposalDeposit.sub(processingReward)), - "Moloch::processProposal - failed to return proposal deposit to proposer" + "Endaoment::processProposal - failed to return proposal deposit to proposer" ); emit ProcessRevocationProposal( @@ -557,9 +557,9 @@ contract Moloch { Member storage member = members[msg.sender]; - require(member.shares >= sharesToBurn, "Moloch::ragequit - insufficient shares"); + require(member.shares >= sharesToBurn, "Endaoment::ragequit - insufficient shares"); - require(canRagequit(member.highestIndexYesVote), "Moloch::ragequit - cant ragequit until highest index proposal member voted YES on is processed"); + require(canRagequit(member.highestIndexYesVote), "Endaoment::ragequit - cant ragequit until highest index proposal member voted YES on is processed"); // burn shares member.shares = member.shares.sub(sharesToBurn); @@ -568,19 +568,19 @@ contract Moloch { // instruct guildBank to transfer fair share of tokens to the ragequitter require( guildBank.withdraw(msg.sender, sharesToBurn, initialTotalShares), - "Moloch::ragequit - withdrawal of tokens from guildBank failed" + "Endaoment::ragequit - withdrawal of tokens from guildBank failed" ); emit Ragequit(msg.sender, sharesToBurn); } function abort(uint256 proposalIndex) public { - require(proposalIndex < proposalQueue.length, "Moloch::abort - proposal does not exist"); + require(proposalIndex < proposalQueue.length, "Endaoment::abort - proposal does not exist"); Proposal storage proposal = proposalQueue[proposalIndex]; - require(msg.sender == proposal.applicant, "Moloch::abort - msg.sender must be applicant"); - require(getCurrentPeriod() < proposal.startingPeriod.add(abortWindow), "Moloch::abort - abort window must not have passed"); - require(!proposal.aborted, "Moloch::abort - proposal must not have already been aborted"); + require(msg.sender == proposal.applicant, "Endaoment::abort - msg.sender must be applicant"); + require(getCurrentPeriod() < proposal.startingPeriod.add(abortWindow), "Endaoment::abort - abort window must not have passed"); + require(!proposal.aborted, "Endaoment::abort - proposal must not have already been aborted"); uint256 tokensToAbort = proposal.tokenTribute; proposal.tokenTribute = 0; @@ -589,19 +589,19 @@ contract Moloch { // return all tokens to the applicant require( approvedToken.transfer(proposal.applicant, tokensToAbort), - "Moloch::processProposal - failed to return tribute to applicant" + "Endaoment::processProposal - failed to return tribute to applicant" ); emit Abort(proposalIndex, msg.sender); } function updateDelegateKey(address newDelegateKey) public onlyMember { - require(newDelegateKey != address(0), "Moloch::updateDelegateKey - newDelegateKey cannot be 0"); + require(newDelegateKey != address(0), "Endaoment::updateDelegateKey - newDelegateKey cannot be 0"); // skip checks if member is setting the delegate key to their member address if (newDelegateKey != msg.sender) { - require(!members[newDelegateKey].exists, "Moloch::updateDelegateKey - cant overwrite existing members"); - require(!members[memberAddressByDelegateKey[newDelegateKey]].exists, "Moloch::updateDelegateKey - cant overwrite existing delegate keys"); + require(!members[newDelegateKey].exists, "Endaoment::updateDelegateKey - cant overwrite existing members"); + require(!members[memberAddressByDelegateKey[newDelegateKey]].exists, "Endaoment::updateDelegateKey - cant overwrite existing delegate keys"); } Member storage member = members[msg.sender]; @@ -630,7 +630,7 @@ contract Moloch { // can only ragequit if the latest proposal you voted YES on has been processed function canRagequit(uint256 highestIndexYesVote) public view returns (bool) { - require(highestIndexYesVote < proposalQueue.length, "Moloch::canRagequit - proposal does not exist"); + require(highestIndexYesVote < proposalQueue.length, "Endaoment::canRagequit - proposal does not exist"); return proposalQueue[highestIndexYesVote].processed; } @@ -639,8 +639,8 @@ contract Moloch { } function getMemberProposalVote(address memberAddress, uint256 proposalIndex) public view returns (Vote) { - require(members[memberAddress].exists, "Moloch::getMemberProposalVote - member doesn't exist"); - require(proposalIndex < proposalQueue.length, "Moloch::getMemberProposalVote - proposal doesn't exist"); + require(members[memberAddress].exists, "Endaoment::getMemberProposalVote - member doesn't exist"); + require(proposalIndex < proposalQueue.length, "Endaoment::getMemberProposalVote - proposal doesn't exist"); return proposalQueue[proposalIndex].votesByMember[memberAddress]; } } \ No newline at end of file diff --git a/contracts/EndaomentFactory.sol b/contracts/EndaomentFactory.sol index 8b44939..d32969b 100644 --- a/contracts/EndaomentFactory.sol +++ b/contracts/EndaomentFactory.sol @@ -1,6 +1,6 @@ pragma solidity ^0.5.0; -import "./Moloch.sol"; +import "./Endaoment.sol"; contract EndaomentFactory { @@ -28,7 +28,7 @@ contract EndaomentFactory { string calldata _description ) external { - Moloch endaoment = new Moloch( + Endaoment endaoment = new Endaoment( summoner, _approvedToken, _periodDuration, diff --git a/test/endaoment-test.js b/test/endaoment-test.js index 7382117..3080322 100644 --- a/test/endaoment-test.js +++ b/test/endaoment-test.js @@ -2,7 +2,7 @@ require('dotenv').config(); const { accounts, contract, web3 } = require('@openzeppelin/test-environment'); const { time, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const Moloch = contract.fromArtifact('Moloch'); +const Endaoment = contract.fromArtifact('Endaoment'); const GuildBank = contract.fromArtifact('GuildBank'); const { toWeiDai, stealDai, approveDai, daiBalance } = require('./helpers'); @@ -15,11 +15,11 @@ const GRACE_DURATION = GRACE_PERIODS * PERIOD_DURATION; const GrantDuration = 30*24*60*60; -describe('Moloch', () => { +describe('Endaoment', () => { const [ summoner, member1, member2, grantee1 ] = accounts; before(async () => { - this.instance = await Moloch.new( + this.instance = await Endaoment.new( summoner, process.env.DAI_ADDR, // address _approvedToken (DAI address) PERIOD_DURATION, // uint256 _periodDuration @@ -45,7 +45,7 @@ describe('Moloch', () => { await approveDai(member2, this.instance.address); }); - it('should see the deployed Moloch contract', async () => { + it('should see the deployed Endaoment contract', async () => { expect(this.instance.address.startsWith('0x')).to.be.true; expect(this.instance.address.length).to.equal(42); }); From 4710adfc515933ebfa3ffb8cad860523bb421efa Mon Sep 17 00:00:00 2001 From: Ben DiFrancesco Date: Fri, 24 Apr 2020 22:04:59 -0400 Subject: [PATCH 13/13] Move Dai contract ABI to a folder in the project root --- {test => abi}/dai.json | 0 test/helpers.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {test => abi}/dai.json (100%) diff --git a/test/dai.json b/abi/dai.json similarity index 100% rename from test/dai.json rename to abi/dai.json diff --git a/test/helpers.js b/test/helpers.js index aaf0023..66c8a2f 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -1,5 +1,5 @@ const { web3 } = require('@openzeppelin/test-environment'); -const daiAbi = require('./dai.json').abi; +const daiAbi = require('../abi/dai.json').abi; const daiFunder = process.env.DAI_FUNDER; const daiAddress = process.env.DAI_ADDR;