diff --git a/ethereum/contracts/DAO.sol b/ethereum/contracts/DAO.sol index f67f5e0..5cadde1 100644 --- a/ethereum/contracts/DAO.sol +++ b/ethereum/contracts/DAO.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.24; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./IAcceptAvailability.sol"; +import "hardhat/console.sol"; + struct Stake { bool inFavor; uint256 amount; @@ -46,7 +48,7 @@ contract DAO is ERC20("Reputation", "REP") { // TODO: Add forum parameters event ValidationPoolInitiated(uint poolIndex); - event ValidationPoolResolved(bool votePasses); + event ValidationPoolResolved(uint poolIndex, bool votePasses); /// Accept fee to initiate a validation pool /// TODO: Rather than accept author as a parameter, accept a reference to a forum post @@ -69,7 +71,7 @@ contract DAO is ERC20("Reputation", "REP") { // Implementing this with adjustable parameters will require more advanced fixed point math. // TODO: Make minting ratio an adjustable parameter // TODO: Make stakeForAuthor an adjustable parameter - _mint(address(this), msg.value); + _mint(author, msg.value); // TODO: We need a way to exclude this pending reputation from the total supply when computing fee distribution _stake(pool, author, msg.value / 2, true); _stake(pool, author, msg.value / 2, false); @@ -84,6 +86,7 @@ contract DAO is ERC20("Reputation", "REP") { bool inFavor ) internal { require(block.timestamp <= pool.endTime, "Pool end time has passed"); + _transfer(sender, address(this), amount); Stake storage s = pool.stakes[pool.stakeCount++]; s.sender = sender; s.inFavor = inFavor; @@ -126,18 +129,25 @@ contract DAO is ERC20("Reputation", "REP") { isMember[pool.author] = true; } pool.resolved = true; - emit ValidationPoolResolved(votePasses); - // Value of losing stakes should be distributed among winners, in proportion to their stakes + emit ValidationPoolResolved(poolIndex, votePasses); + // Value of losing stakes should be di stributed among winners, in proportion to their stakes uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst; uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor; + uint256 totalRewards; for (uint i = 0; i < pool.stakeCount; i++) { s = pool.stakes[i]; if (votePasses == s.inFavor) { uint256 reward = (amountFromLosers * s.amount) / amountFromWinners; _transfer(address(this), s.sender, s.amount + reward); + totalRewards += reward; } } + // Due to rounding, there may be some reward left over. Include this as a reward to the author. + uint256 remainder = amountFromLosers - totalRewards; + if (remainder > 0) { + _transfer(address(this), pool.author, remainder); + } // Distribute fee proportionatly among all reputation holders for (uint i = 0; i < memberCount; i++) { address member = members[i]; diff --git a/ethereum/contracts/Work1.sol b/ethereum/contracts/Work1.sol index 8dfd5d3..2a3d8f1 100644 --- a/ethereum/contracts/Work1.sol +++ b/ethereum/contracts/Work1.sol @@ -7,8 +7,9 @@ import "./IAcceptAvailability.sol"; struct AvailabilityStake { address worker; uint256 amount; - uint duration; + uint endTime; bool assigned; + bool reclaimed; } enum WorkStatus { @@ -54,7 +55,31 @@ contract Work1 is IAcceptAvailability { AvailabilityStake storage stake = stakes[stakeCount++]; stake.worker = sender; stake.amount = amount; - stake.duration = duration; + stake.endTime = block.timestamp + duration; + } + + function extendAvailability(uint stakeIndex, uint duration) external { + AvailabilityStake storage stake = stakes[stakeIndex]; + require( + msg.sender == stake.worker, + "Worker can only extend their own availability stake" + ); + stake.endTime = block.timestamp + duration; + } + + function reclaimAvailability(uint stakeIndex) external { + AvailabilityStake storage stake = stakes[stakeIndex]; + require( + msg.sender == stake.worker, + "Worker can only reclaim their own availability stake" + ); + require( + block.timestamp > stake.endTime, + "Stake duration has not yet elapsed" + ); + require(!stake.reclaimed, "Stake has already been reclaimed"); + stake.reclaimed = true; + dao.transfer(msg.sender, stake.amount); } /// Select a worker randomly from among the available workers, weighted by amount staked @@ -62,12 +87,14 @@ contract Work1 is IAcceptAvailability { uint totalStakes; for (uint i = 0; i < stakeCount; i++) { if (stakes[i].assigned) continue; + if (block.timestamp > stakes[i].endTime) continue; totalStakes += stakes[i].amount; } uint select = block.prevrandao % totalStakes; uint acc; for (uint i = 0; i < stakeCount; i++) { if (stakes[i].assigned) continue; + if (block.timestamp > stakes[i].endTime) continue; acc += stakes[i].amount; if (acc > select) return i; } diff --git a/ethereum/test/DAO.js b/ethereum/test/DAO.js index 4fb8b3b..3888cb2 100644 --- a/ethereum/test/DAO.js +++ b/ethereum/test/DAO.js @@ -22,71 +22,111 @@ describe('DAO', () => { it('Should deploy', async () => { const { dao } = await loadFixture(deploy); expect(dao).to.exist; - expect(await dao.totalValue()).to.equal(0); + expect(await dao.totalSupply()).to.equal(0); }); describe('Validation Pool', () => { let dao; let account1; const POOL_DURATION = 3600; // 1 hour - const fee = 100; + const POOL_FEE = 100; beforeEach(async () => { const setup = await loadFixture(deploy); dao = setup.dao; account1 = setup.account1; - const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: fee }); + const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { 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(fee); + expect(await dao.totalSupply()).to.equal(POOL_FEE); }); - it('should not be able to initiate a validation pool without a fee', async () => { - const setup = await loadFixture(deploy); - const init = () => setup.dao.initiateValidationPool(setup.account1, POOL_DURATION); - await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool'); + 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(setup.account1, POOL_DURATION); + await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool'); + }); + + it('should be able to initiate a second validation pool', async () => { + const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { 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); + }); }); - it('should be able to initiate a second validation pool', async () => { - const init = () => dao.initiateValidationPool(account1, POOL_DURATION, { value: fee }); - await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); - expect(await dao.validationPoolCount()).to.equal(2); + 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 () => { + 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); + }); + + 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(account1, POOL_DURATION, { 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 be able to fetch pool instance', async () => { - const pool = await dao.validationPools(0); - expect(pool).to.exist; - expect(pool.duration).to.equal(POOL_DURATION); - }); + 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 dao.initiateValidationPool(account1, POOL_DURATION, { value: POOL_FEE }); + expect(await dao.balanceOf(dao.target)).to.equal(100); + }); - 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 stake before validation pool has elapsed', async () => { + 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); + 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 be able to evaluate outcome after duration has elapsed', async () => { - time.increase(POOL_DURATION + 1); - await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(true); - expect(await dao.memberCount()).to.equal(1); - expect(await dao.balanceOf(account1)).to.equal(100); - }); + 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 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(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(account1, POOL_DURATION, { value: 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(true); - expect(await dao.balanceOf(account1)).to.equal(100); - await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolResolved').withArgs(true); - expect(await dao.balanceOf(account1)).to.equal(200); + 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); + }); }); }); }); diff --git a/ethereum/test/Work1.js b/ethereum/test/Work1.js index 78493bf..dc57581 100644 --- a/ethereum/test/Work1.js +++ b/ethereum/test/Work1.js @@ -1,4 +1,5 @@ const { + time, loadFixture, } = require('@nomicfoundation/hardhat-toolbox/network-helpers'); const { expect } = require('chai'); @@ -42,6 +43,6 @@ describe('Work1', () => { const stake = await work1.stakes(0); expect(stake.worker).to.equal(account1); expect(stake.amount).to.equal(50); - expect(stake.duration).to.equal(60); + expect(stake.endTime).to.equal(await time.latest() + 60); }); });