From dc18c69b0966db3666775625e1bad8c517ca564f Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Wed, 10 Apr 2024 16:31:31 -0500 Subject: [PATCH] propagate reputation to citations --- ethereum/contracts/core/Forum.sol | 23 ++- ethereum/test/DAO.js | 328 ------------------------------ ethereum/test/Forum.js | 73 +++++++ ethereum/test/ValidationPools.js | 297 +++++++++++++++++++++++++++ 4 files changed, 387 insertions(+), 334 deletions(-) delete mode 100644 ethereum/test/DAO.js create mode 100644 ethereum/test/Forum.js create mode 100644 ethereum/test/ValidationPools.js diff --git a/ethereum/contracts/core/Forum.sol b/ethereum/contracts/core/Forum.sol index 9da1e95..667e2c5 100644 --- a/ethereum/contracts/core/Forum.sol +++ b/ethereum/contracts/core/Forum.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.24; import "./Reputation.sol"; struct Citation { - int[2] weight; + int weightPercent; uint targetPostIndex; } @@ -13,7 +13,6 @@ struct Post { address sender; address author; string contentId; - uint reputation; Citation[] citations; } @@ -41,10 +40,22 @@ contract Forum is Reputation { } function _onValidatePost(uint postIndex, uint amount) internal { - Post storage post = posts[postIndex]; - post.reputation = amount; - _update(address(this), post.author, amount); + _propagateValue(postIndex, int(amount)); } - function _propagateValue() internal {} + function _propagateValue(uint postIndex, int amount) internal { + Post storage post = posts[postIndex]; + int totalCitationWeight; + for (uint i = 0; i < post.citations.length; i++) { + totalCitationWeight += post.citations[i].weightPercent; + } + int totalOutboundAmount; + for (uint i = 0; i < post.citations.length; i++) { + int share = (amount * post.citations[i].weightPercent) / 100; + totalOutboundAmount += share; + _propagateValue(post.citations[i].targetPostIndex, share); + } + uint remaining = uint(amount - totalOutboundAmount); + _update(address(this), post.author, remaining); + } } diff --git a/ethereum/test/DAO.js b/ethereum/test/DAO.js deleted file mode 100644 index 8c2285d..0000000 --- a/ethereum/test/DAO.js +++ /dev/null @@ -1,328 +0,0 @@ -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 emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); - - const initiateValidationPool = ({ - postIndex, duration, - quorum, winRatio, bindingPercent, - redistributeLosingStakes, callbackOnValidate, - callbackData, fee, - } = {}) => dao.initiateValidationPool( - postIndex ?? 0, - duration ?? POOL_DURATION, - quorum ?? [1, 3], - winRatio ?? [1, 2], - bindingPercent ?? 100, - redistributeLosingStakes ?? true, - callbackOnValidate ?? false, - callbackData ?? emptyCallbackData, - { value: fee ?? POOL_FEE }, - ); - - beforeEach(async () => { - ({ dao, account1, account2 } = await loadFixture(deploy)); - await dao.addPost(account1, 'content-id', []); - const init = () => initiateValidationPool({ fee: 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 init = () => initiateValidationPool({ fee: 0 }); - 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 init = () => initiateValidationPool({ quorum: [1, 11] }); - 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 init = () => initiateValidationPool({ quorum: [11, 10] }); - 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 init = () => initiateValidationPool({ duration: 0 }); - 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 init = () => initiateValidationPool({ duration: 40000000000000 }); - 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 init = () => initiateValidationPool({ bindingPercent: 101 }); - await expect(init()).to.be.revertedWith('Binding percent must be <= 100'); - }); - - it('should be able to initiate a second validation pool', async () => { - const init = () => initiateValidationPool(); - 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.params.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); - await dao.evaluateOutcome(0); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(0); - await initiateValidationPool(); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - }); - - it('should be able to stake before validation pool has elapsed', async () => { - await dao.stakeOnValidationPool(1, 10, true); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true); - expect(await dao.balanceOf(account1)).to.equal(200); - expect(await dao.balanceOf(dao.target)).to.equal(0); - }); - - it('should not be able to stake after validation pool has elapsed', async () => { - time.increase(POOL_DURATION + 1); - await expect(dao.stakeOnValidationPool(1, 10, true)).to.be.revertedWith('Pool end time has passed'); - }); - - it('should be able to stake against a validation pool', async () => { - await dao.stakeOnValidationPool(1, 10, false); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, true); - expect(await dao.balanceOf(account1)).to.equal(200); - expect(await dao.balanceOf(dao.target)).to.equal(0); - 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 if not all rep has been staked', async () => { - time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(0)); - await initiateValidationPool({ fee: 100 }); - await expect(dao.evaluateOutcome(1)).to.be.revertedWith('Pool end time has not yet arrived'); - }); - - it('should not be able to evaluate outcome before duration has elapsed unless all rep has been staked', async () => { - time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(0)); - await initiateValidationPool({ fee: 100 }); - await dao.stakeOnValidationPool(1, 100, true); - await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true); - }); - - 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, 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, 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], - [1, 2], - 100, - true, - false, - emptyCallbackData, - { 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, true); - expect(await dao.balanceOf(account1)).to.equal(100); - await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, 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, true); - - const init = () => initiateValidationPool({ quorum: [1, 1] }); - 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.emit(dao, 'ValidationPoolResolved').withArgs(1, false, false); - }); - - describe('Validation pool options', () => { - beforeEach(async () => { - time.increase(POOL_DURATION + 1); - await dao.evaluateOutcome(0); - await dao.addPost(account2, 'content-id', []); - const init = () => initiateValidationPool({ postIndex: 1 }); - 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 = () => initiateValidationPool(); - await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); - await dao.connect(account1).stakeOnValidationPool(2, 10, true); - await dao.connect(account2).stakeOnValidationPool(2, 10, false); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(account2)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - 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 = () => initiateValidationPool({ bindingPercent: 0 }); - await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); - await dao.connect(account1).stakeOnValidationPool(2, 10, true); - await dao.connect(account2).stakeOnValidationPool(2, 10, false); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(account2)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - 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 = () => initiateValidationPool({ bindingPercent: 50 }); - await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); - await dao.connect(account1).stakeOnValidationPool(2, 10, true); - await dao.connect(account2).stakeOnValidationPool(2, 10, false); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(account2)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - 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 = () => initiateValidationPool({ - bindingPercent: 50, - redistributeLosingStakes: false, - }); - await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); - await dao.connect(account1).stakeOnValidationPool(2, 10, true); - await dao.connect(account2).stakeOnValidationPool(2, 10, false); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(account2)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - 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); - }); - - it('If redistributeLosingStakes is false and bindingPercent is 0, accounts should recover initial balances', async () => { - const init = () => initiateValidationPool({ - bindingPercent: 0, - redistributeLosingStakes: false, - }); - await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); - await dao.connect(account1).stakeOnValidationPool(2, 10, true); - await dao.connect(account2).stakeOnValidationPool(2, 10, false); - expect(await dao.balanceOf(account1)).to.equal(100); - expect(await dao.balanceOf(account2)).to.equal(100); - expect(await dao.balanceOf(dao.target)).to.equal(100); - 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); - expect(await dao.totalSupply()).to.equal(300); - }); - }); - }); - }); -}); diff --git a/ethereum/test/Forum.js b/ethereum/test/Forum.js new file mode 100644 index 0000000..c19ee9c --- /dev/null +++ b/ethereum/test/Forum.js @@ -0,0 +1,73 @@ +const { + time, + loadFixture, +} = require('@nomicfoundation/hardhat-toolbox/network-helpers'); +const { expect } = require('chai'); +const { ethers } = require('hardhat'); + +describe('Forum', () => { + async function deploy() { + const [account1, account2] = await ethers.getSigners(); + const DAO = await ethers.getContractFactory('DAO'); + const dao = await DAO.deploy(); + return { dao, account1, account2 }; + } + let dao; + let account1; + let account2; + const POOL_DURATION = 3600; // 1 hour + const POOL_FEE = 100; + const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); + + const initiateValidationPool = ({ + postIndex, duration, + quorum, winRatio, bindingPercent, + redistributeLosingStakes, callbackOnValidate, + callbackData, fee, + } = {}) => dao.initiateValidationPool( + postIndex ?? 0, + duration ?? POOL_DURATION, + quorum ?? [1, 3], + winRatio ?? [1, 2], + bindingPercent ?? 100, + redistributeLosingStakes ?? true, + callbackOnValidate ?? false, + callbackData ?? emptyCallbackData, + { value: fee ?? POOL_FEE }, + ); + + describe('Post', () => { + beforeEach(async () => { + ({ dao, account1, account2 } = await loadFixture(deploy)); + }); + + it('should be able to add a post', async () => { + 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 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); + }); + + it('should propagate reputation to citations', async () => { + await dao.addPost(account1, 'content-id', []); + await dao.addPost(account2, 'second-content-id', [{ weightPercent: 50, targetPostIndex: 0 }]); + await initiateValidationPool({ postIndex: 1 }); + const pool = await dao.validationPools(0); + expect(pool.postIndex).to.equal(1); + await dao.evaluateOutcome(0); + expect(await dao.balanceOf(account2)).to.equal(50); + expect(await dao.balanceOf(account1)).to.equal(50); + }); + }); +}); diff --git a/ethereum/test/ValidationPools.js b/ethereum/test/ValidationPools.js new file mode 100644 index 0000000..bc5d7fb --- /dev/null +++ b/ethereum/test/ValidationPools.js @@ -0,0 +1,297 @@ +const { + time, + loadFixture, +} = require('@nomicfoundation/hardhat-toolbox/network-helpers'); +const { expect } = require('chai'); +const { ethers } = require('hardhat'); + +describe('Validation Pools', () => { + async function deploy() { + const [account1, account2] = await ethers.getSigners(); + const DAO = await ethers.getContractFactory('DAO'); + const dao = await DAO.deploy(); + return { dao, account1, account2 }; + } + let dao; + let account1; + let account2; + const POOL_DURATION = 3600; // 1 hour + const POOL_FEE = 100; + const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); + + const initiateValidationPool = ({ + postIndex, duration, + quorum, winRatio, bindingPercent, + redistributeLosingStakes, callbackOnValidate, + callbackData, fee, + } = {}) => dao.initiateValidationPool( + postIndex ?? 0, + duration ?? POOL_DURATION, + quorum ?? [1, 3], + winRatio ?? [1, 2], + bindingPercent ?? 100, + redistributeLosingStakes ?? true, + callbackOnValidate ?? false, + callbackData ?? emptyCallbackData, + { value: fee ?? POOL_FEE }, + ); + + beforeEach(async () => { + ({ dao, account1, account2 } = await loadFixture(deploy)); + await dao.addPost(account1, 'content-id', []); + const init = () => initiateValidationPool({ fee: 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 init = () => initiateValidationPool({ fee: 0 }); + 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 init = () => initiateValidationPool({ quorum: [1, 11] }); + 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 init = () => initiateValidationPool({ quorum: [11, 10] }); + 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 init = () => initiateValidationPool({ duration: 0 }); + 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 init = () => initiateValidationPool({ duration: 40000000000000 }); + 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 init = () => initiateValidationPool({ bindingPercent: 101 }); + await expect(init()).to.be.revertedWith('Binding percent must be <= 100'); + }); + + it('should be able to initiate a second validation pool', async () => { + const init = () => initiateValidationPool(); + 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.params.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); + await dao.evaluateOutcome(0); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(0); + await initiateValidationPool(); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + }); + + it('should be able to stake before validation pool has elapsed', async () => { + await dao.stakeOnValidationPool(1, 10, true); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + time.increase(POOL_DURATION + 1); + await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true); + expect(await dao.balanceOf(account1)).to.equal(200); + expect(await dao.balanceOf(dao.target)).to.equal(0); + }); + + it('should not be able to stake after validation pool has elapsed', async () => { + time.increase(POOL_DURATION + 1); + await expect(dao.stakeOnValidationPool(1, 10, true)).to.be.revertedWith('Pool end time has passed'); + }); + + it('should be able to stake against a validation pool', async () => { + await dao.stakeOnValidationPool(1, 10, false); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + time.increase(POOL_DURATION + 1); + await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, true); + expect(await dao.balanceOf(account1)).to.equal(200); + expect(await dao.balanceOf(dao.target)).to.equal(0); + 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 if not all rep has been staked', async () => { + time.increase(POOL_DURATION + 1); + await expect(dao.evaluateOutcome(0)); + await initiateValidationPool({ fee: 100 }); + await expect(dao.evaluateOutcome(1)).to.be.revertedWith('Pool end time has not yet arrived'); + }); + + it('should not be able to evaluate outcome before duration has elapsed unless all rep has been staked', async () => { + time.increase(POOL_DURATION + 1); + await expect(dao.evaluateOutcome(0)); + await initiateValidationPool({ fee: 100 }); + await dao.stakeOnValidationPool(1, 100, true); + await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, true); + }); + + 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, 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, 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], + [1, 2], + 100, + true, + false, + emptyCallbackData, + { 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, true); + expect(await dao.balanceOf(account1)).to.equal(100); + await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, true, 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, true); + + const init = () => initiateValidationPool({ quorum: [1, 1] }); + 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.emit(dao, 'ValidationPoolResolved').withArgs(1, false, false); + }); + + describe('Validation pool options', () => { + beforeEach(async () => { + time.increase(POOL_DURATION + 1); + await dao.evaluateOutcome(0); + await dao.addPost(account2, 'content-id', []); + const init = () => initiateValidationPool({ postIndex: 1 }); + 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 = () => initiateValidationPool(); + await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); + await dao.connect(account1).stakeOnValidationPool(2, 10, true); + await dao.connect(account2).stakeOnValidationPool(2, 10, false); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + 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 = () => initiateValidationPool({ bindingPercent: 0 }); + await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); + await dao.connect(account1).stakeOnValidationPool(2, 10, true); + await dao.connect(account2).stakeOnValidationPool(2, 10, false); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + 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 = () => initiateValidationPool({ bindingPercent: 50 }); + await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); + await dao.connect(account1).stakeOnValidationPool(2, 10, true); + await dao.connect(account2).stakeOnValidationPool(2, 10, false); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + 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 = () => initiateValidationPool({ + bindingPercent: 50, + redistributeLosingStakes: false, + }); + await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); + await dao.connect(account1).stakeOnValidationPool(2, 10, true); + await dao.connect(account2).stakeOnValidationPool(2, 10, false); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + 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); + }); + + it('If redistributeLosingStakes is false and bindingPercent is 0, accounts should recover initial balances', async () => { + const init = () => initiateValidationPool({ + bindingPercent: 0, + redistributeLosingStakes: false, + }); + await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); + await dao.connect(account1).stakeOnValidationPool(2, 10, true); + await dao.connect(account2).stakeOnValidationPool(2, 10, false); + expect(await dao.balanceOf(account1)).to.equal(100); + expect(await dao.balanceOf(account2)).to.equal(100); + expect(await dao.balanceOf(dao.target)).to.equal(100); + 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); + expect(await dao.totalSupply()).to.equal(300); + }); + }); + }); +});