192 lines
6.6 KiB
Solidity
192 lines
6.6 KiB
Solidity
|
// 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, "");
|
||
|
}
|
||
|
}
|