const { time, loadFixture, } = require('@nomicfoundation/hardhat-toolbox/network-helpers'); const { expect } = require('chai'); const { ethers } = require('hardhat'); describe('Onboarding', () => { const PRICE = 100; const STAKE_DURATION = 60; async function deploy() { // Contracts are deployed using the first signer/account by default const [account1, account2] = await ethers.getSigners(); const DAO = await ethers.getContractFactory('DAO'); const dao = await DAO.deploy(); const Onboarding = await ethers.getContractFactory('Onboarding'); const onboarding = await Onboarding.deploy(dao.target, PRICE); await dao.addPost(account1); const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); await dao.initiateValidationPool(0, 60, false, callbackData, { value: 100 }); await time.increase(61); await dao.evaluateOutcome(0); expect(await dao.balanceOf(account1)).to.equal(100); return { dao, onboarding, account1, account2, }; } it('Should deploy', async () => { const { dao, onboarding, account1 } = await loadFixture(deploy); expect(dao).to.exist; expect(onboarding).to.exist; expect(await dao.memberCount()).to.equal(1); expect(await dao.balanceOf(account1)).to.equal(100); expect(await dao.totalSupply()).to.equal(100); expect(await onboarding.stakeCount()).to.equal(0); }); describe('Stake availability', () => { let dao; let onboarding; let account1; let account2; beforeEach(async () => { ({ dao, onboarding, account1, account2, } = await loadFixture(deploy)); await expect(dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION)).to.emit(onboarding, 'AvailabilityStaked').withArgs(0); }); it('Should be able to stake availability', async () => { expect(await dao.balanceOf(account1)).to.equal(50); expect(await dao.balanceOf(onboarding.target)).to.equal(50); expect(await onboarding.stakeCount()).to.equal(1); const stake = await onboarding.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 not be able to stake availability without reputation value', async () => { await expect(dao.stakeAvailability(onboarding.target, 0, STAKE_DURATION)).to.be.revertedWith('No stake provided'); }); 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 expect(onboarding.reclaimAvailability(0)).to.emit(onboarding, 'AvailabilityStaked').withArgs(0); expect(await dao.balanceOf(account1)).to.equal(100); }); it('should not be able to reclaim staked availability twice', async () => { expect(await dao.balanceOf(account1)).to.equal(50); time.increase(STAKE_DURATION + 1); await onboarding.reclaimAvailability(0); await expect(onboarding.reclaimAvailability(0)).to.be.revertedWith('Stake has already been reclaimed'); }); it('should not be able to reclaim staked availability before duration elapses', async () => { await expect(onboarding.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(onboarding.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 expect(onboarding.extendAvailability(0, STAKE_DURATION)).to.emit(onboarding, 'AvailabilityStaked').withArgs(0); }); it('should be able to extend the duration of an availability stake after it expires', async () => { await time.increase(STAKE_DURATION * 2); await onboarding.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(onboarding.connect(account2).extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Worker can only extend their own availability stake'); }); it('should not be able to extend a stake that has been reclaimed', async () => { await time.increase(STAKE_DURATION * 2); await onboarding.reclaimAvailability(0); await expect(onboarding.extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Stake has already been reclaimed'); }); it('extending a stake before expiration should increase the end time by the given duration', async () => { await time.increase(STAKE_DURATION / 2); await onboarding.extendAvailability(0, STAKE_DURATION * 2); const expectedEndTime = await time.latest() + 2.5 * STAKE_DURATION; const stake = await onboarding.stakes(0); expect(stake.endTime).to.be.within(expectedEndTime - 1, expectedEndTime); }); it('extending a stake after expiration should restart the stake for the given duration', async () => { await time.increase(STAKE_DURATION * 2); await onboarding.extendAvailability(0, STAKE_DURATION * 2); const expectedEndTime = await time.latest() + STAKE_DURATION * 2; const stake = await onboarding.stakes(0); expect(stake.endTime).to.be.within(expectedEndTime - 1, expectedEndTime); }); }); describe('Request and assign work', () => { it('should be able to request work and assign to a worker', async () => { const { dao, onboarding, account1, account2, } = await loadFixture(deploy); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE }); await expect(requestWork()).to.emit(onboarding, 'WorkAssigned').withArgs(account1, 0); expect(await onboarding.requestCount()).to.equal(1); const request = await onboarding.requests(0); expect(request.customer).to.equal(account2); }); it('should not be able to reclaim stake after work is assigned', async () => { const { dao, onboarding, account1, account2, } = await loadFixture(deploy); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE }); await expect(requestWork()).to.emit(onboarding, 'WorkAssigned').withArgs(account1, 0); await time.increase(STAKE_DURATION + 1); await expect(onboarding.reclaimAvailability(0)).to.be.revertedWith('Stake has already been assigned work'); }); it('should not be able to request work if there are no availability stakes', async () => { const { onboarding, account2, } = await loadFixture(deploy); const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE }); await expect(requestWork()).to.be.revertedWith('No available worker stakes'); }); it('should not be able to request work if fee is insufficient', async () => { const { onboarding, account2, } = await loadFixture(deploy); const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE / 2 }); await expect(requestWork()).to.be.revertedWith('Insufficient fee'); }); it('should not assign work to an expired availability stake', async () => { const { dao, onboarding, account2, } = await loadFixture(deploy); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE }); await time.increase(STAKE_DURATION + 1); await expect(requestWork()).to.be.revertedWith('No available worker stakes'); }); it('should not assign work to the same availability stake twice', async () => { const { dao, onboarding, account1, account2, } = await loadFixture(deploy); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); const requestWork = () => onboarding.connect(account2).requestWork({ value: PRICE }); await expect(requestWork()).to.emit(onboarding, 'WorkAssigned').withArgs(account1, 0); await expect(requestWork()).to.be.revertedWith('No available worker stakes'); }); it('should not be able to extend a stake that has been assigned work', async () => { const { dao, onboarding, account2, } = await loadFixture(deploy); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); await onboarding.connect(account2).requestWork({ value: PRICE }); await time.increase(STAKE_DURATION * 2); await expect(onboarding.extendAvailability(0, STAKE_DURATION)).to.be.revertedWith('Stake has already been assigned work'); }); }); describe('Work evidence and approval/disapproval', () => { let dao; let onboarding; let account1; let account2; beforeEach(async () => { ({ dao, onboarding, account1, account2, } = await loadFixture(deploy)); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); }); it('should be able to submit work evidence', async () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await expect(onboarding.submitWorkEvidence(0)).to.emit(onboarding, 'WorkEvidenceSubmitted').withArgs(0); }); it('should not be able to submit work evidence twice', async () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await expect(onboarding.submitWorkEvidence(0)).to.emit(onboarding, 'WorkEvidenceSubmitted').withArgs(0); await expect(onboarding.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested'); }); it('should not be able to submit work evidence for a different worker', async () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await expect(onboarding.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 () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await onboarding.submitWorkEvidence(0); await expect(onboarding.submitWorkApproval(0, true)) .to.emit(dao, 'ValidationPoolInitiated').withArgs(1) .to.emit(onboarding, 'WorkApprovalSubmitted').withArgs(0, true); const post = await dao.posts(1); expect(post.author).to.equal(account1); expect(post.sender).to.equal(onboarding.target); const pool = await dao.validationPools(1); expect(pool.postIndex).to.equal(1); expect(pool.fee).to.equal(PRICE * 0.9); expect(pool.sender).to.equal(onboarding.target); }); it('should be able to submit work disapproval', async () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await onboarding.submitWorkEvidence(0); await expect(onboarding.submitWorkApproval(0, false)) .to.emit(dao, 'ValidationPoolInitiated').withArgs(1) .to.emit(onboarding, 'WorkApprovalSubmitted').withArgs(0, false); }); it('should not be able to submit work approval/disapproval twice', async () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await onboarding.submitWorkEvidence(0); await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); await expect(onboarding.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted'); }); it('should not be able to submit work evidence after work approval', async () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await onboarding.submitWorkEvidence(0); await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); await expect(onboarding.submitWorkEvidence(0)).to.be.revertedWith('Status must be Requested'); }); it('should not be able to submit work approval/disapproval before work evidence', async () => { await onboarding.connect(account2).requestWork({ value: PRICE }); await expect(onboarding.submitWorkApproval(0, true)).to.be.revertedWith('Status must be EvidenceSubmitted'); }); }); describe('Onboarding followup', () => { it('resolving the first validation pool should trigger a second pool', async () => { const { dao, onboarding, account2, } = await loadFixture(deploy); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); await onboarding.connect(account2).requestWork({ value: PRICE }); await onboarding.submitWorkEvidence(0); await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); await time.increase(86401); await expect(dao.evaluateOutcome(1)).to.emit(dao, 'ValidationPoolInitiated').withArgs(2); expect(await dao.postCount()).to.equal(3); const post = await dao.posts(2); expect(post.author).to.equal(account2); expect(post.sender).to.equal(onboarding.target); const pool = await dao.validationPools(2); expect(pool.postIndex).to.equal(2); expect(pool.fee).to.equal(PRICE * 0.1); expect(pool.sender).to.equal(onboarding.target); expect(pool.fee); }); it('if the first validation pool is rejected it should not trigger a second pool', async () => { const { dao, onboarding, account2, } = await loadFixture(deploy); await dao.stakeAvailability(onboarding.target, 50, STAKE_DURATION); await onboarding.connect(account2).requestWork({ value: PRICE }); await onboarding.submitWorkEvidence(0); await expect(onboarding.submitWorkApproval(0, true)).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); await dao.stake(1, 50, false); await time.increase(86401); await expect(dao.evaluateOutcome(1)).not.to.emit(dao, 'ValidationPoolInitiated'); expect(await dao.postCount()).to.equal(2); }); }); });