// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.24; import "./DAO.sol"; import "./IOnValidate.sol"; import "hardhat/console.sol"; contract Proposals is DAOContract, IOnValidate { enum Stage { Proposal, Referendum0, Referendum1, Referendum100, Failed, 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 { address sender; uint fee; uint postIndex; uint startTime; Stage stage; mapping(address => uint) attestations; uint attestationTotal; Referendum[3] referenda; } 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); constructor(DAO dao) DAOContract(dao) {} function propose( string calldata contentId, uint referendum0Duration, uint referendum1Duration, uint referendum100Duration ) external payable returns (uint proposalIndex) { uint postIndex = dao.addPost(msg.sender, contentId); 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]; proposal.attestationTotal -= proposal.attestations[msg.sender]; proposal.attestations[msg.sender] = amount; proposal.attestationTotal += amount; } // --- Sequences of validation pool parameters --- // Percentage that each referendum is binding uint[3] referendaBindingPercent = [0, 1, 100]; // Whether to redistribute the binding portion of losing stakes in each referendum 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 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, referendaQuora[referendumIndex], referendaWinRatio[referendumIndex], bindingPercent, redistributeLosingStakes, true, abi.encode(proposalIndex, referendumIndex) ); Pool storage pool = proposal.referenda[referendumIndex].pools.push(); pool.poolIndex = poolIndex; emit ReferendumStarted(proposalIndex, poolIndex); } /// Callback to be executed when referenda pools complete 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, 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; // Participation threshold of 50% bool participationAboveThreshold = 2 * (stakedFor + stakedAgainst) >= dao.totalSupply(); // Handle Referendum 0% if (proposal.stage == Stage.Referendum0) { // If vote passes (2/3 majority) and has >= 50% participation if (votePasses && participationAboveThreshold) { proposal.stage = Stage.Referendum1; } else if (referendum.retryCount[0] >= 3) { proposal.stage = Stage.Failed; emit ProposalFailed(proposalIndex, "Retry count exceeded"); } else { referendum.retryCount[0] += 1; } // Handle Referendum 1% } else if (proposal.stage == Stage.Referendum1) { if (votePasses && participationAboveThreshold) { proposal.stage = Stage.Referendum100; } else if (referendum.retryCount[1] >= 3) { proposal.stage = Stage.Failed; emit ProposalFailed(proposalIndex, "Retry count exceeded"); } else { referendum.retryCount[1] += 1; } // Handle Referendum 100% } 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" ); bool meetsAttestation = 10 * proposal.attestationTotal >= 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; } }