// 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; } }