const { time, loadFixture, } = require('@nomicfoundation/hardhat-toolbox/network-helpers'); const { expect } = require('chai'); const { ethers } = require('hardhat'); describe('DAO', () => { async function deploy() { const [account1, account2] = await ethers.getSigners(); const DAO = await ethers.getContractFactory('DAO'); const dao = await DAO.deploy(); return { dao, account1, account2 }; } it('Should deploy', async () => { const { dao } = await loadFixture(deploy); expect(dao).to.exist; expect(await dao.totalSupply()).to.equal(0); }); describe('Post', () => { it('should be able to add a post', async () => { const { dao, account1 } = await loadFixture(deploy); const contentId = 'some-id'; await expect(dao.addPost(account1, contentId)).to.emit(dao, 'PostAdded').withArgs(0); const post = await dao.posts(0); expect(post.author).to.equal(account1); expect(post.sender).to.equal(account1); expect(post.contentId).to.equal(contentId); }); it('should be able to add a post on behalf of another account', async () => { const { dao, account1, account2 } = await loadFixture(deploy); const contentId = 'some-id'; await dao.addPost(account2, contentId); const post = await dao.posts(0); expect(post.author).to.equal(account2); expect(post.sender).to.equal(account1); expect(post.contentId).to.equal(contentId); }); }); describe('Validation Pool', () => { let dao; let account1; let account2; const POOL_DURATION = 3600; // 1 hour const POOL_FEE = 100; const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); beforeEach(async () => { ({ dao, account1, account2 } = await loadFixture(deploy)); await dao.addPost(account1, 'content-id'); const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0); expect(await dao.validationPoolCount()).to.equal(1); expect(await dao.memberCount()).to.equal(0); expect(await dao.balanceOf(account1)).to.equal(0); expect(await dao.totalSupply()).to.equal(POOL_FEE); }); describe('Initiate', () => { it('should not be able to initiate a validation pool without a fee', async () => { const setup = await loadFixture(deploy); const init = () => setup.dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 100, true, false, callbackData, ); await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool'); }); it('should not be able to initiate a validation pool with a quorum below the minimum', async () => { const setup = await loadFixture(deploy); const init = () => setup.dao.initiateValidationPool( 0, POOL_DURATION, 1, 4, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.be.revertedWith('Quorum is below minimum'); }); it('should not be able to initiate a validation pool with a quorum greater than 1', async () => { const setup = await loadFixture(deploy); const init = () => setup.dao.initiateValidationPool( 0, POOL_DURATION, 11, 10, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.be.revertedWith('Quorum is greater than one'); }); it('should not be able to initiate a validation pool with duration below minimum', async () => { const setup = await loadFixture(deploy); const init = () => setup.dao.initiateValidationPool( 0, 0, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.be.revertedWith('Duration is too short'); }); it('should not be able to initiate a validation pool with duration above maximum', async () => { const setup = await loadFixture(deploy); const init = () => setup.dao.initiateValidationPool( 0, 40000000000000, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.be.revertedWith('Duration is too long'); }); it('should not be able to initiate a validation pool with bindingPercent above 100', async () => { const setup = await loadFixture(deploy); const init = () => setup.dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 101, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.be.revertedWith('Binding percent must be <= 100'); }); it('should be able to initiate a second validation pool', async () => { const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); expect(await dao.validationPoolCount()).to.equal(2); }); it('Should be able to fetch pool instance', async () => { const pool = await dao.validationPools(0); expect(pool).to.exist; expect(pool.duration).to.equal(POOL_DURATION); expect(pool.postIndex).to.equal(0); expect(pool.resolved).to.be.false; expect(pool.sender).to.equal(account1); }); }); describe('Stake', async () => { beforeEach(async () => { time.increase(POOL_DURATION + 1); console.log('evaluating first pool'); await dao.evaluateOutcome(0); expect(await dao.balanceOf(account1)).to.equal(100); expect(await dao.balanceOf(dao.target)).to.equal(0); console.log('initiating second pool'); await dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); expect(await dao.balanceOf(dao.target)).to.equal(100); }); it('should be able to stake before validation pool has elapsed', async () => { console.log('staking on second pool'); await dao.stake(1, 10, true); expect(await dao.balanceOf(account1)).to.equal(90); expect(await dao.balanceOf(dao.target)).to.equal(110); time.increase(POOL_DURATION + 1); console.log('evaluating second pool'); await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true); expect(await dao.balanceOf(dao.target)).to.equal(0); expect(await dao.balanceOf(account1)).to.equal(200); }); it('should not be able to stake after validation pool has elapsed', async () => { time.increase(POOL_DURATION + 1); await expect(dao.stake(1, 10, true)).to.be.revertedWith('Pool end time has passed'); }); it('should be able to stake against a validation pool', async () => { await dao.stake(1, 10, false); expect(await dao.balanceOf(account1)).to.equal(90); expect(await dao.balanceOf(dao.target)).to.equal(110); time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false); expect(await dao.balanceOf(dao.target)).to.equal(0); expect(await dao.balanceOf(account1)).to.equal(200); const pool = await dao.validationPools(1); expect(pool.outcome).to.be.false; }); }); describe('Evaluate outcome', () => { it('should not be able to evaluate outcome before duration has elapsed', async () => { await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool end time has not yet arrived'); }); it('should be able to evaluate outcome after duration has elapsed', async () => { expect(await dao.balanceOf(dao.target)).to.equal(100); time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true); expect(await dao.memberCount()).to.equal(1); expect(await dao.balanceOf(account1)).to.equal(100); const pool = await dao.validationPools(0); expect(pool.resolved).to.be.true; expect(pool.outcome).to.be.true; }); it('should not be able to evaluate outcome more than once', async () => { time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true); await expect(dao.evaluateOutcome(0)).to.be.revertedWith('Pool is already resolved'); }); it('should be able to evaluate outcome of second validation pool', async () => { const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); expect(await dao.validationPoolCount()).to.equal(2); time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true); expect(await dao.balanceOf(account1)).to.equal(100); await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true); expect(await dao.balanceOf(account1)).to.equal(200); }); it('should not be able to evaluate outcome if quorum is not met', async () => { time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true); const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 1, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); expect(await dao.validationPoolCount()).to.equal(2); time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(1)).to.be.revertedWith('Quorum for this pool was not met'); }); describe('Validation pool options', () => { beforeEach(async () => { time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(0); await dao.addPost(account2, 'content-id'); const init = () => dao.initiateValidationPool( 1, POOL_DURATION, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(1); }); it('Binding validation pool should redistribute stakes', async () => { const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 100, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); await dao.connect(account1).stake(2, 10, true); await dao.connect(account2).stake(2, 10, false); expect(await dao.balanceOf(account1)).to.equal(90); expect(await dao.balanceOf(account2)).to.equal(90); expect(await dao.balanceOf(dao.target)).to.equal(120); time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(2); expect(await dao.balanceOf(account1)).to.equal(210); expect(await dao.balanceOf(account2)).to.equal(90); expect(await dao.balanceOf(dao.target)).to.equal(0); }); it('Non binding validation pool should not redistribute stakes', async () => { const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 0, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); await dao.connect(account1).stake(2, 10, true); await dao.connect(account2).stake(2, 10, false); expect(await dao.balanceOf(account1)).to.equal(90); expect(await dao.balanceOf(account2)).to.equal(90); expect(await dao.balanceOf(dao.target)).to.equal(120); time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(2); expect(await dao.balanceOf(account1)).to.equal(200); expect(await dao.balanceOf(account2)).to.equal(100); expect(await dao.balanceOf(dao.target)).to.equal(0); }); it('Partially binding validation pool should redistribute some stakes', async () => { const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 50, true, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); await dao.connect(account1).stake(2, 10, true); await dao.connect(account2).stake(2, 10, false); expect(await dao.balanceOf(account1)).to.equal(90); expect(await dao.balanceOf(account2)).to.equal(90); expect(await dao.balanceOf(dao.target)).to.equal(120); time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(2); expect(await dao.balanceOf(account1)).to.equal(205); expect(await dao.balanceOf(account2)).to.equal(95); expect(await dao.balanceOf(dao.target)).to.equal(0); expect(await dao.totalSupply()).to.equal(300); }); it('If redistributeLosingStakes is false, validation pool should burn binding portion of losing stakes', async () => { const init = () => dao.initiateValidationPool( 0, POOL_DURATION, 1, 3, 50, false, false, callbackData, { value: POOL_FEE }, ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); await dao.connect(account1).stake(2, 10, true); await dao.connect(account2).stake(2, 10, false); expect(await dao.balanceOf(account1)).to.equal(90); expect(await dao.balanceOf(account2)).to.equal(90); expect(await dao.balanceOf(dao.target)).to.equal(120); time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(2); expect(await dao.balanceOf(account1)).to.equal(200); expect(await dao.balanceOf(account2)).to.equal(95); expect(await dao.balanceOf(dao.target)).to.equal(0); expect(await dao.totalSupply()).to.equal(295); }); }); }); }); });