// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.24; import "./DAO.sol"; import "./IAcceptAvailability.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); } /// 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.reviewPoolIndex = dao.initiateValidationPool{ value: request.fee - request.fee / 10 }(postIndex, POOL_DURATION, true, abi.encode(requestIndex)); } /// Callback to be executed when review pool completes function onValidate(bool votePasses, bytes calldata callbackData) external { require( msg.sender == address(dao), "onValidate may only be called by the DAO contract" ); if (!votePasses) return; 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, ""); } }