diff --git a/ethereum/contracts/Work1.sol b/ethereum/contracts/Work1.sol index 8bcd80a..7c4f628 100644 --- a/ethereum/contracts/Work1.sol +++ b/ethereum/contracts/Work1.sol @@ -84,7 +84,7 @@ contract Work1 is IAcceptAvailability { } /// Select a worker randomly from among the available workers, weighted by amount staked - function randomWeightedSelection() internal view returns (uint) { + function randomWeightedSelection() internal view returns (uint stakeIndex) { uint totalStakes; for (uint i = 0; i < stakeCount; i++) { if (stakes[i].assigned) continue; @@ -98,9 +98,11 @@ contract Work1 is IAcceptAvailability { if (stakes[i].assigned) continue; if (block.timestamp > stakes[i].endTime) continue; acc += stakes[i].amount; - if (acc > select) return i; + if (acc > select) { + stakeIndex = i; + break; + } } - revert("Failed to assign worker"); // Should never get here } /// Assign a random available worker @@ -124,9 +126,15 @@ contract Work1 is IAcceptAvailability { /// Accept work evidence from worker function submitWorkEvidence(uint requestIndex) external { WorkRequest storage request = requests[requestIndex]; - require(request.status == WorkStatus.Requested); + require( + request.status == WorkStatus.Requested, + "Status must be Requested" + ); AvailabilityStake storage stake = stakes[request.stakeIndex]; - require(stake.worker == msg.sender); + require( + stake.worker == msg.sender, + "Worker can only submit evidence for work they are assigned" + ); request.status = WorkStatus.EvidenceSubmitted; emit WorkEvidenceSubmitted(requestIndex); } @@ -134,7 +142,10 @@ contract Work1 is IAcceptAvailability { /// Accept work approval/disapproval from customer function submitWorkApproval(uint requestIndex, bool approval) external { WorkRequest storage request = requests[requestIndex]; - require(request.status == WorkStatus.EvidenceSubmitted); + require( + request.status == WorkStatus.EvidenceSubmitted, + "Status must be EvidenceSubmitted" + ); AvailabilityStake storage stake = stakes[request.stakeIndex]; request.status = WorkStatus.ApprovalSubmitted; request.approval = approval; diff --git a/ethereum/test/Work1.js b/ethereum/test/Work1.js index 272d2b4..aba7aed 100644 --- a/ethereum/test/Work1.js +++ b/ethereum/test/Work1.js @@ -9,7 +9,6 @@ const { ethers } = require('hardhat'); describe('Work1', () => { const WORK1_PRICE = 100; const STAKE_DURATION = 60; - const STAKE_AMOUNT = 50; async function deploy() { // Contracts are deployed using the first signer/account by default const [account1, account2] = await ethers.getSigners(); @@ -37,73 +36,173 @@ describe('Work1', () => { expect(await work1.stakeCount()).to.equal(0); }); - it('Should be able to receive availability stake', async () => { - const { dao, work1, account1 } = await loadFixture(deploy); - await dao.stakeAvailability(work1.target, STAKE_AMOUNT, STAKE_DURATION); - expect(await dao.balanceOf(account1)).to.equal(STAKE_AMOUNT); - expect(await dao.balanceOf(work1.target)).to.equal(STAKE_AMOUNT); - expect(await work1.stakeCount()).to.equal(1); - const stake = await work1.stakes(0); - expect(stake.worker).to.equal(account1); - expect(stake.amount).to.equal(STAKE_AMOUNT); - expect(stake.endTime).to.equal(await time.latest() + STAKE_DURATION); + describe('Stake availability', () => { + let dao; + let work1; + let account1; + let account2; + + beforeEach(async () => { + const setup = await loadFixture(deploy); + dao = setup.dao; + work1 = setup.work1; + account1 = setup.account1; + account2 = setup.account2; + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION); + }); + + it('Should be able to stake availability', async () => { + expect(await dao.balanceOf(account1)).to.equal(50); + expect(await dao.balanceOf(work1.target)).to.equal(50); + expect(await work1.stakeCount()).to.equal(1); + const stake = await work1.stakes(0); + expect(stake.worker).to.equal(account1); + expect(stake.amount).to.equal(50); + expect(stake.endTime).to.equal(await time.latest() + STAKE_DURATION); + }); + + it('should be able to reclaim staked availability after duration elapses', async () => { + expect(await dao.balanceOf(account1)).to.equal(50); + time.increase(STAKE_DURATION + 1); + await work1.reclaimAvailability(0); + expect(await dao.balanceOf(account1)).to.equal(100); + }); + + it('should not be able to reclaim staked availability before duration elapses', async () => { + await expect(work1.reclaimAvailability(0)).to.be.revertedWith('Stake duration has not yet elapsed'); + }); + + it('should not be able to reclaim availability staked by another account', async () => { + time.increase(STAKE_DURATION + 1); + await expect(work1.connect(account2).reclaimAvailability(0)).to.be.revertedWith('Worker can only reclaim their own availability stake'); + }); + + it('should be able to extend the duration of an availability stake before it expires', async () => { + await time.increase(STAKE_DURATION / 2); + await work1.extendAvailability(0, STAKE_DURATION); + }); + + it('should be able to extend the duration of an availability stake after it expires', async () => { + await time.increase(STAKE_DURATION * 2); + await work1.extendAvailability(0, STAKE_DURATION); + }); + + it('should not be able to extend the duration of another worker\'s availability stake', async () => { + await time.increase(STAKE_DURATION * 2); + await expect(work1.connect(account2).extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Worker can only extend their own availability stake'); + }); }); - it('should be able to request work and assign to a worker', async () => { - const { - dao, work1, account1, account2, - } = await loadFixture(deploy); - await dao.stakeAvailability(work1.target, STAKE_AMOUNT, STAKE_DURATION, { from: account1 }); - const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); - await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(account1, 0); - expect(await work1.requestCount()).to.equal(1); - const request = await work1.requests(0); - expect(request.customer).to.equal(account2); + describe('Request and assign work', () => { + it('should be able to request work and assign to a worker', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(account1, 0); + expect(await work1.requestCount()).to.equal(1); + const request = await work1.requests(0); + expect(request.customer).to.equal(account2); + }); + + it('should not be able to request work if there are no availability stakes', async () => { + const { + work1, account2, + } = await loadFixture(deploy); + const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await expect(requestWork()).to.be.revertedWith('No available worker stakes'); + }); + + it('should not assign work to an expired availability stake', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await time.increase(61); + await expect(requestWork()).to.be.revertedWith('No available worker stakes'); + }); + + it('should not assign work to the same availability stake twice', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(account1, 0); + await expect(requestWork()).to.be.revertedWith('No available worker stakes'); + }); }); - it('should not be able to request work if there are no availability stakes', async () => { - const { - work1, account2, - } = await loadFixture(deploy); - const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); - await expect(requestWork()).to.be.revertedWith('No available worker stakes'); - }); + describe('Work evidence and approval/disapproval', () => { + it('should be able to submit work evidence', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + await work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await expect(work1.submitWorkEvidence(0)).to.emit(work1, 'WorkEvidenceSubmitted').withArgs(0); + }); - it('should not assign work to an expired availability stake', async () => { - const { - dao, work1, account1, account2, - } = await loadFixture(deploy); - await dao.stakeAvailability(work1.target, STAKE_AMOUNT, STAKE_DURATION, { from: account1 }); - const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); - await time.increase(61); - await expect(requestWork()).to.be.revertedWith('No available worker stakes'); - }); - it('should not assign work to the same availability stake twice', async () => { - const { - dao, work1, account1, account2, - } = await loadFixture(deploy); - await dao.stakeAvailability(work1.target, STAKE_AMOUNT, STAKE_DURATION, { from: account1 }); - const requestWork = () => work1.connect(account2).requestWork({ value: WORK1_PRICE }); - await expect(requestWork()).to.emit(work1, 'WorkAssigned').withArgs(account1, 0); - await expect(requestWork()).to.be.revertedWith('No available worker stakes'); - }); + it('should not be able to submit work evidence twice', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + await work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await expect(work1.submitWorkEvidence(0)).to.emit(work1, 'WorkEvidenceSubmitted').withArgs(0); + await expect(work1.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested'); + }); - it('should be able to submit work evidence', async () => { - const { - dao, work1, account1, account2, - } = await loadFixture(deploy); - await dao.stakeAvailability(work1.target, STAKE_AMOUNT, STAKE_DURATION, { from: account1 }); - await work1.connect(account2).requestWork({ value: WORK1_PRICE }); - await expect(work1.submitWorkEvidence(0)).to.emit(work1, 'WorkEvidenceSubmitted').withArgs(0); - }); + it('should not be able to submit work evidence for a different worker', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + await work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await expect(work1.connect(account2).submitWorkEvidence(0)).to.be.revertedWith('Worker can only submit evidence for work they are assigned'); + }); - it('should be able to submit work approval', async () => { - const { - dao, work1, account1, account2, - } = await loadFixture(deploy); - await dao.stakeAvailability(work1.target, STAKE_AMOUNT, STAKE_DURATION, { from: account1 }); - await work1.connect(account2).requestWork({ value: WORK1_PRICE }); - await work1.submitWorkEvidence(0); - await expect(work1.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); + it('should be able to submit work approval', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + await work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await work1.submitWorkEvidence(0); + await expect(work1.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); + }); + + it('should not be able to submit work approval/disapproval twice', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + await work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await work1.submitWorkEvidence(0); + await expect(work1.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); + await expect(work1.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted'); + }); + + it('should not be able to submit work evidence after work approval', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + await work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await work1.submitWorkEvidence(0); + await expect(work1.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); + await expect(work1.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested'); + }); + + it('should not be able to submit work approval/disapproval before work evidence', async () => { + const { + dao, work1, account1, account2, + } = await loadFixture(deploy); + await dao.stakeAvailability(work1.target, 50, STAKE_DURATION, { from: account1 }); + await work1.connect(account2).requestWork({ value: WORK1_PRICE }); + await expect(work1.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted'); + }); }); });