const { time, loadFixture, } = require('@nomicfoundation/hardhat-toolbox/network-helpers'); const { expect } = require('chai'); const { ethers } = require('hardhat'); const { beforeEach } = require('mocha'); const deployDAO = require('./util/deploy-dao'); describe('Proposal', () => { async function deploy() { // Contracts are deployed using the first signer/account by default const [account1, account2] = await ethers.getSigners(); const { dao } = await deployDAO(); const Proposals = await ethers.getContractFactory('Proposals'); const proposals = await Proposals.deploy(dao.target); await dao.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'some-content-id', []); await dao.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'some-other-content-id', []); const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); await dao.initiateValidationPool( 'some-content-id', 60, [1, 3], [1, 2], 100, true, false, callbackData, { value: 1000 }, ); await dao.initiateValidationPool( 'some-other-content-id', 60, [1, 3], [1, 2], 100, true, false, callbackData, { value: 1000 }, ); await time.increase(61); await dao.evaluateOutcome(0); await dao.evaluateOutcome(1); return { dao, proposals, account1, account2, }; } it('Should deploy', async () => { const { dao, proposals, account1, account2, } = await loadFixture(deploy); expect(dao).to.exist; expect(proposals).to.exist; expect(await dao.memberCount()).to.equal(2); expect(await dao.balanceOf(account1)).to.equal(1000); expect(await dao.balanceOf(account2)).to.equal(1000); expect(await dao.totalSupply()).to.equal(2000); expect(await proposals.proposalCount()).to.equal(0); }); describe('Attestation', () => { let dao; let proposals; let account1; let account2; let proposal; beforeEach(async () => { ({ dao, proposals, account1, account2, } = await loadFixture(deploy)); const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); await dao.addPost([{ authorAddress: account1, weightPPM: 1000000 }], 'proposal-content-id', []); await proposals.propose('proposal-content-id', [20, 20, 20], false, emptyCallbackData, { value: 100 }); expect(await proposals.proposalCount()).to.equal(1); proposal = await proposals.proposals(0); expect(proposal.postId).to.equal('proposal-content-id'); expect(proposal.stage).to.equal(0); }); it('Can submit a proposal', async () => { const postAuthors = await dao.getPostAuthors('proposal-content-id'); expect(postAuthors).to.have.length(1); expect(postAuthors[0].weightPPM).to.equal(1000000); expect(postAuthors[0].authorAddress).to.equal(account1); }); it('Can attest for a proposal', async () => { await proposals.connect(account1).attest(0, 200); // Nonbinding, non-encumbering expect(await dao.balanceOf(account1)).to.equal(1000); }); describe('Evaluate attestation', () => { it('when threshold is met, advance to referendum 0% binding', async () => { await proposals.attest(0, 200); await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(1); }); it('threshold may be met by accumulation of attestations', async () => { await proposals.connect(account1).attest(0, 100); await proposals.connect(account2).attest(0, 100); await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(1); }); it('when threshold is not met, and duration has not elapsed, do nothing', async () => { await proposals.evaluateAttestation(0); expect(proposal.stage).to.equal(0); }); it('when threshold is not met, and duration has elapsed, close the proposal', async () => { await time.increase(365 * 86400 + 1); // 1 year + 1 second await proposals.evaluateAttestation(0); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(4); // Stage.Failed }); }); describe('Referendum 0% binding', () => { beforeEach(async () => { await proposals.attest(0, 200); await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(1); }); it('proposal dies if it fails to meet quorum', async () => { await time.increase(21); await expect(dao.evaluateOutcome(2)).to.emit(dao, 'ValidationPoolResolved').withArgs(2, false, false); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(4); // Stage.Failed }); it('referendum retries if it fails to meet participation rate', async () => { await dao.stakeOnValidationPool(2, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(2)) .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(3); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(1); }); it('referendum retries if it fails to meet win ratio', async () => { await dao.stakeOnValidationPool(2, 1000, false); await time.increase(21); await expect(dao.evaluateOutcome(2)) .to.emit(dao, 'ValidationPoolResolved').withArgs(2, false, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(3); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(1); const pools = await proposals.getPools(0); expect(pools[0][0].started).to.be.true; expect(pools[0][1].started).to.be.true; expect(pools[0][2].started).to.be.false; expect(pools[0][0].completed).to.be.true; expect(pools[0][1].completed).to.be.false; expect(pools[0][2].completed).to.be.false; }); it('proposal fails if a referendum fails to meet participation rate 3 times', async () => { await dao.stakeOnValidationPool(2, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(2)) .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(3); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(1); await dao.stakeOnValidationPool(3, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(3)) .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(4); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(1); await dao.stakeOnValidationPool(4, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(4)) .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(4); }); it('advances to next referendum if it meets participation rate and win ratio', async () => { await dao.stakeOnValidationPool(2, 1000, true); await time.increase(21); await expect(dao.evaluateOutcome(2)) .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(3); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(2); }); }); describe('Referendum 1% binding', () => { beforeEach(async () => { await proposals.attest(0, 200); await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); await dao.stakeOnValidationPool(2, 1000, true); await time.increase(21); await expect(dao.evaluateOutcome(2)) .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(3); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(2); }); afterEach(async () => { const pool = await dao.validationPools(3); expect(pool.props.resolved).to.be.true; }); it('proposal dies if it fails to meet quorum', async () => { await time.increase(21); await expect(dao.evaluateOutcome(3)).to.emit(dao, 'ValidationPoolResolved').withArgs(3, false, false); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(4); // Stage.Failed }); it('referendum retries if it fails to meet participation rate', async () => { await dao.stakeOnValidationPool(3, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(3)) .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(4); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(2); }); it('referendum retries if it fails to meet win ratio', async () => { await dao.stakeOnValidationPool(3, 1000, false); await time.increase(21); await expect(dao.evaluateOutcome(3)) .to.emit(dao, 'ValidationPoolResolved').withArgs(3, false, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(4); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(2); }); it('proposal fails if a referendum fails to meet participation rate 3 times', async () => { await dao.stakeOnValidationPool(3, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(3)) .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(4); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(2); await dao.stakeOnValidationPool(4, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(4)) .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(5); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(2); await dao.stakeOnValidationPool(5, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(5)) .to.emit(dao, 'ValidationPoolResolved').withArgs(5, true, true); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(4); }); it('advances to next referendum if it meets participation rate and win ratio', async () => { await dao.stakeOnValidationPool(3, 1000, true); await time.increase(21); await expect(dao.evaluateOutcome(3)) .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(4); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(3); }); }); describe('Referendum 100% binding', () => { beforeEach(async () => { await proposals.attest(0, 200); await expect(proposals.evaluateAttestation(0)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); await dao.stakeOnValidationPool(2, 1000, true); await time.increase(21); await expect(dao.evaluateOutcome(2)) .to.emit(dao, 'ValidationPoolResolved').withArgs(2, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(3); await dao.stakeOnValidationPool(3, 1000, true); await time.increase(21); await expect(dao.evaluateOutcome(3)) .to.emit(dao, 'ValidationPoolResolved').withArgs(3, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(4); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(3); }); afterEach(async () => { const pool = await dao.validationPools(4); expect(pool.props.resolved).to.be.true; }); it('proposal dies if it fails to meet quorum', async () => { await time.increase(21); await expect(dao.evaluateOutcome(4)).to.emit(dao, 'ValidationPoolResolved').withArgs(4, false, false); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(4); // Stage.Failed }); it('referendum retries if it fails to meet participation rate', async () => { await dao.stakeOnValidationPool(4, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(4)) .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(5); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(3); }); it('referendum retries if it fails to meet win ratio', async () => { await dao.stakeOnValidationPool(4, 1000, false); await time.increase(21); await expect(dao.evaluateOutcome(4)) .to.emit(dao, 'ValidationPoolResolved').withArgs(4, false, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(5); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(3); }); it('proposal fails if a referendum fails to meet participation rate 3 times', async () => { await dao.stakeOnValidationPool(4, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(4)) .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(5); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(3); await dao.stakeOnValidationPool(5, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(5)) .to.emit(dao, 'ValidationPoolResolved').withArgs(5, true, true) .to.emit(dao, 'ValidationPoolInitiated').withArgs(6); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(3); await dao.stakeOnValidationPool(6, 200, true); await time.increase(21); await expect(dao.evaluateOutcome(6)) .to.emit(dao, 'ValidationPoolResolved').withArgs(6, true, true); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(4); }); it('advances to accepted stage if it meets participation rate and win ratio', async () => { await dao.connect(account1).stakeOnValidationPool(4, 1000, true); await dao.connect(account2).stakeOnValidationPool(4, 1000, true); await time.increase(21); await expect(dao.evaluateOutcome(4)) .to.emit(dao, 'ValidationPoolResolved').withArgs(4, true, true); proposal = await proposals.proposals(0); expect(proposal.stage).to.equal(5); }); }); }); });