dgf-prototype/ethereum/contracts/Onboarding.sol

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, "");
}
}