2024-03-26 18:28:38 -05:00
|
|
|
// SPDX-License-Identifier: Unlicense
|
|
|
|
pragma solidity ^0.8.24;
|
|
|
|
|
|
|
|
import "./DAO.sol";
|
2024-03-28 15:06:14 -05:00
|
|
|
import "./IOnValidate.sol";
|
|
|
|
|
2024-03-26 21:32:41 -05:00
|
|
|
import "hardhat/console.sol";
|
2024-03-26 18:28:38 -05:00
|
|
|
|
2024-03-28 15:06:14 -05:00
|
|
|
contract Proposals is DAOContract, IOnValidate {
|
2024-03-26 18:28:38 -05:00
|
|
|
enum Stage {
|
|
|
|
Proposal,
|
|
|
|
Referendum0,
|
|
|
|
Referendum1,
|
|
|
|
Referendum100,
|
2024-03-27 14:03:57 -05:00
|
|
|
Failed,
|
|
|
|
Accepted
|
2024-03-26 18:28:38 -05:00
|
|
|
}
|
2024-03-26 21:32:41 -05:00
|
|
|
|
2024-03-28 15:06:14 -05:00
|
|
|
struct Pool {
|
|
|
|
uint poolIndex;
|
|
|
|
uint stakedFor;
|
|
|
|
uint stakedAgainst;
|
|
|
|
}
|
|
|
|
|
2024-03-26 21:32:41 -05:00
|
|
|
struct Referendum {
|
|
|
|
uint duration;
|
2024-03-27 14:03:57 -05:00
|
|
|
uint fee;
|
2024-03-28 15:06:14 -05:00
|
|
|
// Each referendum may retry up to 3x
|
|
|
|
Pool[] pools;
|
|
|
|
uint[3] retryCount;
|
2024-03-26 21:32:41 -05:00
|
|
|
}
|
|
|
|
|
2024-03-26 18:28:38 -05:00
|
|
|
struct Proposal {
|
|
|
|
address sender;
|
2024-03-26 21:32:41 -05:00
|
|
|
uint fee;
|
|
|
|
uint postIndex;
|
2024-03-26 18:28:38 -05:00
|
|
|
uint startTime;
|
|
|
|
Stage stage;
|
2024-03-28 18:01:55 -05:00
|
|
|
mapping(address => uint) attestations;
|
|
|
|
uint attestationTotal;
|
2024-03-26 21:32:41 -05:00
|
|
|
Referendum[3] referenda;
|
2024-03-26 18:28:38 -05:00
|
|
|
}
|
|
|
|
|
2024-03-26 21:32:41 -05:00
|
|
|
mapping(uint => Proposal) public proposals;
|
|
|
|
uint public proposalCount;
|
2024-03-26 18:28:38 -05:00
|
|
|
|
2024-03-27 14:03:57 -05:00
|
|
|
event NewProposal(uint proposalIndex);
|
|
|
|
event ReferendumStarted(uint proposalIndex, uint poolIndex);
|
|
|
|
event ProposalFailed(uint proposalIndex, string reason);
|
|
|
|
event ProposalAccepted(uint proposalIndex);
|
|
|
|
|
2024-03-26 18:28:38 -05:00
|
|
|
constructor(DAO dao) DAOContract(dao) {}
|
|
|
|
|
|
|
|
function propose(
|
2024-03-28 18:01:55 -05:00
|
|
|
string calldata contentId,
|
2024-03-26 21:32:41 -05:00
|
|
|
uint referendum0Duration,
|
|
|
|
uint referendum1Duration,
|
|
|
|
uint referendum100Duration
|
|
|
|
) external payable returns (uint proposalIndex) {
|
2024-03-28 18:01:55 -05:00
|
|
|
uint postIndex = dao.addPost(msg.sender, contentId);
|
2024-03-26 18:28:38 -05:00
|
|
|
proposalIndex = proposalCount++;
|
|
|
|
Proposal storage proposal = proposals[proposalIndex];
|
2024-03-26 21:32:41 -05:00
|
|
|
proposal.postIndex = postIndex;
|
2024-03-26 18:28:38 -05:00
|
|
|
proposal.startTime = block.timestamp;
|
2024-03-26 21:32:41 -05:00
|
|
|
proposal.referenda[0].duration = referendum0Duration;
|
|
|
|
proposal.referenda[1].duration = referendum1Duration;
|
|
|
|
proposal.referenda[2].duration = referendum100Duration;
|
|
|
|
proposal.fee = msg.value;
|
2024-03-27 14:03:57 -05:00
|
|
|
proposal.referenda[0].fee = proposal.fee / 3;
|
|
|
|
proposal.referenda[1].fee = proposal.fee / 3;
|
|
|
|
proposal.referenda[2].fee = proposal.fee - (proposal.fee * 2) / 3;
|
|
|
|
emit NewProposal(proposalIndex);
|
2024-03-26 18:28:38 -05:00
|
|
|
}
|
|
|
|
|
2024-03-27 14:11:53 -05:00
|
|
|
/// External function for reputation holders to attest toward a given proposal;
|
|
|
|
/// This is non-binding and non-encumbering, so it does not transfer any reputation.
|
2024-03-26 18:28:38 -05:00
|
|
|
function attest(uint proposalIndex, uint amount) external {
|
|
|
|
// Since this is non-binding, non-encumbering, we only need to verify that
|
|
|
|
// the sender actually has the rep they claim to stake.
|
|
|
|
require(
|
|
|
|
dao.balanceOf(msg.sender) >= amount,
|
|
|
|
"Sender has insufficient REP balance"
|
|
|
|
);
|
|
|
|
Proposal storage proposal = proposals[proposalIndex];
|
2024-03-28 18:01:55 -05:00
|
|
|
proposal.attestationTotal -= proposal.attestations[msg.sender];
|
|
|
|
proposal.attestations[msg.sender] = amount;
|
|
|
|
proposal.attestationTotal += amount;
|
2024-03-26 18:28:38 -05:00
|
|
|
}
|
|
|
|
|
2024-03-27 20:05:53 -05:00
|
|
|
// --- Sequences of validation pool parameters ---
|
|
|
|
// Percentage that each referendum is binding
|
2024-03-27 20:00:46 -05:00
|
|
|
uint[3] referendaBindingPercent = [0, 1, 100];
|
2024-03-27 20:05:53 -05:00
|
|
|
// Whether to redistribute the binding portion of losing stakes in each referendum
|
2024-03-27 20:00:46 -05:00
|
|
|
bool[3] referendaRedistributeLosingStakes = [false, false, true];
|
2024-03-27 20:05:53 -05:00
|
|
|
// For each referendum, a numerator-denominator pair representing its quorum
|
2024-03-27 20:01:25 -05:00
|
|
|
uint[2][3] referendaQuora = [[1, 10], [1, 2], [1, 3]];
|
2024-03-28 15:06:14 -05:00
|
|
|
// Win ratios
|
|
|
|
uint[2][3] referendaWinRatio = [[2, 3], [2, 3], [2, 3]];
|
2024-03-27 20:00:46 -05:00
|
|
|
|
2024-03-27 14:11:53 -05:00
|
|
|
/// Internal convenience function to wrap our call to dao.initiateValidationPool
|
|
|
|
/// and to emit an event
|
2024-03-27 14:03:57 -05:00
|
|
|
function initiateValidationPool(
|
|
|
|
uint proposalIndex,
|
|
|
|
uint referendumIndex
|
|
|
|
) internal {
|
|
|
|
uint bindingPercent = referendaBindingPercent[referendumIndex];
|
|
|
|
bool redistributeLosingStakes = referendaRedistributeLosingStakes[
|
|
|
|
referendumIndex
|
|
|
|
];
|
|
|
|
Proposal storage proposal = proposals[proposalIndex];
|
|
|
|
uint poolIndex = dao.initiateValidationPool{
|
|
|
|
value: proposal.referenda[referendumIndex].fee
|
|
|
|
}(
|
|
|
|
proposal.postIndex,
|
|
|
|
proposal.referenda[referendumIndex].duration,
|
2024-03-28 15:06:14 -05:00
|
|
|
referendaQuora[referendumIndex],
|
|
|
|
referendaWinRatio[referendumIndex],
|
2024-03-27 14:03:57 -05:00
|
|
|
bindingPercent,
|
|
|
|
redistributeLosingStakes,
|
|
|
|
true,
|
2024-03-28 15:06:14 -05:00
|
|
|
abi.encode(proposalIndex, referendumIndex)
|
2024-03-27 14:03:57 -05:00
|
|
|
);
|
2024-03-28 15:06:14 -05:00
|
|
|
Pool storage pool = proposal.referenda[referendumIndex].pools.push();
|
|
|
|
pool.poolIndex = poolIndex;
|
2024-03-27 14:03:57 -05:00
|
|
|
emit ReferendumStarted(proposalIndex, poolIndex);
|
|
|
|
}
|
2024-03-26 21:32:41 -05:00
|
|
|
|
2024-03-27 14:03:57 -05:00
|
|
|
/// Callback to be executed when referenda pools complete
|
|
|
|
function onValidate(
|
|
|
|
bool votePasses,
|
|
|
|
bool quorumMet,
|
2024-03-28 15:06:14 -05:00
|
|
|
uint stakedFor,
|
|
|
|
uint stakedAgainst,
|
2024-03-27 14:03:57 -05:00
|
|
|
bytes calldata callbackData
|
|
|
|
) external {
|
|
|
|
require(
|
|
|
|
msg.sender == address(dao),
|
|
|
|
"onValidate may only be called by the DAO contract"
|
|
|
|
);
|
2024-03-28 15:06:14 -05:00
|
|
|
(uint proposalIndex, uint referendumIndex) = abi.decode(
|
|
|
|
callbackData,
|
|
|
|
(uint, uint)
|
|
|
|
);
|
2024-03-27 14:03:57 -05:00
|
|
|
Proposal storage proposal = proposals[proposalIndex];
|
|
|
|
if (!quorumMet) {
|
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(proposalIndex, "Quorum not met");
|
|
|
|
return;
|
|
|
|
}
|
2024-03-28 15:06:14 -05:00
|
|
|
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%
|
2024-03-27 14:03:57 -05:00
|
|
|
if (proposal.stage == Stage.Referendum0) {
|
2024-03-28 15:06:14 -05:00
|
|
|
bool participationAboveThreshold = 2 *
|
|
|
|
(stakedFor + stakedAgainst) >=
|
|
|
|
dao.totalSupply();
|
|
|
|
// If vote passes (2/3 majority) and has >= 50% participation
|
|
|
|
if (votePasses && participationAboveThreshold) {
|
2024-03-27 14:03:57 -05:00
|
|
|
proposal.stage = Stage.Referendum1;
|
2024-03-28 15:06:14 -05:00
|
|
|
} else if (referendum.retryCount[0] >= 3) {
|
2024-03-27 14:03:57 -05:00
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(proposalIndex, "Retry count exceeded");
|
|
|
|
} else {
|
2024-03-28 15:06:14 -05:00
|
|
|
referendum.retryCount[0] += 1;
|
2024-03-27 14:03:57 -05:00
|
|
|
}
|
2024-03-28 15:06:14 -05:00
|
|
|
// Handle Referendum 1%
|
2024-03-27 14:03:57 -05:00
|
|
|
} else if (proposal.stage == Stage.Referendum1) {
|
|
|
|
if (votePasses) {
|
|
|
|
proposal.stage = Stage.Referendum100;
|
2024-03-28 15:06:14 -05:00
|
|
|
} else if (referendum.retryCount[1] >= 3) {
|
2024-03-27 14:03:57 -05:00
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(proposalIndex, "Retry count exceeded");
|
|
|
|
} else {
|
2024-03-28 15:06:14 -05:00
|
|
|
referendum.retryCount[1] += 1;
|
2024-03-27 14:03:57 -05:00
|
|
|
}
|
2024-03-28 15:06:14 -05:00
|
|
|
// Handle Referendum 100%
|
2024-03-27 14:03:57 -05:00
|
|
|
} else if (proposal.stage == Stage.Referendum100) {
|
|
|
|
// Note that no retries are attempted for referendum 100%
|
|
|
|
if (votePasses) {
|
|
|
|
// TODO: The proposal has passed all referenda and should become "law"
|
|
|
|
// This is an opportunity for some actions to occur
|
|
|
|
// We should at least emit an event
|
|
|
|
proposal.stage = Stage.Accepted;
|
|
|
|
emit ProposalAccepted(proposalIndex);
|
|
|
|
} else {
|
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(proposalIndex, "Binding pool was rejected");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (proposal.stage == Stage.Referendum0) {
|
|
|
|
initiateValidationPool(proposalIndex, 0);
|
|
|
|
} else if (proposal.stage == Stage.Referendum1) {
|
|
|
|
initiateValidationPool(proposalIndex, 1);
|
|
|
|
} else if (proposal.stage == Stage.Referendum100) {
|
|
|
|
initiateValidationPool(proposalIndex, 2);
|
|
|
|
}
|
|
|
|
}
|
2024-03-26 21:32:41 -05:00
|
|
|
|
2024-03-27 14:11:53 -05:00
|
|
|
/// External function that will advance a proposal to the referendum process
|
|
|
|
/// if attestation threshold has been reached
|
2024-03-26 18:28:38 -05:00
|
|
|
function evaluateAttestation(uint proposalIndex) external returns (bool) {
|
|
|
|
Proposal storage proposal = proposals[proposalIndex];
|
|
|
|
require(
|
|
|
|
proposal.stage == Stage.Proposal,
|
|
|
|
"Attestation only pertains to Proposal stage"
|
|
|
|
);
|
2024-03-28 18:01:55 -05:00
|
|
|
bool meetsAttestation = 10 * proposal.attestationTotal >=
|
|
|
|
dao.totalSupply();
|
2024-03-26 21:32:41 -05:00
|
|
|
bool expired = block.timestamp > proposal.startTime + 365 days;
|
2024-03-26 18:28:38 -05:00
|
|
|
if (!meetsAttestation) {
|
2024-03-26 21:32:41 -05:00
|
|
|
if (expired) {
|
2024-03-27 14:03:57 -05:00
|
|
|
// Expired without meeting attestation threshold
|
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(
|
|
|
|
proposalIndex,
|
|
|
|
"Expired without meeting attestation threshold"
|
|
|
|
);
|
2024-03-26 18:28:38 -05:00
|
|
|
return false;
|
|
|
|
}
|
2024-03-27 14:03:57 -05:00
|
|
|
// Not yet expired, but has not met attestation threshold
|
2024-03-26 18:28:38 -05:00
|
|
|
return false;
|
|
|
|
}
|
2024-03-27 14:03:57 -05:00
|
|
|
|
|
|
|
// Attestation threshold is met.
|
|
|
|
// Note that this may succeed even after expiry
|
|
|
|
// It can only happen once because the stage advances, and we required it above.
|
2024-03-26 18:28:38 -05:00
|
|
|
proposal.stage = Stage.Referendum0;
|
2024-03-27 14:03:57 -05:00
|
|
|
// Initiate validation pool
|
|
|
|
initiateValidationPool(proposalIndex, 0);
|
2024-03-26 21:32:41 -05:00
|
|
|
return true;
|
2024-03-26 18:28:38 -05:00
|
|
|
}
|
|
|
|
}
|