From 7ffd5322924f42c890614c0156e7f279516d797b Mon Sep 17 00:00:00 2001 From: Ladd Hoffman Date: Thu, 28 Mar 2024 15:06:14 -0500 Subject: [PATCH] Add win ratio VP param --- client/src/App.jsx | 4 +-- client/src/components/Proposals.jsx | 54 +++++++++++++++++++++++++++++ ethereum/contracts/DAO.sol | 36 ++++++++++++------- ethereum/contracts/IOnValidate.sol | 2 ++ ethereum/contracts/Onboarding.sol | 10 +++--- ethereum/contracts/Proposal.sol | 53 ++++++++++++++++++++++------ ethereum/contracts/WorkContract.sol | 4 +-- ethereum/test/DAO.js | 16 ++++----- ethereum/test/Onboarding.js | 12 ++++++- ethereum/test/Proposals.js | 24 +++++++++++-- ethereum/test/Work1.js | 12 ++++++- 11 files changed, 184 insertions(+), 43 deletions(-) create mode 100644 client/src/components/Proposals.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index ec5b4ef..dde9299 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -218,8 +218,8 @@ function App() { await DAO.methods.initiateValidationPool( postIndex, poolDuration ?? 3600, - 1, - 3, + [1, 3], + [1, 2], 100, true, false, diff --git a/client/src/components/Proposals.jsx b/client/src/components/Proposals.jsx new file mode 100644 index 0000000..13372a4 --- /dev/null +++ b/client/src/components/Proposals.jsx @@ -0,0 +1,54 @@ +import { useContext, useMemo } from 'react'; +import { PropTypes } from 'prop-types'; +import useList from '../utils/List'; +import WorkContractContext from '../contexts/WorkContractContext'; +import AvailabilityStakes from './work-contracts/AvailabilityStakes'; +import WorkRequests from './work-contracts/WorkRequests'; +import Web3Context from '../contexts/Web3Context'; + +function Proposals() { + const [proposals, dispatchProposal] = useList(); + const {provider} = useContext(Web3Context); + + return ( + <> +

Proposals

+
+ + + + + {/* */} + + + + {proposals.filter((x) => !!x).map((request) => ( + + + + ))} + +
IDPool ID
{request.id.toString()}
+
+ + ); +} + +WorkContract.propTypes = { + workContract: PropTypes.any.isRequired, // eslint-disable-line react/forbid-prop-types + showRequestWork: PropTypes.bool, + title: PropTypes.string.isRequired, + verb: PropTypes.string.isRequired, + showAvailabilityActions: PropTypes.bool, + showAvailabilityAmount: PropTypes.bool, + onlyShowAvailable: PropTypes.bool, +}; + +WorkContract.defaultProps = { + showRequestWork: false, + showAvailabilityActions: true, + showAvailabilityAmount: true, + onlyShowAvailable: false, +}; + +export default WorkContract; diff --git a/ethereum/contracts/DAO.sol b/ethereum/contracts/DAO.sol index 3edef70..4dd7126 100644 --- a/ethereum/contracts/DAO.sol +++ b/ethereum/contracts/DAO.sol @@ -26,6 +26,7 @@ struct ValidationPoolParams { uint quorumPPB; uint bindingPercent; bool redistributeLosingStakes; + uint[2] winRatio; // [ Numerator, Denominator ] } struct ValidationPool { @@ -91,8 +92,8 @@ contract DAO is ERC20("Reputation", "REP") { function initiateValidationPool( uint postIndex, uint duration, - uint quorumNumerator, - uint quorumDenominator, + uint[2] calldata quorum, // [Numerator, Denominator] + uint[2] calldata winRatio, // [Numerator, Denominator] uint bindingPercent, bool redistributeLosingStakes, bool callbackOnValidate, @@ -102,14 +103,11 @@ contract DAO is ERC20("Reputation", "REP") { require(duration >= minDuration, "Duration is too short"); require(duration <= maxDuration, "Duration is too long"); require( - (1_000_000_000 * quorumNumerator) / quorumDenominator >= - minQuorumPPB, + (1_000_000_000 * quorum[0]) / quorum[1] >= minQuorumPPB, "Quorum is below minimum" ); - require( - quorumNumerator <= quorumDenominator, - "Quorum is greater than one" - ); + require(quorum[0] <= quorum[1], "Quorum is greater than one"); + require(winRatio[0] <= winRatio[1], "Win ratio is greater than one"); require(bindingPercent <= 100, "Binding percent must be <= 100"); Post storage post = posts[postIndex]; require(post.author != address(0), "Target post not found"); @@ -118,9 +116,8 @@ contract DAO is ERC20("Reputation", "REP") { pool.sender = msg.sender; pool.postIndex = postIndex; pool.fee = msg.value; - pool.params.quorumPPB = - (1_000_000_000 * quorumNumerator) / - quorumDenominator; + pool.params.quorumPPB = (1_000_000_000 * quorum[0]) / quorum[1]; + pool.params.winRatio = winRatio; pool.params.bindingPercent = bindingPercent; pool.params.redistributeLosingStakes = redistributeLosingStakes; pool.duration = duration; @@ -198,6 +195,8 @@ contract DAO is ERC20("Reputation", "REP") { IOnValidate(pool.sender).onValidate( votePasses, false, + stakedFor, + stakedAgainst, pool.callbackData ); } @@ -207,7 +206,18 @@ contract DAO is ERC20("Reputation", "REP") { // A tie is resolved in favor of the validation pool. // This is especially important so that the DAO's first pool can pass, // when no reputation has yet been minted. - votePasses = stakedFor >= stakedAgainst; + // jconsole.log( + // "staked for %d against %d, win ratio %d / %d", + console.log("stakedFor", stakedFor); + console.log("stakedAgainst", stakedAgainst); + console.log( + "winRatio", + pool.params.winRatio[0], + pool.params.winRatio[1] + ); + votePasses = + stakedFor * pool.params.winRatio[1] >= + (stakedFor + stakedAgainst) * pool.params.winRatio[0]; if (votePasses && !isMember[post.author]) { members[memberCount++] = post.author; isMember[post.author] = true; @@ -268,6 +278,8 @@ contract DAO is ERC20("Reputation", "REP") { IOnValidate(pool.sender).onValidate( votePasses, true, + stakedFor, + stakedAgainst, pool.callbackData ); } diff --git a/ethereum/contracts/IOnValidate.sol b/ethereum/contracts/IOnValidate.sol index fd2e090..9a52be2 100644 --- a/ethereum/contracts/IOnValidate.sol +++ b/ethereum/contracts/IOnValidate.sol @@ -5,6 +5,8 @@ interface IOnValidate { function onValidate( bool votePasses, bool quorumMet, + uint stakedFor, + uint stakedAgainst, bytes calldata callbackData ) external; } diff --git a/ethereum/contracts/Onboarding.sol b/ethereum/contracts/Onboarding.sol index d7c8e7e..3240230 100644 --- a/ethereum/contracts/Onboarding.sol +++ b/ethereum/contracts/Onboarding.sol @@ -30,8 +30,8 @@ contract Onboarding is WorkContract, IOnValidate { }( postIndex, POOL_DURATION, - 1, - 3, + [uint256(1), uint256(3)], + [uint256(1), uint256(2)], 100, true, true, @@ -44,6 +44,8 @@ contract Onboarding is WorkContract, IOnValidate { function onValidate( bool votePasses, bool quorumMet, + uint, + uint, bytes calldata callbackData ) external { require( @@ -64,8 +66,8 @@ contract Onboarding is WorkContract, IOnValidate { dao.initiateValidationPool{value: request.fee / 10}( postIndex, POOL_DURATION, - 1, - 3, + [uint256(1), uint256(3)], + [uint256(1), uint256(2)], 100, true, false, diff --git a/ethereum/contracts/Proposal.sol b/ethereum/contracts/Proposal.sol index 710047f..faea108 100644 --- a/ethereum/contracts/Proposal.sol +++ b/ethereum/contracts/Proposal.sol @@ -2,9 +2,11 @@ pragma solidity ^0.8.24; import "./DAO.sol"; +import "./IOnValidate.sol"; + import "hardhat/console.sol"; -contract Proposals is DAOContract { +contract Proposals is DAOContract, IOnValidate { struct Attestation { address sender; uint amount; @@ -19,9 +21,18 @@ contract Proposals is DAOContract { Accepted } + struct Pool { + uint poolIndex; + uint stakedFor; + uint stakedAgainst; + } + struct Referendum { uint duration; uint fee; + // Each referendum may retry up to 3x + Pool[] pools; + uint[3] retryCount; } struct Proposal { @@ -33,7 +44,6 @@ contract Proposals is DAOContract { mapping(uint => Attestation) attestations; uint attestationCount; Referendum[3] referenda; - uint[3] retryCount; } mapping(uint => Proposal) public proposals; @@ -91,6 +101,8 @@ contract Proposals is DAOContract { bool[3] referendaRedistributeLosingStakes = [false, false, true]; // For each referendum, a numerator-denominator pair representing its quorum uint[2][3] referendaQuora = [[1, 10], [1, 2], [1, 3]]; + // Win ratios + uint[2][3] referendaWinRatio = [[2, 3], [2, 3], [2, 3]]; /// Internal convenience function to wrap our call to dao.initiateValidationPool /// and to emit an event @@ -108,13 +120,15 @@ contract Proposals is DAOContract { }( proposal.postIndex, proposal.referenda[referendumIndex].duration, - referendaQuora[referendumIndex][0], - referendaQuora[referendumIndex][1], + referendaQuora[referendumIndex], + referendaWinRatio[referendumIndex], bindingPercent, redistributeLosingStakes, true, - abi.encode(proposalIndex) + abi.encode(proposalIndex, referendumIndex) ); + Pool storage pool = proposal.referenda[referendumIndex].pools.push(); + pool.poolIndex = poolIndex; emit ReferendumStarted(proposalIndex, poolIndex); } @@ -122,37 +136,54 @@ contract Proposals is DAOContract { function onValidate( bool votePasses, bool quorumMet, + uint stakedFor, + uint stakedAgainst, bytes calldata callbackData ) external { require( msg.sender == address(dao), "onValidate may only be called by the DAO contract" ); - uint proposalIndex = abi.decode(callbackData, (uint)); + (uint proposalIndex, uint referendumIndex) = abi.decode( + callbackData, + (uint, uint) + ); Proposal storage proposal = proposals[proposalIndex]; if (!quorumMet) { proposal.stage = Stage.Failed; emit ProposalFailed(proposalIndex, "Quorum not met"); return; } + Referendum storage referendum = proposal.referenda[referendumIndex]; + Pool storage pool = referendum.pools[referendum.pools.length - 1]; + // Make a record of this result + pool.stakedFor = stakedFor; + pool.stakedAgainst = stakedAgainst; + // Handle Referendum 0% if (proposal.stage == Stage.Referendum0) { - if (votePasses) { + bool participationAboveThreshold = 2 * + (stakedFor + stakedAgainst) >= + dao.totalSupply(); + // If vote passes (2/3 majority) and has >= 50% participation + if (votePasses && participationAboveThreshold) { proposal.stage = Stage.Referendum1; - } else if (proposal.retryCount[0] >= 3) { + } else if (referendum.retryCount[0] >= 3) { proposal.stage = Stage.Failed; emit ProposalFailed(proposalIndex, "Retry count exceeded"); } else { - proposal.retryCount[0] += 1; + referendum.retryCount[0] += 1; } + // Handle Referendum 1% } else if (proposal.stage == Stage.Referendum1) { if (votePasses) { proposal.stage = Stage.Referendum100; - } else if (proposal.retryCount[1] >= 3) { + } else if (referendum.retryCount[1] >= 3) { proposal.stage = Stage.Failed; emit ProposalFailed(proposalIndex, "Retry count exceeded"); } else { - proposal.retryCount[1] += 1; + referendum.retryCount[1] += 1; } + // Handle Referendum 100% } else if (proposal.stage == Stage.Referendum100) { // Note that no retries are attempted for referendum 100% if (votePasses) { diff --git a/ethereum/contracts/WorkContract.sol b/ethereum/contracts/WorkContract.sol index 585ba33..5f9c7d0 100644 --- a/ethereum/contracts/WorkContract.sol +++ b/ethereum/contracts/WorkContract.sol @@ -176,8 +176,8 @@ abstract contract WorkContract is DAOContract, IAcceptAvailability { uint poolIndex = dao.initiateValidationPool{value: request.fee}( postIndex, POOL_DURATION, - 1, - 3, + [uint256(1), uint256(3)], + [uint256(1), uint256(2)], 100, true, false, diff --git a/ethereum/test/DAO.js b/ethereum/test/DAO.js index a70786a..96e2120 100644 --- a/ethereum/test/DAO.js +++ b/ethereum/test/DAO.js @@ -51,14 +51,14 @@ describe('DAO', () => { const initiateValidationPool = ({ postIndex, duration, - quorumNumerator, quorumDenominator, bindingPercent, + quorum, winRatio, bindingPercent, redistributeLosingStakes, callbackOnValidate, callbackData, fee, } = {}) => dao.initiateValidationPool( postIndex ?? 0, duration ?? POOL_DURATION, - quorumNumerator ?? 1, - quorumDenominator ?? 3, + quorum ?? [1, 3], + winRatio ?? [1, 2], bindingPercent ?? 100, redistributeLosingStakes ?? true, callbackOnValidate ?? false, @@ -84,12 +84,12 @@ describe('DAO', () => { }); it('should not be able to initiate a validation pool with a quorum below the minimum', async () => { - const init = () => initiateValidationPool({ quorumNumerator: 1, quorumDenominator: 11 }); + const init = () => initiateValidationPool({ quorum: [1, 11] }); await expect(init()).to.be.revertedWith('Quorum is below minimum'); }); it('should not be able to initiate a validation pool with a quorum greater than 1', async () => { - const init = () => initiateValidationPool({ quorumNumerator: 11, quorumDenominator: 10 }); + const init = () => initiateValidationPool({ quorum: [11, 10] }); await expect(init()).to.be.revertedWith('Quorum is greater than one'); }); @@ -192,8 +192,8 @@ describe('DAO', () => { const init = () => dao.initiateValidationPool( 0, POOL_DURATION, - 1, - 3, + [1, 3], + [1, 2], 100, true, false, @@ -213,7 +213,7 @@ describe('DAO', () => { time.increase(POOL_DURATION + 1); await expect(dao.evaluateOutcome(0)).to.emit(dao, 'ValidationPoolResolved').withArgs(0, true, true); - const init = () => initiateValidationPool({ quorumNumerator: 1, quorumDenominator: 1 }); + const init = () => initiateValidationPool({ quorum: [1, 1] }); await expect(init()).to.emit(dao, 'ValidationPoolInitiated').withArgs(1); expect(await dao.validationPoolCount()).to.equal(2); time.increase(POOL_DURATION + 1); diff --git a/ethereum/test/Onboarding.js b/ethereum/test/Onboarding.js index 1577db2..42bf60f 100644 --- a/ethereum/test/Onboarding.js +++ b/ethereum/test/Onboarding.js @@ -19,7 +19,17 @@ describe('Onboarding', () => { await dao.addPost(account1, 'content-id'); const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); - await dao.initiateValidationPool(0, 60, 1, 3, 100, true, false, callbackData, { value: 100 }); + await dao.initiateValidationPool( + 0, + 60, + [1, 3], + [1, 2], + 100, + true, + false, + callbackData, + { value: 100 }, + ); await time.increase(61); await dao.evaluateOutcome(0); expect(await dao.balanceOf(account1)).to.equal(100); diff --git a/ethereum/test/Proposals.js b/ethereum/test/Proposals.js index 0daf64c..1aea448 100644 --- a/ethereum/test/Proposals.js +++ b/ethereum/test/Proposals.js @@ -19,8 +19,28 @@ describe('Proposal', () => { await dao.addPost(account1, 'some-content-id'); await dao.addPost(account2, 'some-other-content-id'); const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); - await dao.initiateValidationPool(0, 60, 1, 3, 100, true, false, callbackData, { value: 100 }); - await dao.initiateValidationPool(1, 60, 1, 3, 100, true, false, callbackData, { value: 100 }); + await dao.initiateValidationPool( + 0, + 60, + [1, 3], + [1, 2], + 100, + true, + false, + callbackData, + { value: 100 }, + ); + await dao.initiateValidationPool( + 1, + 60, + [1, 3], + [1, 2], + 100, + true, + false, + callbackData, + { value: 100 }, + ); await time.increase(61); await dao.evaluateOutcome(0); await dao.evaluateOutcome(1); diff --git a/ethereum/test/Work1.js b/ethereum/test/Work1.js index 7090453..a5ff00a 100644 --- a/ethereum/test/Work1.js +++ b/ethereum/test/Work1.js @@ -19,7 +19,17 @@ describe('Work1', () => { await dao.addPost(account1, 'some-content-id'); const callbackData = ethers.AbiCoder.defaultAbiCoder().encode([], []); - await dao.initiateValidationPool(0, 60, 1, 3, 100, true, false, callbackData, { value: 100 }); + await dao.initiateValidationPool( + 0, + 60, + [1, 3], + [1, 2], + 100, + true, + false, + callbackData, + { value: 100 }, + ); await time.increase(61); await dao.evaluateOutcome(0);