diff --git a/ethereum/contracts/Availability.sol b/ethereum/contracts/Availability.sol new file mode 100644 index 0000000..9b9a30d --- /dev/null +++ b/ethereum/contracts/Availability.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.24; + +import "./core/DAO.sol"; +import "./interfaces/IAcceptAvailability.sol"; + +contract Availability is IAcceptAvailability, DAOContract { + struct AvailabilityStake { + address worker; + uint256 amount; + uint endTime; + bool assigned; + } + + mapping(uint => AvailabilityStake) public stakes; + uint public stakeCount; + + event AvailabilityStaked(uint stakeIndex); + event WorkAssigned(uint requestIndex, uint stakeIndex); + + constructor(DAO dao) DAOContract(dao) {} + + /// Accept availability stakes as reputation token transfer + function acceptAvailability( + address sender, + uint256 amount, + uint duration + ) external { + require( + msg.sender == address(dao), + "acceptAvailability must only be called by DAO contract" + ); + 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.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); + } + + /// 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(requestIndex, stakeIndex); + } +} diff --git a/ethereum/contracts/RollableWorkContract.sol b/ethereum/contracts/RollableWorkContract.sol new file mode 100644 index 0000000..13e79de --- /dev/null +++ b/ethereum/contracts/RollableWorkContract.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.24; + +import "./WorkContract.sol"; +import "./Rollup.sol"; + +abstract contract RollableWorkContract is WorkContract { + constructor( + DAO dao, + Proposals proposalsContract, + uint price, + Rollup rollupContract_ + ) WorkContract(dao, proposalsContract, price) {} +} diff --git a/ethereum/contracts/Rollup.sol b/ethereum/contracts/Rollup.sol new file mode 100644 index 0000000..4c8ffae --- /dev/null +++ b/ethereum/contracts/Rollup.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.24; + +import "./core/DAO.sol"; +import "./Availability.sol"; + +contract Rollup is Availability { + constructor(DAO dao) Availability(dao) {} +} diff --git a/ethereum/contracts/WorkContract.sol b/ethereum/contracts/WorkContract.sol index 2bf3909..03decca 100644 --- a/ethereum/contracts/WorkContract.sol +++ b/ethereum/contracts/WorkContract.sol @@ -3,22 +3,11 @@ pragma solidity ^0.8.24; import "./core/DAO.sol"; import "./core/Forum.sol"; +import "./Availability.sol"; import "./Proposals.sol"; -import "./interfaces/IAcceptAvailability.sol"; import "./interfaces/IOnProposalAccepted.sol"; -abstract contract WorkContract is - DAOContract, - IAcceptAvailability, - IOnProposalAccepted -{ - struct AvailabilityStake { - address worker; - uint256 amount; - uint endTime; - bool assigned; - } - +abstract contract WorkContract is Availability, IOnProposalAccepted { enum WorkStatus { Requested, EvidenceSubmitted, @@ -46,15 +35,11 @@ abstract contract WorkContract is uint public price; mapping(uint => PriceProposal) public priceProposals; uint public priceProposalCount; - mapping(uint => AvailabilityStake) public stakes; - uint public stakeCount; mapping(uint => WorkRequest) public requests; uint public requestCount; uint constant POOL_DURATION = 20; - event AvailabilityStaked(uint stakeIndex); - event WorkAssigned(uint requestIndex, uint stakeIndex); event WorkEvidenceSubmitted(uint requestIndex); event WorkApprovalSubmitted(uint requestIndex, bool approval); event PriceChangeProposed(uint priceProposalIndex); @@ -64,71 +49,11 @@ abstract contract WorkContract is DAO dao, Proposals proposalsContract_, uint price_ - ) DAOContract(dao) { + ) Availability(dao) { price = price_; proposalsContract = proposalsContract_; } - /// 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.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); - } - - /// 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(requestIndex, stakeIndex); - } - /// Accept work request with fee function requestWork(string calldata requestContentId) external payable { require(msg.value >= price, "Insufficient fee"); diff --git a/ethereum/test/Work1.js b/ethereum/test/Work1.js index 5fa952e..4bc3fee 100644 --- a/ethereum/test/Work1.js +++ b/ethereum/test/Work1.js @@ -78,6 +78,10 @@ describe('Work1', () => { await expect(dao.stakeAvailability(work1.target, 0, STAKE_DURATION)).to.be.revertedWith('No stake provided'); }); + it('should not be able to call acceptAvailability directly', async () => { + await expect(work1.acceptAvailability(account1, 50, STAKE_DURATION)).to.be.revertedWith('acceptAvailability must only be called by DAO contract'); + }); + it('should be able to extend the duration of an availability stake before it expires', async () => { await time.increase(STAKE_DURATION / 2); await expect(work1.extendAvailability(0, STAKE_DURATION)).to.emit(work1, 'AvailabilityStaked').withArgs(0);