diff --git a/contracts/Daico/Daico.sol b/contracts/Daico/Daico.sol index 378a107..352a832 100644 --- a/contracts/Daico/Daico.sol +++ b/contracts/Daico/Daico.sol @@ -23,7 +23,7 @@ contract Daico is Ownable { uint[] public tapAmounts; uint[] public tapTimestampsFinishAt; - enum VotingType { ReleaseTap, ReleaseTapDecreasedQuorum, ChangeRoadmap, TerminateProject } + enum VotingType { ReleaseTap, ReleaseTapDecreasedQuorum, ChangeRoadmap, ChangeRoadmapDecreasedQuorum, TerminateProject, TerminateProjectDecreasedQuorum } enum VotingResult { Accept, Decline, QuorumNotReached, NoDecision } mapping(uint => mapping(uint => uint)) public tapVotings; @@ -157,6 +157,17 @@ contract Daico is Ownable { return VotingResult.NoDecision; } + /** + * @dev Checks whether investor already voted in particular voting + * @param _votingIndex voting index + * @param _investorAddress investor address + * @return whether investor has already voted in particular voting + */ + function isInvestorVoted(uint _votingIndex, address _investorAddress) external view validVotingIndex(_votingIndex) returns(bool) { + require(_investorAddress != address(0)); + return votings[_votingIndex].voted[_investorAddress]; + } + /** * @dev Checks whether project is terminated * @return is project terminated @@ -164,8 +175,8 @@ contract Daico is Ownable { function isProjectTerminated() public view returns(bool) { bool isTerminated = false; Voting memory latestVoting = votings[votingsCount.sub(1)]; - // if latest voting is of type TerminateProject and result Accept then set isTerminated to true - if((latestVoting.votingType == VotingType.TerminateProject) && (getVotingResult(votingsCount.sub(1)) == VotingResult.Accept)) { + // if latest voting is of type TerminateProject or TerminateProjectDecreasedQuorum and result Accept then set isTerminated to true + if(((latestVoting.votingType == VotingType.TerminateProject) || (latestVoting.votingType == VotingType.TerminateProjectDecreasedQuorum)) && (getVotingResult(votingsCount.sub(1)) == VotingResult.Accept)) { isTerminated = true; } return isTerminated; @@ -182,16 +193,8 @@ contract Daico is Ownable { uint latestVotingIndex = tapVotings[_tapIndex][tapVotingsCount[_tapIndex].sub(1)]; Voting memory voting = votings[latestVotingIndex]; bool isVotingAccepted = getVotingResult(latestVotingIndex) == VotingResult.Accept; - // if voting of type ReleaseTap and result is Accept then set isWithdrawAccepted to true - if((voting.votingType == VotingType.ReleaseTap) && isVotingAccepted) { - isWithdrawAccepted = true; - } - // if voting of type ReleaseTapDecreasedQuorum and result is Accept then set isWithdrawAccepted to true - if((voting.votingType == VotingType.ReleaseTapDecreasedQuorum) && isVotingAccepted) { - isWithdrawAccepted = true; - } - // if voting of type ChangeRoadmap and result is Accept then set isWithdrawAccepted to true - if((voting.votingType == VotingType.ChangeRoadmap) && isVotingAccepted) { + // if voting is of types: ReleaseTap, ReleaseTapDecreasedQuorum, ChangeRoadmap or ChangeRoadmapDecreasedQuorum then set isWithdrawAccepted to true + if(((voting.votingType != VotingType.TerminateProject) && (voting.votingType != VotingType.TerminateProjectDecreasedQuorum)) && isVotingAccepted) { isWithdrawAccepted = true; } return isWithdrawAccepted; @@ -202,34 +205,66 @@ contract Daico is Ownable { */ /** - * @dev Creates a new voting by investor. Investors can create votings of 2 types: ChangeRoadmap and TerminateProject. + * @dev Creates a new voting by investor. Investors can create votings of 4 types: ChangeRoadmap, ChangeRoadmapDecreasedQuorum, TerminateProject, TerminateProjectDecreasedQuorum. * @param _tapIndex tap index * @param _votingType voting type */ function createVotingByInvestor(uint _tapIndex, VotingType _votingType) external onlyInvestor validTapIndex(_tapIndex) { // common validation - require(_votingType == VotingType.ChangeRoadmap || _votingType == VotingType.TerminateProject); + require(_votingType == VotingType.ChangeRoadmap || _votingType == VotingType.ChangeRoadmapDecreasedQuorum || _votingType == VotingType.TerminateProject || _votingType == VotingType.TerminateProjectDecreasedQuorum); uint latestVotingIndex = tapVotings[_tapIndex][tapVotingsCount[_tapIndex].sub(1)]; Voting memory latestVoting = votings[latestVotingIndex]; + VotingResult votingResult = getVotingResult(latestVotingIndex); + // check that last voting is finished require(now >= latestVoting.finishAt); // if investor wants to create voting of type ChangeRoadmap if(_votingType == VotingType.ChangeRoadmap) { - // check that latest voting is of type ReleaseTap or ReleaseTapDecreasedQuorum - require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum); - // check that latest voting result is no decision - require(getVotingResult(latestVotingIndex) == VotingResult.NoDecision); + // check that latest voting is of types ReleaseTap, ReleaseTapDecreasedQuorum, TerminateProject, TerminateProjectDecreasedQuorum + require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum || latestVoting.votingType == VotingType.TerminateProject || latestVoting.votingType == VotingType.TerminateProjectDecreasedQuorum); + // if latest voting is ReleaseTap + if(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum) { + // check that latest voting result is no decision + require(votingResult == VotingResult.NoDecision); + } + // if latest voting is TerminateProject + if(latestVoting.votingType == VotingType.TerminateProject || latestVoting.votingType == VotingType.TerminateProjectDecreasedQuorum) { + // check that latest voting result is decline + require(votingResult == VotingResult.Decline); + } // create a new voting _createVoting(_tapIndex, minQuorumRate, now + 3 weeks, now + 4 weeks, VotingType.ChangeRoadmap); } + // if investor wants to create voting of type ChangeRoadmapDecreasedQuorum + if(_votingType == VotingType.ChangeRoadmapDecreasedQuorum) { + // check that latest voting is of type ChangeRoadmap or ChangeRoadmapDecreasedQuorum + require(latestVoting.votingType == VotingType.ChangeRoadmap || latestVoting.votingType == VotingType.ChangeRoadmapDecreasedQuorum); + // check that latest voting result has not reached quorum or has no decision + require((votingResult == VotingResult.QuorumNotReached) || (votingResult == VotingResult.NoDecision)); + // create a new voting + _createVoting(_tapIndex, 50, now + 3 weeks, now + 4 weeks, VotingType.ChangeRoadmapDecreasedQuorum); + } + // if investor wants to create voting of type TerminateProject if(_votingType == VotingType.TerminateProject) { + // check that latest voting is of types: ReleaseTap, ReleaseTapDecreasedQuorum, ChangeRoadmap, ChangeRoadmapDecreasedQuorum + require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum || latestVoting.votingType == VotingType.ChangeRoadmap || latestVoting.votingType == VotingType.ChangeRoadmapDecreasedQuorum); // check that latest voting result is decline - require(getVotingResult(latestVotingIndex) == VotingResult.Decline); + require(votingResult == VotingResult.Decline); // create a new voting _createVoting(_tapIndex, minQuorumRate, now, now + 2 weeks, VotingType.TerminateProject); } + + // if investor wants to create voting of type TerminateProjectDecreasedQuorum + if(_votingType == VotingType.TerminateProjectDecreasedQuorum) { + // check that latest voting is of type TerminateProject or TerminateProjectDecreasedQuorum + require(latestVoting.votingType == VotingType.TerminateProject || latestVoting.votingType == VotingType.TerminateProjectDecreasedQuorum); + // check that latest voting result has not reached quorum or has no decision + require((votingResult == VotingResult.QuorumNotReached) || (votingResult == VotingResult.NoDecision)); + // create a new voting + _createVoting(_tapIndex, 50, now, now + 2 weeks, VotingType.TerminateProjectDecreasedQuorum); + } } /** @@ -242,6 +277,7 @@ contract Daico is Ownable { require(now >= votings[_votingIndex].createdAt); require(now < votings[_votingIndex].finishAt); require(!votings[_votingIndex].voted[msg.sender]); + require(!isProjectTerminated()); // vote votings[_votingIndex].voted[msg.sender] = true; if(_isYes) { @@ -267,8 +303,8 @@ contract Daico is Ownable { Voting memory latestVoting = votings[latestVotingIndex]; // check that latest voting is finished require(now >= latestVoting.finishAt); - // check that latest voting is of type ReleaseTap or ReleaseTapDecreasedQuorum or TerminateProject - require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum || latestVoting.votingType == VotingType.TerminateProject); + // check that latest voting is of type ReleaseTap or ReleaseTapDecreasedQuorum + require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum); // check that latest voting result is quorum not reached require(getVotingResult(latestVotingIndex) == VotingResult.QuorumNotReached); // create a new voting diff --git a/contracts/Daico/DaicoTestable.sol b/contracts/Daico/DaicoTestable.sol new file mode 100644 index 0000000..8d092e7 --- /dev/null +++ b/contracts/Daico/DaicoTestable.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.4.24; + +import "zeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; + +import "./Daico.sol"; + +contract DaicoTestable is Daico { + + constructor( + address _daiTokenAddress, + address _projectTokenAddress, + address _projectOwnerAddress, + uint _tapsCount, + uint[] _tapAmounts, + uint[] _tapTimestampsFinishAt, + uint _minQuorumRate, + uint _minVoteRate, + uint _tokenHoldersCount + ) public Daico( + _daiTokenAddress, + _projectTokenAddress, + _projectOwnerAddress, + _tapsCount, + _tapAmounts, + _tapTimestampsFinishAt, + _minQuorumRate, + _minVoteRate, + _tokenHoldersCount + ) {} + + function createVoting(uint _tapIndex, uint _quorumRate, uint _createdAt, uint _finishAt, VotingType _votingType) external { + _createVoting(_tapIndex, _quorumRate, _createdAt, _finishAt, _votingType); + } + +} diff --git a/package-lock.json b/package-lock.json index bc48b89..a476247 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6507,6 +6507,11 @@ } } }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index 3487700..18a2748 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dotenv": "^6.0.0", "ganache-cli": "^6.1.4", "growl": "^1.10.0", + "moment": "^2.22.2", "solidity-coverage": "^0.5.5", "truffle": "^4.1.8", "truffle-hdwallet-provider": "0.0.5", diff --git a/test/Daico.functional.tests.js b/test/Daico.functional.tests.js deleted file mode 100644 index 8f324a8..0000000 --- a/test/Daico.functional.tests.js +++ /dev/null @@ -1,36 +0,0 @@ -const CheckExceptions = require("./utils/checkexceptions"); -const should = require("./utils/helpers"); - -const DaicoFactory = artifacts.require("DaicoFactory"); -const Daico = artifacts.require("Daico"); -const DaicoWithUnpackers = artifacts.require("DaicoWithUnpackers"); -const DaicoProject = artifacts.require("DaicoProject"); - - -contract("Daico", (accounts) => { - const creator = accounts[0]; - const investor1 = accounts[1]; - const investor2 = accounts[2]; - const investor3 = accounts[3]; - const projectOwner1 = accounts[4]; - const projectOwner2 = accounts[5]; - - let daicoFactory; - let daoBase; - let store; - let daicoAuto; - let daico; - - beforeEach(async () => { - daicoFactory = await DaicoFactory.new([investor1, investor2, investor3]); - // daoBase = DaoBaseWithUnpackers.at(await daicoFactory.daoBase()); - // store = DaoStorage.at(await daicoFactory.store()); - // daicoAuto = DaicoAuto.at(await daicoFactory.daicoAuto()); - // daico = Daico.at(await daicoFactory.daico()); - }); - - it("BoD member should be able to add new proposal", async () => { - // await daico.addNewProject(5, 1000, { from: projectOwner1 }).should.be.fulfilled; - - }); -}); \ No newline at end of file diff --git a/test/Daico.tests.js b/test/Daico.tests.js new file mode 100644 index 0000000..966e7a5 --- /dev/null +++ b/test/Daico.tests.js @@ -0,0 +1,287 @@ +const moment = require("moment"); +const { increaseTime } = require("./utils/helpers"); + +const Daico = artifacts.require("DaicoTestable"); +const MintableToken = artifacts.require("MintableToken"); + +contract("Daico", (accounts) => { + + const evercityMemberAddress = accounts[0]; + const projectOwnerAddress = accounts[1]; + const inverstorAddress = accounts[2]; + const inverstorAddress2 = accounts[3]; + const inverstorAddress3 = accounts[4]; + const inverstorAddress4 = accounts[5]; + + const VOTING_TYPE_RELEASE_TAP = 0; + const VOTING_TYPE_RELEASE_TAP_DECREASED_QUORUM = 1; + const VOTING_TYPE_CHANGE_ROADMAP = 2; + const VOTING_TYPE_CHANGE_ROADMAP_DECREASED_QUORUM = 3; + const VOTING_TYPE_TERMINATE_PROJECT = 4; + const VOTING_TYPE_TERMINATE_PROJECT_DECREASED_QUORUM = 5; + + const VOTING_RESULT_ACCEPT = 0; + const VOTING_RESULT_DECLINE = 1; + const VOTING_RESULT_QUORUM_NOT_REACHED = 2; + const VOTING_RESULT_NO_DECISION = 3; + + const timestampsFinishAt = [moment().add(1, 'week').unix(), moment().add(1, '5 weeks').unix()]; + const minQuorumRate = 70; + const minVoteRate = 70; + const tokenHoldersCount = 5; + + let daico; + let daiToken; + let projectToken; + + beforeEach(async() => { + daiToken = await MintableToken.new(); + projectToken = await MintableToken.new(); + await projectToken.mint(inverstorAddress, 1); + await projectToken.mint(inverstorAddress2, 1); + await projectToken.mint(inverstorAddress3, 1); + await projectToken.mint(inverstorAddress4, 1); + daico = await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [1, 2], timestampsFinishAt, minVoteRate, minQuorumRate, tokenHoldersCount); + }); + + describe("constructor()", () => { + it("should revert if DAI token address is 0x00", async() => { + await Daico.new(0x00, projectToken.address, projectOwnerAddress, 2, [1, 2], timestampsFinishAt, minVoteRate, minQuorumRate, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if project token address is 0x00", async() => { + await Daico.new(daiToken.address, 0x00, projectOwnerAddress, 2, [1, 2], timestampsFinishAt, minVoteRate, minQuorumRate, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if project owner address is 0x00", async() => { + await Daico.new(daiToken.address, projectToken.address, 0x00, 2, [1, 2], timestampsFinishAt, minVoteRate, minQuorumRate, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if taps count is 0", async() => { + await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 0, [1, 2], timestampsFinishAt, minVoteRate, minQuorumRate, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if tap amounts array length not equal to taps count", async() => { + await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [], timestampsFinishAt, minVoteRate, minQuorumRate, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if tap timestamps finish at array length not equal to taps count", async() => { + await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [1, 2], [], minVoteRate, minQuorumRate, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if min quorum rate is 0", async() => { + await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [1, 2], timestampsFinishAt, 0, minVoteRate, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if min vote rate is 0", async() => { + await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [1, 2], timestampsFinishAt, minQuorumRate, 0, tokenHoldersCount).should.be.rejectedWith("revert"); + }); + + it("should revert if token holders count is 0", async() => { + await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [1, 2], timestampsFinishAt, minQuorumRate, minVoteRate, 0).should.be.rejectedWith("revert"); + }); + + it("should set contract properties", async() => { + const daicoNew = await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [1, 2], [3,4], 3, 4, 5).should.be.fulfilled; + assert.equal(await daicoNew.daiToken(), daiToken.address); + assert.equal(await daicoNew.projectToken(), projectToken.address); + assert.equal(await daicoNew.projectOwner(), projectOwnerAddress); + assert.equal(await daicoNew.tapsCount(), 2); + assert.equal(await daicoNew.tapAmounts(0), 1); + assert.equal(await daicoNew.tapAmounts(1), 2); + assert.equal(await daicoNew.tapTimestampsFinishAt(0), 3); + assert.equal(await daicoNew.tapTimestampsFinishAt(1), 4); + assert.equal(await daicoNew.minQuorumRate(), 3); + assert.equal(await daicoNew.minVoteRate(), 4); + assert.equal(await daicoNew.tokenHoldersCount(), 5); + }); + + it("should create initial votings of type ReleaseTap", async() => { + assert.equal(await daico.votingsCount(), 2); + const voting = await daico.votings(0); + assert.equal(voting[5].sub(voting[4]), 7 * 24 * 60 * 60); + assert.equal(voting[6], VOTING_TYPE_RELEASE_TAP); + }); + }); + + describe("createVoting()", () => { + it("should revert if quorum rate is 0", async() => { + await daico.createVoting(0, 0, 1, 1, VOTING_TYPE_RELEASE_TAP).should.be.rejectedWith("revert"); + }); + + it("should revert if created at is 0", async() => { + await daico.createVoting(0, 1, 0, 1, VOTING_TYPE_RELEASE_TAP).should.be.rejectedWith("revert"); + }); + + it("should revert if finish at is 0", async() => { + await daico.createVoting(0, 1, 1, 0, VOTING_TYPE_RELEASE_TAP).should.be.rejectedWith("revert"); + }); + + it("should create a new voting", async() => { + await daico.createVoting(0, 1, 2, 3, VOTING_TYPE_RELEASE_TAP).should.be.fulfilled; + const votingsCount = await daico.votingsCount(); + const voting = await daico.votings(votingsCount.sub(1)); + assert.equal(voting[0], 0); + assert.equal(voting[3], 1); + assert.equal(voting[4], 2); + assert.equal(voting[5], 3); + assert.equal(voting[6], VOTING_TYPE_RELEASE_TAP); + }); + + it("should update contract properties", async() => { + const votingsCountBefore = (await daico.votingsCount()).toNumber(); + const tapVotingsCountBefore = (await daico.tapVotingsCount(0)).toNumber(); + + await daico.createVoting(0, 1, 2, 3, VOTING_TYPE_RELEASE_TAP).should.be.fulfilled; + + const votingsCountAfter = (await daico.votingsCount()).toNumber(); + const tapVotingsCountAfter = (await daico.tapVotingsCount(0)).toNumber(); + + assert.equal(await daico.tapVotings(0, tapVotingsCountAfter - 1), votingsCountAfter - 1); + assert.equal(tapVotingsCountAfter - 1, tapVotingsCountBefore); + assert.equal(votingsCountAfter - 1, votingsCountBefore); + }); + }); + + describe("createVotingByInvestor()", () => { + it("should revert if voting type is not: ChangeRoadmap, ChangeRoadmapDecreasedQuorum, TerminateProject, TerminateProjectDecreasedQuorum", async() => { + await daico.createVotingByInvestor(0, VOTING_TYPE_RELEASE_TAP, {from: inverstorAddress}).should.be.rejectedWith("revert"); + }); + + it("should revert if last voting is not finished", async() => { + await daico.createVotingByInvestor(0, VOTING_TYPE_CHANGE_ROADMAP, {from: inverstorAddress}).should.be.rejectedWith("revert"); + }); + }); + + describe("createVotingByOwner()", () => { + it("should revert if voting type is not release tap decreased quorum", async() => { + await daico.createVotingByOwner(0, VOTING_TYPE_RELEASE_TAP, {from: evercityMemberAddress}).should.be.rejectedWith("revert"); + }); + + it("should revert if latest voting for particular tap is not finished", async() => { + await daico.createVotingByOwner(0, VOTING_TYPE_RELEASE_TAP_DECREASED_QUORUM, {from: evercityMemberAddress}).should.be.rejectedWith("revert"); + }); + + // TODO: test 'require(latestVoting.votingType == VotingType.ReleaseTap || latestVoting.votingType == VotingType.ReleaseTapDecreasedQuorum);' + + // TODO: fix increaseTime issue + // it("should revert if latest voting result is not quorum not reached", async() => { + // await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + // await daico.vote(0, true, {from: inverstorAddress2}).should.be.fulfilled; + // await daico.vote(0, true, {from: inverstorAddress3}).should.be.fulfilled; + // await daico.vote(0, true, {from: inverstorAddress4}).should.be.fulfilled; + // await increaseTime(7 * 24 * 60 * 60); + // await daico.createVotingByOwner(0, VOTING_TYPE_RELEASE_TAP_DECREASED_QUORUM, {from: evercityMemberAddress}).should.be.rejectedWith("revert"); + // }); + + // TODO: fix increaseTime issue + // it("should create a new voting", async() => { + // await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + // await daico.vote(0, true, {from: inverstorAddress2}).should.be.fulfilled; + // await daico.vote(0, true, {from: inverstorAddress3}).should.be.fulfilled; + // await increaseTime(7 * 24 * 60 * 60); + // await daico.createVotingByOwner(0, VOTING_TYPE_RELEASE_TAP_DECREASED_QUORUM, {from: evercityMemberAddress}).should.be.fulfilled; + // }); + }); + + describe("getVotingResult()", () => { + it("should return quorum not reached", async() => { + await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress2}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress3}).should.be.fulfilled; + assert.equal(await daico.getVotingResult(0), VOTING_RESULT_QUORUM_NOT_REACHED); + }); + + it("should return accept", async() => { + await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress2}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress3}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress4}).should.be.fulfilled; + assert.equal(await daico.getVotingResult(0), VOTING_RESULT_ACCEPT); + }); + + it("should return decline", async() => { + await daico.vote(0, false, {from: inverstorAddress}).should.be.fulfilled; + await daico.vote(0, false, {from: inverstorAddress2}).should.be.fulfilled; + await daico.vote(0, false, {from: inverstorAddress3}).should.be.fulfilled; + await daico.vote(0, false, {from: inverstorAddress4}).should.be.fulfilled; + assert.equal(await daico.getVotingResult(0), VOTING_RESULT_DECLINE); + }); + + it("should return no decision", async() => { + await daico.vote(0, false, {from: inverstorAddress}).should.be.fulfilled; + await daico.vote(0, false, {from: inverstorAddress2}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress3}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress4}).should.be.fulfilled; + assert.equal(await daico.getVotingResult(0), VOTING_RESULT_NO_DECISION); + }); + }); + + describe("isInvestorVoted()", () => { + it("should revert if investor address is 0x00", async() => { + await daico.isInvestorVoted(0, 0x00).should.be.rejectedWith("revert"); + }); + + it("should return false if investor has not yet voted", async() => { + assert.equal(await daico.isInvestorVoted(0, inverstorAddress), false); + }); + + it("should return true if investor has already voted", async() => { + await daico.vote(0, false, {from: inverstorAddress}).should.be.fulfilled; + assert.equal(await daico.isInvestorVoted(0, inverstorAddress), true); + }); + }); + + describe("isTapWithdrawAcceptedByInvestors()", () => { + it("should return false if investors have not accepted tap release", async() => { + assert.equal(await daico.isTapWithdrawAcceptedByInvestors(0), false); + }); + + it("should return true if investors have accepted tap release", async() => { + await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress2}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress3}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress4}).should.be.fulfilled; + assert.equal(await daico.isTapWithdrawAcceptedByInvestors(0), true); + }); + }); + + describe("vote()", () => { + it("should revert if it is too early to vote", async() => { + await daico.vote(1, true, {from: inverstorAddress}).should.be.rejectedWith("revert"); + }); + + // TODO: fix increase time issue when timestampsFinishAt not in sync with current blockhain timestamp after 'increaseTime()' + // it("should revert if it is too late to vote", async() => { + // const daicoNew = await Daico.new(daiToken.address, projectToken.address, projectOwnerAddress, 2, [1, 2], timestampsFinishAt, minVoteRate, minQuorumRate, tokenHoldersCount); + // await increaseTime(7 * 24 * 60 * 60); + // await daicoNew.vote(0, true, {from: inverstorAddress}).should.be.rejectedWith("revert"); + // }); + + // TODO: test 'require(!isProjectTerminated())' + + it("should revert if investor has already voted", async() => { + await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + await daico.vote(0, true, {from: inverstorAddress}).should.be.rejectedWith("revert"); + }); + + it("should mark investor as already voted", async() => { + assert.equal(await daico.isInvestorVoted(0, inverstorAddress), false); + await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + assert.equal(await daico.isInvestorVoted(0, inverstorAddress), true); + }); + + it("should update yes votes count", async() => { + assert.equal((await daico.votings(0))[1], 0); + await daico.vote(0, true, {from: inverstorAddress}).should.be.fulfilled; + assert.equal((await daico.votings(0))[1], 1); + }); + + it("should update no votes count", async() => { + assert.equal((await daico.votings(0))[2], 0); + await daico.vote(0, false, {from: inverstorAddress}).should.be.fulfilled; + assert.equal((await daico.votings(0))[2], 1); + }); + }); + +});