Compare commits

...

2 Commits

Author SHA1 Message Date
Ladd Hoffman 37fd387cf3 add post separate from propose
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 38s Details
2024-04-28 16:51:35 -05:00
Ladd Hoffman 8e272bf2e8 refactor and stub for rollup 2024-04-28 15:06:10 -05:00
9 changed files with 274 additions and 98 deletions

View File

@ -0,0 +1,84 @@
// 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);
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() internal returns (uint stakeIndex) {
stakeIndex = randomWeightedSelection();
AvailabilityStake storage stake = stakes[stakeIndex];
stake.assigned = true;
}
}

View File

@ -59,22 +59,17 @@ contract Proposals is DAOContract, IOnValidate {
// TODO receive : we want to be able to accept refunds from validation pools
/// Submit a post as a proposal. DAO.addPost should be called before this.
function propose(
string calldata contentId,
address author,
string calldata postId,
uint[3] calldata durations,
bool callbackOnAccepted,
bytes calldata callbackData
) external payable returns (uint proposalIndex) {
// TODO: Take citations as a parameter
Citation[] memory emptyCitations;
Author[] memory authors = new Author[](1);
authors[0] = Author(1000000, author);
dao.addPost(authors, contentId, emptyCitations);
proposalIndex = proposalCount++;
Proposal storage proposal = proposals[proposalIndex];
proposal.sender = msg.sender;
proposal.postId = contentId;
proposal.postId = postId;
proposal.startTime = block.timestamp;
proposal.referenda[0].duration = durations[0];
proposal.referenda[1].duration = durations[1];

View File

@ -0,0 +1,72 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./WorkContract.sol";
import "./Rollup.sol";
abstract contract RollableWorkContract is WorkContract {
Rollup immutable rollupContract;
constructor(
DAO dao,
Proposals proposalsContract,
uint price,
Rollup rollupContract_
) WorkContract(dao, proposalsContract, price) {
rollupContract = rollupContract_;
}
/// Accept work approval/disapproval from customer
function submitWorkApproval(
uint requestIndex,
bool approval
) external override {
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;
emit WorkApprovalSubmitted(requestIndex, approval);
// Make work evidence post
Author[] memory authors = new Author[](1);
authors[0] = Author(1000000, stake.worker);
// TODO: send post and fee to rollup contract
dao.forwardAllowance(
stake.worker,
address(rollupContract),
stake.amount
);
payable(address(rollupContract)).transfer(request.fee);
rollupContract.addItem(
stake.worker,
stake.amount,
request.fee,
request.evidenceContentId
);
// dao.addPost(authors, request.evidenceContentId, request.citations);
// Initiate validation pool
uint poolIndex = dao.initiateValidationPool{value: request.fee}(
request.evidenceContentId,
POOL_DURATION,
[uint256(1), uint256(3)],
[uint256(1), uint256(2)],
100,
true,
false,
""
);
// We have an approval from stake.worker to transfer up to stake.amount
dao.delegatedStakeOnValidationPool(
poolIndex,
stake.worker,
stake.amount,
true
);
}
}

View File

@ -0,0 +1,82 @@
// 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) {}
struct BatchItem {
address worker;
uint stakeAmount;
uint fee;
string evidenceContentId;
}
mapping(uint => BatchItem) items;
uint itemCount;
address batchWorker;
uint batchWorkerStakeIndex;
function addItem(
address worker,
uint stakeAmount,
uint fee,
string calldata evidenceContentId
) public {
BatchItem storage item = items[itemCount++];
item.worker = worker;
item.stakeAmount = stakeAmount;
item.fee = fee;
item.evidenceContentId = evidenceContentId;
}
function submitBatch(
string calldata batchPostId,
uint poolDuration
) public {
if (batchWorker != address(0)) {
require(
msg.sender == batchWorker,
"Batch result must be submitted by current batch worker"
);
}
// initiate a validation pool for this batch
uint fee;
for (uint i = 0; i < itemCount; i++) {
fee += items[i].fee;
}
uint poolIndex = dao.initiateValidationPool{value: fee}(
batchPostId,
poolDuration,
[uint256(1), uint256(3)],
[uint256(1), uint256(2)],
100,
true,
false,
""
);
// Include all the availability stakes from the batched work
for (uint i = 0; i < itemCount; i++) {
dao.delegatedStakeOnValidationPool(
poolIndex,
items[i].worker,
items[i].stakeAmount,
true
);
}
// Include availability stakes from the batch worker
dao.delegatedStakeOnValidationPool(
poolIndex,
batchWorker,
stakes[batchWorkerStakeIndex].amount,
true
);
// Reset item count so we can start the next batch
itemCount = 0;
// Select the next worker
batchWorkerStakeIndex = assignWork();
batchWorker = stakes[batchWorkerStakeIndex].worker;
}
}

View File

@ -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,14 +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);
@ -64,71 +50,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");
@ -136,8 +62,9 @@ abstract contract WorkContract is
WorkRequest storage request = requests[requestIndex];
request.customer = msg.sender;
request.fee = msg.value;
request.stakeIndex = assignWork(requestIndex);
request.stakeIndex = assignWork();
request.requestContentId = requestContentId;
emit WorkAssigned(requestIndex, request.stakeIndex);
}
/// Accept work evidence from worker
@ -202,9 +129,11 @@ abstract contract WorkContract is
);
}
/// Initiate a new proposal to change the price for this work contract.
/// This takes a postId; DAO.addPost should be called before or concurrently with this.
function proposeNewPrice(
uint newPrice,
string calldata contentId,
string calldata postId,
uint[3] calldata durations
) external payable {
uint priceProposalIndex = priceProposalCount++;
@ -214,13 +143,7 @@ abstract contract WorkContract is
priceProposal.price = newPrice;
priceProposal.proposalIndex = proposalsContract.propose{
value: msg.value
}(
contentId,
msg.sender,
durations,
true,
abi.encode(priceProposalIndex)
);
}(postId, durations, true, abi.encode(priceProposalIndex));
emit PriceChangeProposed(priceProposalIndex);
}

View File

@ -24,4 +24,13 @@ contract Reputation is ERC20("Reputation", "REP") {
) public pure override returns (bool) {
revert("REP transfer is not allowed");
}
function forwardAllowance(
address owner,
address to,
uint256 amount
) public {
_spendAllowance(owner, msg.sender, amount);
_approve(owner, to, allowance(owner, to) + amount);
}
}

View File

@ -79,7 +79,8 @@ describe('Proposal', () => {
} = await loadFixture(deploy));
const emptyCallbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []);
await proposals.propose('proposal-content-id', account1, [20, 20, 20], false, emptyCallbackData, { value: 100 });
await dao.addPost([{ authorAddress: account1, weightPPM: 1000000 }], 'proposal-content-id', []);
await proposals.propose('proposal-content-id', [20, 20, 20], false, emptyCallbackData, { value: 100 });
expect(await proposals.proposalCount()).to.equal(1);
proposal = await proposals.proposals(0);
expect(proposal.postId).to.equal('proposal-content-id');

View File

@ -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);

View File

@ -28,7 +28,7 @@ const getProposalStatus = (proposal) => {
function Proposals() {
const {
provider, chainId, account, reputation,
provider, chainId, account, reputation, DAORef,
} = useContext(Web3Context);
const [proposals, dispatchProposal] = useList();
const proposalsContract = useRef();
@ -105,9 +105,15 @@ function Proposals() {
const onSubmitProposal = useCallback(async (post) => {
const web3 = new Web3(provider);
const emptyCallbackData = web3.eth.abi.encodeParameter('bytes', '0x00');
// First add a post for this proposal
await DAORef.current.methods.addPost(
[{ authorAddress: account, weightPPM: 1000000 }],
post.hash,
[], // TODO: Proposal can cite posts from matrix, semantic scholar, etc
);
// Now submit the post as a proposal
await proposalsContract.current.methods.propose(
post.hash,
account,
durations,
false,
emptyCallbackData,
@ -116,7 +122,7 @@ function Proposals() {
gas: 1000000,
value: 10000,
});
}, [provider, account, proposalsContract, durations]);
}, [provider, account, proposalsContract, durations, DAORef]);
const handleAttest = useCallback(async (proposalIndex) => {
await proposalsContract.current.methods.attest(proposalIndex, reputation).send({