dgf-prototype/ethereum/contracts/Proposals.sol

296 lines
11 KiB
Solidity
Raw Normal View History

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;
import "./DAO.sol";
2024-04-09 05:33:04 -05:00
import "./interfaces/IOnValidate.sol";
import "./interfaces/IOnProposalAccepted.sol";
2024-03-28 15:06:14 -05:00
2024-03-26 21:32:41 -05:00
import "hardhat/console.sol";
2024-03-28 15:06:14 -05:00
contract Proposals is DAOContract, IOnValidate {
enum Stage {
Proposal,
Referendum0,
Referendum1,
Referendum100,
2024-03-27 14:03:57 -05:00
Failed,
Accepted
}
2024-03-26 21:32:41 -05:00
2024-03-28 15:06:14 -05:00
struct Pool {
uint poolIndex;
2024-03-29 18:08:30 -05:00
bool started;
bool completed;
2024-03-28 15:06:14 -05:00
uint stakedFor;
uint stakedAgainst;
2024-03-29 18:08:30 -05:00
bool votePasses;
bool quorumMet;
2024-03-28 15:06:14 -05:00
}
2024-03-26 21:32:41 -05:00
struct Referendum {
uint duration;
2024-03-28 15:06:14 -05:00
// Each referendum may retry up to 3x
2024-03-29 18:08:30 -05:00
Pool[3] pools;
2024-03-29 10:59:29 -05:00
uint retryCount;
2024-03-26 21:32:41 -05:00
}
struct Proposal {
address sender;
2024-03-26 21:32:41 -05:00
uint fee;
2024-03-29 10:59:29 -05:00
uint remainingFee;
2024-03-26 21:32:41 -05:00
uint postIndex;
uint startTime;
Stage stage;
mapping(address => uint) attestations;
uint attestationTotal;
2024-03-26 21:32:41 -05:00
Referendum[3] referenda;
bool callbackOnAccepted;
2024-03-30 17:21:26 -05:00
bytes callbackData;
}
2024-03-26 21:32:41 -05:00
mapping(uint => Proposal) public proposals;
uint public proposalCount;
2024-03-27 14:03:57 -05:00
event NewProposal(uint proposalIndex);
2024-03-29 18:08:30 -05:00
event Attestation(uint proposalIndex);
2024-03-27 14:03:57 -05:00
event ReferendumStarted(uint proposalIndex, uint poolIndex);
event ProposalFailed(uint proposalIndex, string reason);
event ProposalAccepted(uint proposalIndex);
constructor(DAO dao) DAOContract(dao) {}
2024-03-29 18:08:30 -05:00
// TODO receive : we want to be able to accept refunds from validation pools
function propose(
string calldata contentId,
address author,
uint[3] calldata durations,
bool callbackOnAccepted,
2024-03-30 17:21:26 -05:00
bytes calldata callbackData
2024-03-26 21:32:41 -05:00
) external payable returns (uint proposalIndex) {
// TODO: Consider taking author as a parameter,
// or else accepting a postIndex instead of contentId,
// or support post lookup by contentId
uint postIndex = dao.addPost(author, contentId);
proposalIndex = proposalCount++;
Proposal storage proposal = proposals[proposalIndex];
proposal.sender = msg.sender;
2024-03-26 21:32:41 -05:00
proposal.postIndex = postIndex;
proposal.startTime = block.timestamp;
proposal.referenda[0].duration = durations[0];
proposal.referenda[1].duration = durations[1];
proposal.referenda[2].duration = durations[2];
2024-03-26 21:32:41 -05:00
proposal.fee = msg.value;
2024-03-29 10:59:29 -05:00
proposal.remainingFee = proposal.fee;
proposal.callbackOnAccepted = callbackOnAccepted;
2024-03-30 17:21:26 -05:00
proposal.callbackData = callbackData;
2024-03-27 14:03:57 -05:00
emit NewProposal(proposalIndex);
}
2024-04-05 11:47:41 -05:00
/// Provides a summary of pools for a given proposal. Useful for displaying a summary.
2024-03-29 18:08:30 -05:00
function getPools(
uint proposalIndex
) public view returns (Pool[3][3] memory pools) {
Proposal storage proposal = proposals[proposalIndex];
pools[0] = proposal.referenda[0].pools;
pools[1] = proposal.referenda[1].pools;
pools[2] = proposal.referenda[2].pools;
}
// TODO: function getProposals()
// Enumerate timing so clients can render it
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.
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;
2024-03-29 18:08:30 -05:00
emit Attestation(proposalIndex);
}
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-29 10:59:29 -05:00
uint[2][3] referendaQuora = [[1, 10], [1, 10], [1, 10]];
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,
2024-03-29 10:59:29 -05:00
uint referendumIndex,
uint fee
2024-03-27 14:03:57 -05:00
) internal {
Proposal storage proposal = proposals[proposalIndex];
2024-03-29 10:59:29 -05:00
proposal.remainingFee -= fee;
uint poolIndex = dao.initiateValidationPool{value: fee}(
2024-03-27 14:03:57 -05:00
proposal.postIndex,
proposal.referenda[referendumIndex].duration,
2024-03-28 15:06:14 -05:00
referendaQuora[referendumIndex],
referendaWinRatio[referendumIndex],
2024-03-29 10:59:29 -05:00
referendaBindingPercent[referendumIndex],
referendaRedistributeLosingStakes[referendumIndex],
2024-03-27 14:03:57 -05:00
true,
2024-03-29 10:59:29 -05:00
abi.encode(proposalIndex, referendumIndex, fee)
2024-03-27 14:03:57 -05:00
);
2024-03-29 18:08:30 -05:00
Referendum storage referendum = proposal.referenda[referendumIndex];
Pool storage pool = referendum.pools[referendum.retryCount];
2024-03-28 15:06:14 -05:00
pool.poolIndex = poolIndex;
2024-03-29 18:08:30 -05:00
pool.started = true;
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 returns (uint) {
2024-03-27 14:03:57 -05:00
require(
msg.sender == address(dao),
"onValidate may only be called by the DAO contract"
);
2024-03-29 10:59:29 -05:00
(uint proposalIndex, uint referendumIndex, uint fee) = abi.decode(
2024-03-28 15:06:14 -05:00
callbackData,
2024-03-29 10:59:29 -05:00
(uint, uint, uint)
2024-03-28 15:06:14 -05:00
);
2024-03-27 14:03:57 -05:00
Proposal storage proposal = proposals[proposalIndex];
2024-03-29 18:08:30 -05:00
Referendum storage referendum = proposal.referenda[referendumIndex];
Pool storage pool = referendum.pools[referendum.retryCount];
// Make a record of this result
pool.completed = true;
pool.stakedFor = stakedFor;
pool.stakedAgainst = stakedAgainst;
pool.quorumMet = quorumMet;
pool.votePasses = votePasses;
2024-03-27 14:03:57 -05:00
if (!quorumMet) {
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Quorum not met");
2024-03-29 10:59:29 -05:00
proposal.remainingFee += fee;
return 1;
2024-03-27 14:03:57 -05:00
}
// Participation threshold of 50%
bool participationAboveThreshold = 2 * (stakedFor + stakedAgainst) >=
dao.totalSupply();
2024-03-28 15:06:14 -05:00
// Handle Referendum 0%
2024-03-27 14:03:57 -05:00
if (proposal.stage == Stage.Referendum0) {
2024-03-29 10:59:29 -05:00
require(referendumIndex == 0, "Stage 0 index mismatch");
2024-03-28 15:06:14 -05:00
// 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-29 10:59:29 -05:00
} else if (referendum.retryCount >= 2) {
2024-03-27 14:03:57 -05:00
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Retry count exceeded");
} else {
2024-03-29 10:59:29 -05:00
referendum.retryCount += 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) {
2024-03-29 10:59:29 -05:00
require(referendumIndex == 1, "Stage 1 index mismatch");
if (votePasses && participationAboveThreshold) {
2024-03-27 14:03:57 -05:00
proposal.stage = Stage.Referendum100;
2024-03-29 10:59:29 -05:00
} else if (referendum.retryCount >= 2) {
2024-03-27 14:03:57 -05:00
proposal.stage = Stage.Failed;
emit ProposalFailed(proposalIndex, "Retry count exceeded");
} else {
2024-03-29 10:59:29 -05:00
referendum.retryCount += 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) {
2024-03-29 10:59:29 -05:00
require(referendumIndex == 2, "Stage 2 index mismatch");
if (votePasses && participationAboveThreshold) {
2024-03-30 17:21:26 -05:00
// The proposal has passed all referenda and should become "law"
proposal.stage = Stage.Accepted;
2024-03-27 14:03:57 -05:00
// This is an opportunity for some actions to occur
// Emit an event
2024-03-27 14:03:57 -05:00
emit ProposalAccepted(proposalIndex);
// Execute a callback, if requested
if (proposal.callbackOnAccepted) {
IOnProposalAccepted(proposal.sender).onProposalAccepted(
stakedFor,
stakedAgainst,
proposal.callbackData
);
2024-03-30 17:21:26 -05:00
}
2024-03-29 10:59:29 -05:00
} else if (referendum.retryCount >= 2) {
2024-03-27 14:03:57 -05:00
proposal.stage = Stage.Failed;
2024-03-29 10:59:29 -05:00
emit ProposalFailed(proposalIndex, "Retry count exceeded");
} else {
referendum.retryCount += 1;
2024-03-27 14:03:57 -05:00
}
}
if (proposal.stage == Stage.Referendum0) {
2024-03-29 10:59:29 -05:00
initiateValidationPool(proposalIndex, 0, proposal.fee / 10);
2024-03-27 14:03:57 -05:00
} else if (proposal.stage == Stage.Referendum1) {
2024-03-29 10:59:29 -05:00
initiateValidationPool(proposalIndex, 1, proposal.fee / 10);
2024-03-27 14:03:57 -05:00
} else if (proposal.stage == Stage.Referendum100) {
2024-03-29 10:59:29 -05:00
initiateValidationPool(proposalIndex, 2, proposal.fee / 10);
2024-03-27 14:03:57 -05:00
}
return 0;
2024-03-27 14:03:57 -05:00
}
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
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();
2024-03-26 21:32:41 -05:00
bool expired = block.timestamp > proposal.startTime + 365 days;
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"
);
return false;
}
2024-03-27 14:03:57 -05:00
// Not yet expired, but has not met attestation threshold
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.
proposal.stage = Stage.Referendum0;
2024-03-27 14:03:57 -05:00
// Initiate validation pool
2024-03-29 10:59:29 -05:00
initiateValidationPool(proposalIndex, 0, proposal.fee / 10);
2024-03-26 21:32:41 -05:00
return true;
}
2024-03-29 18:08:30 -05:00
/// External function to reclaim remaining fees after a proposal has completed all referenda
function reclaimRemainingFee(uint proposalIndex) external {
Proposal storage proposal = proposals[proposalIndex];
require(
proposal.stage == Stage.Failed || proposal.stage == Stage.Accepted,
"Remaining fees can only be reclaimed when proposal has been accepted or failed"
);
uint amount = proposal.remainingFee;
proposal.remainingFee = 0;
payable(msg.sender).transfer(amount);
}
}