diff --git a/backend/src/event-handlers/validation-pools.js b/backend/src/event-handlers/validation-pools.js index beb8571..6b693df 100644 --- a/backend/src/event-handlers/validation-pools.js +++ b/backend/src/event-handlers/validation-pools.js @@ -28,7 +28,7 @@ if (ENABLE_STAKING === 'false') { const start = async () => { dao.on('ValidationPoolInitiated', async (poolIndex) => { console.log('Validation Pool Initiated, index', poolIndex); - const pool = await dao.validationPools(poolIndex); + const pool = await dao.getValidationPool(poolIndex); // Read post from database let post; try { diff --git a/ethereum/contracts/core/DAO.sol b/ethereum/contracts/core/DAO.sol index 42d1dfd..8d04e99 100644 --- a/ethereum/contracts/core/DAO.sol +++ b/ethereum/contracts/core/DAO.sol @@ -4,16 +4,16 @@ pragma solidity ^0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./Reputation.sol"; import "./Bench.sol"; +import "./LightweightBench.sol"; import "../GlobalForum.sol"; import "../interfaces/IAcceptAvailability.sol"; import "../interfaces/IOnValidate.sol"; -import "hardhat/console.sol"; - contract DAO { Reputation rep; GlobalForum forum; Bench bench; + LightweightBench lightweightBench; mapping(uint => address) public members; uint public memberCount; mapping(address => bool) public isMember; @@ -31,16 +31,28 @@ contract DAO { bool votePasses, bool quorumMet ); + event LWResultProposed( + uint poolIndex, + uint proposedResultIndex, + string proposedResultHash + ); - constructor(Reputation reputation_, Bench bench_, GlobalForum forum_) { + constructor( + Reputation reputation_, + Bench bench_, + LightweightBench lightweightBench_, + GlobalForum forum_ + ) { rep = reputation_; bench = bench_; + lightweightBench = lightweightBench_; forum = forum_; rep.registerDAO(this); bench.registerDAO(this, forum); + lightweightBench.registerDAO(this); } - function emitPostAdded(string memory id) public { + function emitPostAdded(string calldata id) public { emit PostAdded(id); } @@ -60,9 +72,22 @@ contract DAO { emit LWValidationPoolInitiated(poolIndex); } + function emitLWResultProposed( + uint poolIndex, + uint proposedResultIndex, + string calldata proposedResultHash + ) public { + emit LWResultProposed( + poolIndex, + proposedResultIndex, + proposedResultHash + ); + } + function update(address from, address to, uint256 value) public { require( - msg.sender == address(bench), + msg.sender == address(lightweightBench) || + msg.sender == address(bench), "Only DAO core contracts may call update" ); rep.update(from, to, value); @@ -70,7 +95,8 @@ contract DAO { function mint(address account, uint256 value) public { require( - msg.sender == address(bench), + msg.sender == address(lightweightBench) || + msg.sender == address(bench), "Only DAO core contracts may call mint" ); rep.mint(account, value); @@ -78,7 +104,8 @@ contract DAO { function burn(address account, uint256 value) public { require( - msg.sender == address(bench), + msg.sender == address(lightweightBench) || + msg.sender == address(bench), "Only DAO core contracts may call burn" ); rep.burn(account, value); @@ -86,7 +113,8 @@ contract DAO { function registerMember(address account) public { require( - msg.sender == address(bench), + msg.sender == address(lightweightBench) || + msg.sender == address(bench), "Only DAO core contracts may call registerMember" ); if (!isMember[account]) { @@ -155,7 +183,7 @@ contract DAO { } } - function validationPools( + function getValidationPool( uint poolIndex ) public @@ -173,7 +201,7 @@ contract DAO { return bench.validationPools(poolIndex); } - function validationPoolCount() public view returns (uint) { + function getValidationPoolCount() public view returns (uint) { return bench.validationPoolCount(); } @@ -210,7 +238,6 @@ contract DAO { balanceOf(msg.sender) >= amount, "Insufficient REP balance to cover stake" ); - // TODO: Encumber tokens bench.stakeOnValidationPool(poolIndex, msg.sender, amount, inFavor); } @@ -232,6 +259,104 @@ contract DAO { return bench.evaluateOutcome(poolIndex); } + function getLWValidationPool( + uint poolIndex + ) + public + view + returns ( + uint id, + address sender, + uint stakeCount, + LWVPoolParams memory params, + LWVPoolProps memory props, + bool callbackOnValidate, + bytes memory callbackData + ) + { + return lightweightBench.validationPools(poolIndex); + } + + function getLWValidationPoolCount() public view returns (uint) { + return lightweightBench.validationPoolCount(); + } + + function initiateLWValidationPool( + string calldata postId, + uint duration, + uint[2] calldata quorum, // [Numerator, Denominator] + uint[2] calldata winRatio, // [Numerator, Denominator] + uint bindingPercent, + bool redistributeLosingStakes, + bool callbackOnValidate, + bytes calldata callbackData + ) external payable returns (uint) { + return + lightweightBench.initiateValidationPool{value: msg.value}( + msg.sender, + postId, + duration, + quorum, + winRatio, + bindingPercent, + redistributeLosingStakes, + callbackOnValidate, + callbackData + ); + } + + function proposeLWResult( + uint poolIndex, + string calldata resultHash, + Transfer[] calldata transfers + ) external { + lightweightBench.proposeResult(poolIndex, resultHash, transfers); + } + + function stakeOnLWValidationPool( + uint poolIndex, + string calldata resultHash, + uint256 amount, + bool inFavor + ) public { + require( + balanceOf(msg.sender) >= amount, + "Insufficient REP balance to cover stake" + ); + lightweightBench.stakeOnValidationPool( + poolIndex, + resultHash, + msg.sender, + amount, + inFavor + ); + } + + /// Accept reputation stakes toward a validation pool + function delegatedStakeOnLWValidationPool( + uint poolIndex, + string calldata resultHash, + address owner, + uint256 amount, + bool inFavor + ) public { + if (allowance(owner, msg.sender) < amount) { + amount = allowance(owner, msg.sender); + } + rep.spendAllowance(owner, msg.sender, amount); + lightweightBench.stakeOnValidationPool( + poolIndex, + resultHash, + owner, + amount, + inFavor + ); + } + + function evaluateLWOutcome(uint poolIndex) public returns (bool) { + return lightweightBench.evaluateOutcome(poolIndex); + } + function onValidate( address target, bool votePasses, @@ -241,7 +366,8 @@ contract DAO { bytes calldata callbackData ) public { require( - msg.sender == address(bench), + msg.sender == address(lightweightBench) || + msg.sender == address(bench), "Only DAO core contracts may call onValidate" ); IOnValidate(target).onValidate( diff --git a/ethereum/contracts/core/LightweightBench.sol b/ethereum/contracts/core/LightweightBench.sol index c6684b1..ee4a3d7 100644 --- a/ethereum/contracts/core/LightweightBench.sol +++ b/ethereum/contracts/core/LightweightBench.sol @@ -20,13 +20,13 @@ struct LWVPoolProps { bool outcome; } -contract LightweightBench { - struct Transfer { - address from; - address to; - uint amount; - } +struct Transfer { + address from; + address to; + uint amount; +} +contract LightweightBench { struct ProposedResult { Transfer[] transfers; uint stakedFor; @@ -121,6 +121,10 @@ contract LightweightBench { string calldata resultHash, Transfer[] calldata transfers ) external { + require( + transfers.length > 0, + "The proposed result contains no transfers" + ); Pool storage pool = validationPools[poolIndex]; require( block.timestamp <= pool.props.endTime, @@ -129,14 +133,16 @@ contract LightweightBench { ProposedResult storage proposedResult = pool.proposedResults[ resultHash ]; - pool.proposedResultHashes.push(resultHash); require( proposedResult.transfers.length == 0, "This result hash has already been proposed" ); + uint resultIndex = pool.proposedResultHashes.length; + pool.proposedResultHashes.push(resultHash); for (uint i = 0; i < transfers.length; i++) { proposedResult.transfers.push(transfers[i]); } + dao.emitLWResultProposed(poolIndex, resultIndex, resultHash); } /// Register a stake for/against a validation pool diff --git a/ethereum/scripts/automatic-staking.js b/ethereum/scripts/automatic-staking.js index 942af6a..0ae7c66 100644 --- a/ethereum/scripts/automatic-staking.js +++ b/ethereum/scripts/automatic-staking.js @@ -39,7 +39,7 @@ const fetchPost = async (postIndex) => { const fetchValidationPool = async (poolIndex) => { const { id, postIndex, sender, stakeCount, fee, duration, endTime, resolved, outcome, - } = await dao.validationPools(poolIndex); + } = await dao.getValidationPool(poolIndex); const pool = { id, postIndex, sender, stakeCount, fee, duration, endTime, resolved, outcome, }; @@ -49,7 +49,7 @@ const fetchValidationPool = async (poolIndex) => { }; const fetchValidationPools = async () => { - const count = await dao.validationPoolCount(); + const count = await dao.getValidationPoolCount(); console.log(`validation pool count: ${count}`); const promises = []; validationPools = []; diff --git a/ethereum/test/Forum.js b/ethereum/test/Forum.js index 0578264..c62d4c7 100644 --- a/ethereum/test/Forum.js +++ b/ethereum/test/Forum.js @@ -113,7 +113,7 @@ describe('Forum', () => { await addPost(account1, 'content-id', []); await addPost(account2, 'second-content-id', [{ weightPPM: 500000, targetPostId: 'content-id' }]); await initiateValidationPool({ postId: 'second-content-id' }); - const pool = await dao.validationPools(0); + const pool = await dao.getValidationPool(0); expect(pool.props.postId).to.equal('second-content-id'); await dao.evaluateOutcome(0); expect(await dao.balanceOf(account1)).to.equal(50); @@ -127,7 +127,7 @@ describe('Forum', () => { expect(await dao.balanceOf(account1)).to.equal(100); await addPost(account2, 'second-content-id', [{ weightPPM: -500000, targetPostId: 'content-id' }]); await initiateValidationPool({ postId: 'second-content-id' }); - const pool = await dao.validationPools(1); + const pool = await dao.getValidationPool(1); expect(pool.props.postId).to.equal('second-content-id'); await time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(1); @@ -147,7 +147,7 @@ describe('Forum', () => { { weightPPM: 1000000, targetPostId: 'second-content-id' }, ]); await initiateValidationPool({ postId: 'third-content-id' }); - const pool = await dao.validationPools(1); + const pool = await dao.getValidationPool(1); expect(pool.props.postId).to.equal('third-content-id'); await time.increase(POOL_DURATION + 1); await dao.evaluateOutcome(1); @@ -183,7 +183,7 @@ describe('Forum', () => { { weightPPM: 100000, targetPostId: 'nonexistent-content-id' }, ]); await initiateValidationPool({ postId: 'second-content-id' }); - const pool = await dao.validationPools(0); + const pool = await dao.getValidationPool(0); expect(pool.props.postId).to.equal('second-content-id'); await dao.evaluateOutcome(0); expect(await dao.balanceOf(account1)).to.equal(10); diff --git a/ethereum/test/LightweightBench.js b/ethereum/test/LightweightBench.js new file mode 100644 index 0000000..f1d6576 --- /dev/null +++ b/ethereum/test/LightweightBench.js @@ -0,0 +1,318 @@ +const { + time, + loadFixture, +} = require('@nomicfoundation/hardhat-toolbox/network-helpers'); +const { expect } = require('chai'); +const { ethers } = require('hardhat'); +const deployDAO = require('./util/deploy-dao'); + +describe('Lightweight Validation Pools', () => { + async function deploy() { + const [account1, account2] = await ethers.getSigners(); + const { dao, forum } = await deployDAO(); + return { + dao, forum, account1, account2, + }; + } + let dao; + let forum; + let account1; + let account2; + const POOL_DURATION = 3600; // 1 hour + const POOL_FEE = 100; + const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); + + const initiateValidationPool = ({ + postId, duration, + quorum, winRatio, bindingPercent, + redistributeLosingStakes, callbackOnValidate, + callbackData, fee, + } = {}) => dao.initiateLWValidationPool( + postId ?? 'content-id', + duration ?? POOL_DURATION, + quorum ?? [1, 3], + winRatio ?? [1, 2], + bindingPercent ?? 100, + redistributeLosingStakes ?? true, + callbackOnValidate ?? false, + callbackData ?? emptyCallbackData, + { value: fee ?? POOL_FEE }, + ); + + beforeEach(async () => { + ({ + dao, forum, account1, account2, + } = await loadFixture(deploy)); + await forum.addPost([{ weightPPM: 1000000, authorAddress: account1 }], 'content-id', []); + const init = () => initiateValidationPool({ fee: POOL_FEE }); + await expect(init()).to.emit(dao, 'LWValidationPoolInitiated').withArgs(0); + expect(await dao.getLWValidationPoolCount()).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 be able to initiate a validation pool without a fee', async () => { + const init = () => initiateValidationPool({ fee: 0 }); + await expect(init()).to.emit(dao, 'LWValidationPoolInitiated'); + }); + + 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, 'LWValidationPoolInitiated').withArgs(1); + expect(await dao.getLWValidationPoolCount()).to.equal(2); + }); + + it('Should be able to fetch pool instance', async () => { + const pool = await dao.getLWValidationPool(0); + expect(pool).to.exist; + expect(pool.params.duration).to.equal(POOL_DURATION); + expect(pool.props.postId).to.equal('content-id'); + expect(pool.props.resolved).to.be.false; + expect(pool.sender).to.equal(account1); + }); + }); + + describe('Propose Result', () => { + it('should not be able to propose an empty result', async () => { + await expect(dao.proposeLWResult(0, 'some-hash', [])).to.be.revertedWith('The proposed result contains no transfers'); + }); + + it('should be able to propose a result', async () => { + await expect(dao.proposeLWResult(0, 'some-hash', [{ from: account1, to: account2, amount: 0 }])).to.emit(dao, 'LWResultProposed').withArgs(0, 0, 'some-hash'); + await expect(dao.proposeLWResult(0, 'some-other-hash', [{ from: account1, to: account2, amount: 0 }])).to.emit(dao, 'LWResultProposed').withArgs(0, 1, 'some-other-hash'); + }); + + it('should not be able to propose the same result twice', async () => { + await expect(dao.proposeLWResult(0, 'some-hash', [{ from: account1, to: account2, amount: 0 }])).to.emit(dao, 'LWResultProposed').withArgs(0, 0, 'some-hash'); + await expect(dao.proposeLWResult(0, 'some-hash', [{ from: account1, to: account2, amount: 0 }])).to.be.revertedWith('This result hash has already been proposed'); + }); + }); + + 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.stakeOnLWValidationPool(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.getValidationPool(1); + 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 + }); + }); + + 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.getValidationPool(0); + expect(pool.props.resolved).to.be.true; + expect(pool.props.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 = () => initiateValidationPool(); + await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); + expect(await dao.getValidationPoolCount()).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.getValidationPoolCount()).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 forum.addPost([{ weightPPM: 1000000, authorAddress: account2 }], 'content-id-2', []); + const init = () => initiateValidationPool({ postId: 'content-id-2' }); + 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/Onboarding.js b/ethereum/test/Onboarding.js index 9a2aa8c..6a450a6 100644 --- a/ethereum/test/Onboarding.js +++ b/ethereum/test/Onboarding.js @@ -76,7 +76,7 @@ describe('Onboarding', () => { expect(post.authors).to.have.length(1); expect(post.authors[0].weightPPM).to.equal(1000000); expect(post.authors[0].authorAddress).to.equal(account1); - const pool = await dao.validationPools(1); + const pool = await dao.getValidationPool(1); expect(pool.props.postId).to.equal('evidence-content-id'); expect(pool.props.fee).to.equal(PRICE * 0.9); expect(pool.sender).to.equal(onboarding.target); @@ -127,7 +127,7 @@ describe('Onboarding', () => { expect(post.authors).to.have.length(1); expect(post.authors[0].weightPPM).to.equal(1000000); expect(post.authors[0].authorAddress).to.equal(account2); - const pool = await dao.validationPools(2); + const pool = await dao.getValidationPool(2); expect(pool.props.postId).to.equal('req-content-id'); expect(pool.props.fee).to.equal(PRICE * 0.1); expect(pool.sender).to.equal(onboarding.target); diff --git a/ethereum/test/Proposals.js b/ethereum/test/Proposals.js index 116c353..5bd50f7 100644 --- a/ethereum/test/Proposals.js +++ b/ethereum/test/Proposals.js @@ -224,7 +224,7 @@ describe('Proposal', () => { }); afterEach(async () => { - const pool = await dao.validationPools(3); + const pool = await dao.getValidationPool(3); expect(pool.props.resolved).to.be.true; }); @@ -310,7 +310,7 @@ describe('Proposal', () => { }); afterEach(async () => { - const pool = await dao.validationPools(4); + const pool = await dao.getValidationPool(4); expect(pool.props.resolved).to.be.true; }); diff --git a/ethereum/test/ValidationPools.js b/ethereum/test/ValidationPools.js index 35d4eba..3eac492 100644 --- a/ethereum/test/ValidationPools.js +++ b/ethereum/test/ValidationPools.js @@ -46,7 +46,7 @@ describe('Validation Pools', () => { await forum.addPost([{ weightPPM: 1000000, authorAddress: 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.getValidationPoolCount()).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); @@ -86,11 +86,11 @@ describe('Validation Pools', () => { 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); + expect(await dao.getValidationPoolCount()).to.equal(2); }); it('Should be able to fetch pool instance', async () => { - const pool = await dao.validationPools(0); + const pool = await dao.getValidationPool(0); expect(pool).to.exist; expect(pool.params.duration).to.equal(POOL_DURATION); expect(pool.props.postId).to.equal('content-id'); @@ -133,7 +133,7 @@ describe('Validation Pools', () => { 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); + const pool = await dao.getValidationPool(1); expect(pool.props.outcome).to.be.false; }); @@ -170,7 +170,7 @@ describe('Validation Pools', () => { 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); + const pool = await dao.getValidationPool(0); expect(pool.props.resolved).to.be.true; expect(pool.props.outcome).to.be.true; }); @@ -184,7 +184,7 @@ describe('Validation Pools', () => { it('should be able to evaluate outcome of second validation pool', async () => { const init = () => initiateValidationPool(); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); - expect(await dao.validationPoolCount()).to.equal(2); + expect(await dao.getValidationPoolCount()).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); @@ -198,7 +198,7 @@ describe('Validation Pools', () => { const init = () => initiateValidationPool({ quorum: [1, 1] }); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); - expect(await dao.validationPoolCount()).to.equal(2); + expect(await dao.getValidationPoolCount()).to.equal(2); time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(1, false, false); }); diff --git a/ethereum/test/Work1.js b/ethereum/test/Work1.js index 3ffcac7..5959f03 100644 --- a/ethereum/test/Work1.js +++ b/ethereum/test/Work1.js @@ -229,7 +229,7 @@ describe('Work1', () => { expect(post.authors).to.have.length(1); expect(post.authors[0].weightPPM).to.equal(1000000); expect(post.authors[0].authorAddress).to.equal(account1); - const pool = await dao.validationPools(1); + const pool = await dao.getValidationPool(1); expect(pool.props.fee).to.equal(WORK1_PRICE); expect(pool.sender).to.equal(work1.target); expect(pool.props.postId).to.equal('evidence-content-id'); diff --git a/ethereum/test/util/deploy-dao.js b/ethereum/test/util/deploy-dao.js index 53c6d48..9fc197b 100644 --- a/ethereum/test/util/deploy-dao.js +++ b/ethereum/test/util/deploy-dao.js @@ -3,14 +3,17 @@ const { ethers } = require('hardhat'); const deployDAO = async () => { const Reputation = await ethers.getContractFactory('Reputation'); const Bench = await ethers.getContractFactory('Bench'); + const LightweightBench = await ethers.getContractFactory('LightweightBench'); const DAO = await ethers.getContractFactory('DAO'); const GlobalForum = await ethers.getContractFactory('GlobalForum'); const forum = await GlobalForum.deploy(); const reputation = await Reputation.deploy(); const bench = await Bench.deploy(); + const lightweightBench = await LightweightBench.deploy(); const dao = await DAO.deploy( reputation.target, bench.target, + lightweightBench.target, forum.target, ); return {