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
+
+
+
+
+ ID |
+ {/* Pool ID | */}
+
+
+
+ {proposals.filter((x) => !!x).map((request) => (
+
+ {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);