2024-03-26 18:28:38 -05:00
|
|
|
// SPDX-License-Identifier: Unlicense
|
|
|
|
pragma solidity ^0.8.24;
|
|
|
|
|
|
|
|
import "./DAO.sol";
|
2024-03-26 21:32:41 -05:00
|
|
|
import "hardhat/console.sol";
|
2024-03-26 18:28:38 -05:00
|
|
|
|
|
|
|
contract Proposals is DAOContract {
|
|
|
|
struct Attestation {
|
|
|
|
address sender;
|
|
|
|
uint amount;
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
struct Referendum {
|
|
|
|
uint duration;
|
2024-03-27 14:03:57 -05:00
|
|
|
uint fee;
|
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;
|
|
|
|
mapping(uint => Attestation) attestations;
|
|
|
|
uint attestationCount;
|
2024-03-26 21:32:41 -05:00
|
|
|
Referendum[3] referenda;
|
2024-03-27 14:03:57 -05:00
|
|
|
uint[3] retryCount;
|
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-26 21:32:41 -05:00
|
|
|
uint postIndex,
|
|
|
|
uint referendum0Duration,
|
|
|
|
uint referendum1Duration,
|
|
|
|
uint referendum100Duration
|
|
|
|
) external payable returns (uint proposalIndex) {
|
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];
|
|
|
|
uint attestationIndex = proposal.attestationCount++;
|
|
|
|
Attestation storage attestation = proposal.attestations[
|
|
|
|
attestationIndex
|
|
|
|
];
|
|
|
|
attestation.sender = msg.sender;
|
|
|
|
attestation.amount = amount;
|
|
|
|
}
|
|
|
|
|
2024-03-27 20:00:46 -05:00
|
|
|
// Sequences of validation pool parameters
|
|
|
|
uint[3] referendaBindingPercent = [0, 1, 100];
|
|
|
|
bool[3] referendaRedistributeLosingStakes = [false, false, true];
|
|
|
|
uint[2][3] referendaQuora = [[1, 3], [1, 2], [1, 3]];
|
|
|
|
|
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-27 20:00:46 -05:00
|
|
|
referendaQuora[referendumIndex][0],
|
|
|
|
referendaQuora[referendumIndex][1],
|
2024-03-27 14:03:57 -05:00
|
|
|
bindingPercent,
|
|
|
|
redistributeLosingStakes,
|
|
|
|
true,
|
|
|
|
abi.encode(proposalIndex)
|
|
|
|
);
|
|
|
|
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,
|
|
|
|
bytes calldata callbackData
|
|
|
|
) external {
|
|
|
|
require(
|
|
|
|
msg.sender == address(dao),
|
|
|
|
"onValidate may only be called by the DAO contract"
|
|
|
|
);
|
|
|
|
uint proposalIndex = abi.decode(callbackData, (uint));
|
|
|
|
Proposal storage proposal = proposals[proposalIndex];
|
|
|
|
if (!quorumMet) {
|
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(proposalIndex, "Quorum not met");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (proposal.stage == Stage.Referendum0) {
|
|
|
|
if (votePasses) {
|
|
|
|
proposal.stage = Stage.Referendum1;
|
|
|
|
} else if (proposal.retryCount[0] >= 3) {
|
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(proposalIndex, "Retry count exceeded");
|
|
|
|
} else {
|
|
|
|
proposal.retryCount[0] += 1;
|
|
|
|
}
|
|
|
|
} else if (proposal.stage == Stage.Referendum1) {
|
|
|
|
if (votePasses) {
|
|
|
|
proposal.stage = Stage.Referendum100;
|
|
|
|
} else if (proposal.retryCount[1] >= 3) {
|
|
|
|
proposal.stage = Stage.Failed;
|
|
|
|
emit ProposalFailed(proposalIndex, "Retry count exceeded");
|
|
|
|
} else {
|
|
|
|
proposal.retryCount[1] += 1;
|
|
|
|
}
|
|
|
|
} 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"
|
|
|
|
);
|
|
|
|
uint totalAttestation;
|
|
|
|
for (uint i = 0; i < proposal.attestationCount; i++) {
|
|
|
|
totalAttestation += proposal.attestations[i].amount;
|
|
|
|
}
|
|
|
|
bool meetsAttestation = 10 * totalAttestation >= 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
|
|
|
}
|
|
|
|
}
|