From 37fd387cf33db5cd73cfdc8ba6facfa800a36496 Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Sun, 28 Apr 2024 16:51:35 -0500 Subject: [PATCH] add post separate from propose --- ethereum/contracts/Availability.sol | 4 +- ethereum/contracts/Proposals.sol | 11 +--- ethereum/contracts/RollableWorkContract.sol | 60 ++++++++++++++++- ethereum/contracts/Rollup.sol | 73 +++++++++++++++++++++ ethereum/contracts/WorkContract.sol | 16 ++--- ethereum/contracts/core/Reputation.sol | 9 +++ ethereum/test/Proposals.js | 3 +- frontend/src/components/Proposals.jsx | 12 +++- 8 files changed, 163 insertions(+), 25 deletions(-) diff --git a/ethereum/contracts/Availability.sol b/ethereum/contracts/Availability.sol index 9b9a30d..51a98ef 100644 --- a/ethereum/contracts/Availability.sol +++ b/ethereum/contracts/Availability.sol @@ -16,7 +16,6 @@ contract Availability is IAcceptAvailability, DAOContract { uint public stakeCount; event AvailabilityStaked(uint stakeIndex); - event WorkAssigned(uint requestIndex, uint stakeIndex); constructor(DAO dao) DAOContract(dao) {} @@ -77,10 +76,9 @@ contract Availability is IAcceptAvailability, DAOContract { } /// Assign a random available worker - function assignWork(uint requestIndex) internal returns (uint stakeIndex) { + function assignWork() internal returns (uint stakeIndex) { stakeIndex = randomWeightedSelection(); AvailabilityStake storage stake = stakes[stakeIndex]; stake.assigned = true; - emit WorkAssigned(requestIndex, stakeIndex); } } diff --git a/ethereum/contracts/Proposals.sol b/ethereum/contracts/Proposals.sol index d7c258a..eb92de8 100644 --- a/ethereum/contracts/Proposals.sol +++ b/ethereum/contracts/Proposals.sol @@ -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]; diff --git a/ethereum/contracts/RollableWorkContract.sol b/ethereum/contracts/RollableWorkContract.sol index 13e79de..d5177a2 100644 --- a/ethereum/contracts/RollableWorkContract.sol +++ b/ethereum/contracts/RollableWorkContract.sol @@ -5,10 +5,68 @@ 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) {} + ) 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 + ); + } } diff --git a/ethereum/contracts/Rollup.sol b/ethereum/contracts/Rollup.sol index 4c8ffae..29e34bb 100644 --- a/ethereum/contracts/Rollup.sol +++ b/ethereum/contracts/Rollup.sol @@ -6,4 +6,77 @@ 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; + } } diff --git a/ethereum/contracts/WorkContract.sol b/ethereum/contracts/WorkContract.sol index 03decca..55837d4 100644 --- a/ethereum/contracts/WorkContract.sol +++ b/ethereum/contracts/WorkContract.sol @@ -40,6 +40,7 @@ abstract contract WorkContract is Availability, IOnProposalAccepted { uint constant POOL_DURATION = 20; + event WorkAssigned(uint requestIndex, uint stakeIndex); event WorkEvidenceSubmitted(uint requestIndex); event WorkApprovalSubmitted(uint requestIndex, bool approval); event PriceChangeProposed(uint priceProposalIndex); @@ -61,8 +62,9 @@ abstract contract WorkContract is Availability, IOnProposalAccepted { 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 @@ -127,9 +129,11 @@ abstract contract WorkContract is Availability, IOnProposalAccepted { ); } + /// 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++; @@ -139,13 +143,7 @@ abstract contract WorkContract is Availability, IOnProposalAccepted { 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); } diff --git a/ethereum/contracts/core/Reputation.sol b/ethereum/contracts/core/Reputation.sol index c17e201..42c8f4a 100644 --- a/ethereum/contracts/core/Reputation.sol +++ b/ethereum/contracts/core/Reputation.sol @@ -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); + } } diff --git a/ethereum/test/Proposals.js b/ethereum/test/Proposals.js index 99394af..cc871be 100644 --- a/ethereum/test/Proposals.js +++ b/ethereum/test/Proposals.js @@ -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'); diff --git a/frontend/src/components/Proposals.jsx b/frontend/src/components/Proposals.jsx index 7a1692d..21b5f9b 100644 --- a/frontend/src/components/Proposals.jsx +++ b/frontend/src/components/Proposals.jsx @@ -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({