From a1e28d254ed2624014b76e7cdb1097dcf632e5c3 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Mon, 18 Mar 2024 10:53:39 -0500 Subject: [PATCH] refactor abstract contract WorkContract --- ethereum/contracts/Onboarding.sol | 176 +++------------------------ ethereum/contracts/Work1.sol | 174 +-------------------------- ethereum/contracts/WorkContract.sol | 179 ++++++++++++++++++++++++++++ ethereum/test/DAO.js | 35 +++++- ethereum/test/Work1.js | 4 +- 5 files changed, 234 insertions(+), 334 deletions(-) create mode 100644 ethereum/contracts/WorkContract.sol diff --git a/ethereum/contracts/Onboarding.sol b/ethereum/contracts/Onboarding.sol index 15aa38c..a3ec294 100644 --- a/ethereum/contracts/Onboarding.sol +++ b/ethereum/contracts/Onboarding.sol @@ -2,161 +2,17 @@ pragma solidity ^0.8.24; import "./DAO.sol"; -import "./IAcceptAvailability.sol"; +import "./WorkContract.sol"; import "./IOnValidate.sol"; -struct AvailabilityStake { - address worker; - uint256 amount; - uint endTime; - bool assigned; - bool reclaimed; -} - -enum WorkStatus { - Requested, - EvidenceSubmitted, - ApprovalSubmitted, - Complete -} - -struct WorkRequest { - address customer; - uint256 fee; - WorkStatus status; - uint stakeIndex; - bool approval; - uint reviewPoolIndex; - uint onboardPoolIndex; -} - -contract Onboarding is IAcceptAvailability, IOnValidate { - DAO immutable dao; - uint public immutable price; - mapping(uint => AvailabilityStake) public stakes; - uint public stakeCount; - mapping(uint => WorkRequest) public requests; - uint public requestCount; - - // TODO: Make parameters configurable - uint constant POOL_DURATION = 1 days; - - event AvailabilityStaked(uint stakeIndex); - event WorkAssigned(address worker, uint requestIndex); - event WorkEvidenceSubmitted(uint requestIndex); - event WorkApprovalSubmitted(uint requestIndex, bool approval); - - constructor(DAO dao_, uint price_) { - dao = dao_; - price = price_; - } - - /// Accept availability stakes as reputation token transfer - function acceptAvailability( - address sender, - uint256 amount, - uint duration - ) external { - require(amount > 0, "No stake provided"); - uint stakeIndex = stakeCount++; - AvailabilityStake storage stake = stakes[stakeIndex]; - stake.worker = sender; - stake.amount = amount; - stake.endTime = block.timestamp + duration; - emit AvailabilityStaked(stakeIndex); - } - - 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" - ); - require(!stake.reclaimed, "Stake has already been reclaimed"); - require(!stake.assigned, "Stake has already been assigned work"); - if (block.timestamp > stake.endTime) { - stake.endTime = block.timestamp + duration; - } else { - stake.endTime = stake.endTime + duration; - } - emit AvailabilityStaked(stakeIndex); - } - - 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"); - require(!stake.assigned, "Stake has already been assigned work"); - stake.reclaimed = true; - dao.transfer(msg.sender, stake.amount); - emit AvailabilityStaked(stakeIndex); - } - - /// Select a worker randomly from among the available workers, weighted by amount staked - function randomWeightedSelection() internal view returns (uint stakeIndex) { - 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; - } - require(totalStakes > 0, "No available worker stakes"); - 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) { - stakeIndex = i; - break; - } - } - } - - /// Assign a random available worker - function assignWork(uint requestIndex) internal returns (uint stakeIndex) { - stakeIndex = randomWeightedSelection(); - AvailabilityStake storage stake = stakes[stakeIndex]; - stake.assigned = true; - emit WorkAssigned(stake.worker, requestIndex); - } - - /// Accept work request with fee - function requestWork() external payable { - require(msg.value >= price, "Insufficient fee"); - uint requestIndex = requestCount++; - WorkRequest storage request = requests[requestIndex]; - request.customer = msg.sender; - request.fee = msg.value; - request.stakeIndex = assignWork(requestIndex); - } - - /// Accept work evidence from worker - function submitWorkEvidence(uint requestIndex) external { - WorkRequest storage request = requests[requestIndex]; - require( - request.status == WorkStatus.Requested, - "Status must be Requested" - ); - AvailabilityStake storage stake = stakes[request.stakeIndex]; - require( - stake.worker == msg.sender, - "Worker can only submit evidence for work they are assigned" - ); - request.status = WorkStatus.EvidenceSubmitted; - emit WorkEvidenceSubmitted(requestIndex); - } +contract Onboarding is WorkContract, IOnValidate { + constructor(DAO dao_, uint price_) WorkContract(dao_, price_) {} /// Accept work approval/disapproval from customer - function submitWorkApproval(uint requestIndex, bool approval) external { + function submitWorkApproval( + uint requestIndex, + bool approval + ) external override { WorkRequest storage request = requests[requestIndex]; require( request.status == WorkStatus.EvidenceSubmitted, @@ -169,9 +25,12 @@ contract Onboarding is IAcceptAvailability, IOnValidate { uint postIndex = dao.addPost(stake.worker); emit WorkApprovalSubmitted(requestIndex, approval); // Initiate validation pool - request.reviewPoolIndex = dao.initiateValidationPool{ - value: request.fee - request.fee / 10 - }(postIndex, POOL_DURATION, true, abi.encode(requestIndex)); + dao.initiateValidationPool{value: request.fee - request.fee / 10}( + postIndex, + POOL_DURATION, + true, + abi.encode(requestIndex) + ); } /// Callback to be executed when review pool completes @@ -184,8 +43,11 @@ contract Onboarding is IAcceptAvailability, IOnValidate { uint requestIndex = abi.decode(callbackData, (uint)); WorkRequest storage request = requests[requestIndex]; uint postIndex = dao.addPost(request.customer); - request.onboardPoolIndex = dao.initiateValidationPool{ - value: request.fee / 10 - }(postIndex, POOL_DURATION, false, ""); + dao.initiateValidationPool{value: request.fee / 10}( + postIndex, + POOL_DURATION, + false, + "" + ); } } diff --git a/ethereum/contracts/Work1.sol b/ethereum/contracts/Work1.sol index 66ce392..5c54c96 100644 --- a/ethereum/contracts/Work1.sol +++ b/ethereum/contracts/Work1.sol @@ -2,176 +2,8 @@ pragma solidity ^0.8.24; import "./DAO.sol"; -import "./IAcceptAvailability.sol"; +import "./WorkContract.sol"; -struct AvailabilityStake { - address worker; - uint256 amount; - uint endTime; - bool assigned; - bool reclaimed; -} - -enum WorkStatus { - Requested, - EvidenceSubmitted, - ApprovalSubmitted, - Complete -} - -struct WorkRequest { - address customer; - uint256 fee; - WorkStatus status; - uint stakeIndex; - bool approval; - uint poolIndex; -} - -contract Work1 is IAcceptAvailability { - DAO immutable dao; - uint public immutable price; - mapping(uint => AvailabilityStake) public stakes; - uint public stakeCount; - mapping(uint => WorkRequest) public requests; - uint public requestCount; - - // TODO: Make parameters configurable - uint constant POOL_DURATION = 1 days; - - event AvailabilityStaked(uint stakeIndex); - event WorkAssigned(address worker, uint requestIndex); - event WorkEvidenceSubmitted(uint requestIndex); - event WorkApprovalSubmitted(uint requestIndex, bool approval); - - constructor(DAO dao_, uint price_) { - dao = dao_; - price = price_; - } - - /// Accept availability stakes as reputation token transfer - function acceptAvailability( - address sender, - uint256 amount, - uint duration - ) external { - require(amount > 0, "No stake provided"); - uint stakeIndex = stakeCount++; - AvailabilityStake storage stake = stakes[stakeIndex]; - stake.worker = sender; - stake.amount = amount; - stake.endTime = block.timestamp + duration; - emit AvailabilityStaked(stakeIndex); - } - - 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" - ); - require(!stake.reclaimed, "Stake has already been reclaimed"); - require(!stake.assigned, "Stake has already been assigned work"); - if (block.timestamp > stake.endTime) { - stake.endTime = block.timestamp + duration; - } else { - stake.endTime = stake.endTime + duration; - } - emit AvailabilityStaked(stakeIndex); - } - - 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"); - require(!stake.assigned, "Stake has already been assigned work"); - stake.reclaimed = true; - dao.transfer(msg.sender, stake.amount); - emit AvailabilityStaked(stakeIndex); - } - - /// Select a worker randomly from among the available workers, weighted by amount staked - function randomWeightedSelection() internal view returns (uint stakeIndex) { - 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; - } - require(totalStakes > 0, "No available worker stakes"); - 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) { - stakeIndex = i; - break; - } - } - } - - /// Assign a random available worker - function assignWork(uint requestIndex) internal returns (uint stakeIndex) { - stakeIndex = randomWeightedSelection(); - AvailabilityStake storage stake = stakes[stakeIndex]; - stake.assigned = true; - emit WorkAssigned(stake.worker, requestIndex); - } - - /// Accept work request with fee - function requestWork() external payable { - require(msg.value >= price, "Insufficient fee"); - uint requestIndex = requestCount++; - WorkRequest storage request = requests[requestIndex]; - request.customer = msg.sender; - request.fee = msg.value; - request.stakeIndex = assignWork(requestIndex); - } - - /// Accept work evidence from worker - function submitWorkEvidence(uint requestIndex) external { - WorkRequest storage request = requests[requestIndex]; - require( - request.status == WorkStatus.Requested, - "Status must be Requested" - ); - AvailabilityStake storage stake = stakes[request.stakeIndex]; - require( - stake.worker == msg.sender, - "Worker can only submit evidence for work they are assigned" - ); - request.status = WorkStatus.EvidenceSubmitted; - emit WorkEvidenceSubmitted(requestIndex); - } - - /// Accept work approval/disapproval from customer - function submitWorkApproval(uint requestIndex, bool approval) external { - WorkRequest storage request = requests[requestIndex]; - require( - request.status == WorkStatus.EvidenceSubmitted, - "Status must be EvidenceSubmitted" - ); - AvailabilityStake storage stake = stakes[request.stakeIndex]; - request.status = WorkStatus.ApprovalSubmitted; - request.approval = approval; - // Make work evidence post - uint postIndex = dao.addPost(stake.worker); - emit WorkApprovalSubmitted(requestIndex, approval); - // Initiate validation pool - request.poolIndex = dao.initiateValidationPool{value: request.fee}( - postIndex, - POOL_DURATION, - false, - "" - ); - } +contract Work1 is WorkContract { + constructor(DAO dao_, uint price_) WorkContract(dao_, price_) {} } diff --git a/ethereum/contracts/WorkContract.sol b/ethereum/contracts/WorkContract.sol new file mode 100644 index 0000000..a9da0a3 --- /dev/null +++ b/ethereum/contracts/WorkContract.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.24; + +import "./DAO.sol"; +import "./IAcceptAvailability.sol"; + +abstract contract WorkContract is IAcceptAvailability { + struct AvailabilityStake { + address worker; + uint256 amount; + uint endTime; + bool assigned; + bool reclaimed; + } + + enum WorkStatus { + Requested, + EvidenceSubmitted, + ApprovalSubmitted, + Complete + } + + struct WorkRequest { + address customer; + uint256 fee; + WorkStatus status; + uint stakeIndex; + bool approval; + } + + DAO immutable dao; + uint public immutable price; + mapping(uint => AvailabilityStake) public stakes; + uint public stakeCount; + mapping(uint => WorkRequest) public requests; + uint public requestCount; + + // TODO: Make parameters configurable + uint constant POOL_DURATION = 1 days; + + event AvailabilityStaked(uint stakeIndex); + event WorkAssigned(address worker, uint requestIndex); + event WorkEvidenceSubmitted(uint requestIndex); + event WorkApprovalSubmitted(uint requestIndex, bool approval); + + constructor(DAO dao_, uint price_) { + dao = dao_; + price = price_; + } + + /// Accept availability stakes as reputation token transfer + function acceptAvailability( + address sender, + uint256 amount, + uint duration + ) external { + require(amount > 0, "No stake provided"); + uint stakeIndex = stakeCount++; + AvailabilityStake storage stake = stakes[stakeIndex]; + stake.worker = sender; + stake.amount = amount; + stake.endTime = block.timestamp + duration; + emit AvailabilityStaked(stakeIndex); + } + + 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" + ); + require(!stake.reclaimed, "Stake has already been reclaimed"); + require(!stake.assigned, "Stake has already been assigned work"); + if (block.timestamp > stake.endTime) { + stake.endTime = block.timestamp + duration; + } else { + stake.endTime = stake.endTime + duration; + } + emit AvailabilityStaked(stakeIndex); + } + + 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"); + require(!stake.assigned, "Stake has already been assigned work"); + stake.reclaimed = true; + dao.transfer(msg.sender, stake.amount); + emit AvailabilityStaked(stakeIndex); + } + + /// Select a worker randomly from among the available workers, weighted by amount staked + function randomWeightedSelection() internal view returns (uint stakeIndex) { + 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; + } + require(totalStakes > 0, "No available worker stakes"); + 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) { + stakeIndex = i; + break; + } + } + } + + /// Assign a random available worker + function assignWork(uint requestIndex) internal returns (uint stakeIndex) { + stakeIndex = randomWeightedSelection(); + AvailabilityStake storage stake = stakes[stakeIndex]; + stake.assigned = true; + emit WorkAssigned(stake.worker, requestIndex); + } + + /// Accept work request with fee + function requestWork() external payable { + require(msg.value >= price, "Insufficient fee"); + uint requestIndex = requestCount++; + WorkRequest storage request = requests[requestIndex]; + request.customer = msg.sender; + request.fee = msg.value; + request.stakeIndex = assignWork(requestIndex); + } + + /// Accept work evidence from worker + function submitWorkEvidence(uint requestIndex) external { + WorkRequest storage request = requests[requestIndex]; + require( + request.status == WorkStatus.Requested, + "Status must be Requested" + ); + AvailabilityStake storage stake = stakes[request.stakeIndex]; + require( + stake.worker == msg.sender, + "Worker can only submit evidence for work they are assigned" + ); + request.status = WorkStatus.EvidenceSubmitted; + emit WorkEvidenceSubmitted(requestIndex); + } + + /// Accept work approval/disapproval from customer + function submitWorkApproval( + uint requestIndex, + bool approval + ) external virtual { + WorkRequest storage request = requests[requestIndex]; + require( + request.status == WorkStatus.EvidenceSubmitted, + "Status must be EvidenceSubmitted" + ); + AvailabilityStake storage stake = stakes[request.stakeIndex]; + request.status = WorkStatus.ApprovalSubmitted; + request.approval = approval; + // Make work evidence post + uint postIndex = dao.addPost(stake.worker); + emit WorkApprovalSubmitted(requestIndex, approval); + // Initiate validation pool + dao.initiateValidationPool{value: request.fee}( + postIndex, + POOL_DURATION, + true, + abi.encode(requestIndex) + ); + } +} diff --git a/ethereum/test/DAO.js b/ethereum/test/DAO.js index e019abf..7b43a9b 100644 --- a/ethereum/test/DAO.js +++ b/ethereum/test/DAO.js @@ -42,11 +42,18 @@ describe('DAO', () => { let account1; const POOL_DURATION = 3600; // 1 hour const POOL_FEE = 100; + const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); beforeEach(async () => { ({ dao, account1 } = await loadFixture(deploy)); await dao.addPost(account1); - const init = () => dao.initiateValidationPool(0, POOL_DURATION, { value: POOL_FEE }); + const init = () => dao.initiateValidationPool( + 0, + POOL_DURATION, + false, + callbackData, + { 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); @@ -57,24 +64,42 @@ describe('DAO', () => { 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(0, POOL_DURATION); + const init = () => setup.dao.initiateValidationPool(0, POOL_DURATION, false, callbackData); await expect(init()).to.be.revertedWith('Fee is required to initiate validation pool'); }); it('should not be able to initiate a validation pool with duration below minimum', async () => { const setup = await loadFixture(deploy); - const init = () => setup.dao.initiateValidationPool(0, 59, { value: POOL_FEE }); + const init = () => setup.dao.initiateValidationPool( + 0, + 59, + false, + callbackData, + { value: POOL_FEE }, + ); 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 setup = await loadFixture(deploy); - const init = () => setup.dao.initiateValidationPool(0, 86401, { value: POOL_FEE }); + const init = () => setup.dao.initiateValidationPool( + 0, + 86401, + false, + callbackData, + { value: POOL_FEE }, + ); await expect(init()).to.be.revertedWith('Duration is too long'); }); it('should be able to initiate a second validation pool', async () => { - const init = () => dao.initiateValidationPool(0, POOL_DURATION, { value: POOL_FEE }); + const init = () => dao.initiateValidationPool( + 0, + POOL_DURATION, + false, + callbackData, + { value: POOL_FEE }, + ); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); expect(await dao.validationPoolCount()).to.equal(2); }); diff --git a/ethereum/test/Work1.js b/ethereum/test/Work1.js index f7da3c2..c0e6c92 100644 --- a/ethereum/test/Work1.js +++ b/ethereum/test/Work1.js @@ -18,7 +18,8 @@ describe('Work1', () => { const work1 = await Work1.deploy(dao.target, WORK1_PRICE); await dao.addPost(account1); - await dao.initiateValidationPool(0, 60, { value: 100 }); + const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); + await dao.initiateValidationPool(0, 60, false, callbackData, { value: 100 }); await time.increase(61); await dao.evaluateOutcome(0); @@ -236,6 +237,7 @@ describe('Work1', () => { const pool = await dao.validationPools(1); expect(pool.fee).to.equal(WORK1_PRICE); expect(pool.sender).to.equal(work1.target); + expect(pool.postIndex).to.equal(1); }); it('should be able to submit work disapproval', async () => {