dgf-prototype/ethereum/contracts/Proposal.sol

210 lines
7.5 KiB
Solidity

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./DAO.sol";
import "hardhat/console.sol";
contract Proposals is DAOContract {
struct Attestation {
address sender;
uint amount;
}
enum Stage {
Proposal,
Referendum0,
Referendum1,
Referendum100,
Failed,
Accepted
}
struct Referendum {
uint duration;
uint fee;
}
struct Proposal {
address sender;
uint fee;
uint postIndex;
uint startTime;
Stage stage;
mapping(uint => Attestation) attestations;
uint attestationCount;
Referendum[3] referenda;
uint[3] retryCount;
}
mapping(uint => Proposal) public proposals;
uint public proposalCount;
event NewProposal(uint proposalIndex);
event ReferendumStarted(uint proposalIndex, uint poolIndex);
event ProposalFailed(uint proposalIndex, string reason);
event ProposalAccepted(uint proposalIndex);
uint[3] referendaBindingPercent = [0, 1, 100];
bool[3] referendaRedistributeLosingStakes = [false, false, true];
constructor(DAO dao) DAOContract(dao) {}
function propose(
uint postIndex,
uint referendum0Duration,
uint referendum1Duration,
uint referendum100Duration
) external payable returns (uint proposalIndex) {
proposalIndex = proposalCount++;
Proposal storage proposal = proposals[proposalIndex];
proposal.postIndex = postIndex;
proposal.startTime = block.timestamp;
proposal.referenda[0].duration = referendum0Duration;
proposal.referenda[1].duration = referendum1Duration;
proposal.referenda[2].duration = referendum100Duration;
proposal.fee = msg.value;
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);
}
/// 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.
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;
}
/// Internal convenience function to wrap our call to dao.initiateValidationPool
/// and to emit an event
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,
1,
3,
bindingPercent,
redistributeLosingStakes,
true,
abi.encode(proposalIndex)
);
emit ReferendumStarted(proposalIndex, poolIndex);
}
/// 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);
}
}
/// External function that will advance a proposal to the referendum process
/// if attestation threshold has been reached
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();
bool expired = block.timestamp > proposal.startTime + 365 days;
if (!meetsAttestation) {
if (expired) {
// Expired without meeting attestation threshold
proposal.stage = Stage.Failed;
emit ProposalFailed(
proposalIndex,
"Expired without meeting attestation threshold"
);
return false;
}
// Not yet expired, but has not met attestation threshold
return false;
}
// 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.
proposal.stage = Stage.Referendum0;
// Initiate validation pool
initiateValidationPool(proposalIndex, 0);
return true;
}
}