2024-04-10 16:31:31 -05:00
|
|
|
const {
|
|
|
|
time,
|
|
|
|
loadFixture,
|
|
|
|
} = require('@nomicfoundation/hardhat-toolbox/network-helpers');
|
|
|
|
const { expect } = require('chai');
|
|
|
|
const { ethers } = require('hardhat');
|
2024-04-30 15:56:43 -05:00
|
|
|
const deployDAO = require('./util/deploy-dao');
|
2024-04-10 16:31:31 -05:00
|
|
|
|
|
|
|
describe('Validation Pools', () => {
|
|
|
|
async function deploy() {
|
|
|
|
const [account1, account2] = await ethers.getSigners();
|
2024-06-28 13:44:18 -05:00
|
|
|
const { dao, forum } = await deployDAO();
|
2024-04-30 15:56:43 -05:00
|
|
|
return {
|
2024-06-28 13:44:18 -05:00
|
|
|
dao, forum, account1, account2,
|
2024-04-30 15:56:43 -05:00
|
|
|
};
|
2024-04-10 16:31:31 -05:00
|
|
|
}
|
|
|
|
let dao;
|
2024-06-28 13:44:18 -05:00
|
|
|
let forum;
|
2024-04-10 16:31:31 -05:00
|
|
|
let account1;
|
|
|
|
let account2;
|
|
|
|
const POOL_DURATION = 3600; // 1 hour
|
|
|
|
const POOL_FEE = 100;
|
|
|
|
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
|
|
|
|
|
|
|
|
const initiateValidationPool = ({
|
2024-04-18 18:35:31 -05:00
|
|
|
postId, duration,
|
2024-04-10 16:31:31 -05:00
|
|
|
quorum, winRatio, bindingPercent,
|
|
|
|
redistributeLosingStakes, callbackOnValidate,
|
|
|
|
callbackData, fee,
|
|
|
|
} = {}) => dao.initiateValidationPool(
|
2024-04-18 18:35:31 -05:00
|
|
|
postId ?? 'content-id',
|
2024-04-10 16:31:31 -05:00
|
|
|
duration ?? POOL_DURATION,
|
|
|
|
quorum ?? [1, 3],
|
|
|
|
winRatio ?? [1, 2],
|
|
|
|
bindingPercent ?? 100,
|
|
|
|
redistributeLosingStakes ?? true,
|
|
|
|
callbackOnValidate ?? false,
|
|
|
|
callbackData ?? emptyCallbackData,
|
|
|
|
{ value: fee ?? POOL_FEE },
|
|
|
|
);
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
2024-04-30 15:56:43 -05:00
|
|
|
({
|
2024-06-28 13:44:18 -05:00
|
|
|
dao, forum, account1, account2,
|
2024-04-30 15:56:43 -05:00
|
|
|
} = await loadFixture(deploy));
|
2024-06-28 13:44:18 -05:00
|
|
|
await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []);
|
2024-04-10 16:31:31 -05:00
|
|
|
const init = () => initiateValidationPool({ fee: POOL_FEE });
|
|
|
|
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(0);
|
2024-06-29 12:57:07 -05:00
|
|
|
expect(await dao.getValidationPoolCount()).to.equal(1);
|
2024-04-10 16:31:31 -05:00
|
|
|
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', () => {
|
2024-05-15 11:35:26 -05:00
|
|
|
it('should be able to initiate a validation pool without a fee', async () => {
|
2024-04-10 16:31:31 -05:00
|
|
|
const init = () => initiateValidationPool({ fee: 0 });
|
2024-05-15 11:35:26 -05:00
|
|
|
await expect(init()).to.emit(dao, 'ValidationPoolInitiated');
|
2024-04-10 16:31:31 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
2024-06-29 12:57:07 -05:00
|
|
|
expect(await dao.getValidationPoolCount()).to.equal(2);
|
2024-04-10 16:31:31 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Should be able to fetch pool instance', async () => {
|
2024-06-29 12:57:07 -05:00
|
|
|
const pool = await dao.getValidationPool(0);
|
2024-04-10 16:31:31 -05:00
|
|
|
expect(pool).to.exist;
|
|
|
|
expect(pool.params.duration).to.equal(POOL_DURATION);
|
2024-04-30 15:56:43 -05:00
|
|
|
expect(pool.props.postId).to.equal('content-id');
|
|
|
|
expect(pool.props.resolved).to.be.false;
|
2024-04-10 16:31:31 -05:00
|
|
|
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);
|
2024-06-29 12:57:07 -05:00
|
|
|
const pool = await dao.getValidationPool(1);
|
2024-04-30 15:56:43 -05:00
|
|
|
expect(pool.props.outcome).to.be.false;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not be able to stake more REP than the sender owns', async () => {
|
|
|
|
await expect(dao.stakeOnValidationPool(1, 200, true)).to.be.revertedWith('Insufficient REP balance to cover stake');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Delegated stake', () => {
|
|
|
|
it('should stake the lesser of the allowed amount or the owner\'s remaining balance', async () => {
|
|
|
|
// TODO: owner delegates stake and then loses rep
|
2024-04-10 16:31:31 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
2024-06-29 12:57:07 -05:00
|
|
|
const pool = await dao.getValidationPool(0);
|
2024-04-30 15:56:43 -05:00
|
|
|
expect(pool.props.resolved).to.be.true;
|
|
|
|
expect(pool.props.outcome).to.be.true;
|
2024-04-10 16:31:31 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
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 () => {
|
2024-04-18 18:35:31 -05:00
|
|
|
const init = () => initiateValidationPool();
|
2024-04-10 16:31:31 -05:00
|
|
|
await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1);
|
2024-06-29 12:57:07 -05:00
|
|
|
expect(await dao.getValidationPoolCount()).to.equal(2);
|
2024-04-10 16:31:31 -05:00
|
|
|
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);
|
2024-06-29 12:57:07 -05:00
|
|
|
expect(await dao.getValidationPoolCount()).to.equal(2);
|
2024-04-10 16:31:31 -05:00
|
|
|
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);
|
2024-06-28 13:44:18 -05:00
|
|
|
await forum.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []);
|
2024-04-18 18:35:31 -05:00
|
|
|
const init = () => initiateValidationPool({ postId: 'content-id-2' });
|
2024-04-10 16:31:31 -05:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|