Created
August 5, 2022 17:19
-
-
Save eladmallel/f8ab6b9e5a1bf664666a562b4f6429fd to your computer and use it in GitHub Desktop.
NounsDAOLogic V1-V2 Diff
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- NounsDAOLogicV1.sol 2022-06-10 14:00:55.000000000 -0500 | |
+++ NounsDAOLogicV2.sol 2022-07-21 10:34:55.000000000 -0500 | |
@@ -1,6 +1,6 @@ | |
// SPDX-License-Identifier: BSD-3-Clause | |
-/// @title The Nouns DAO logic version 1 | |
+/// @title The Nouns DAO logic version 2 | |
/********************************* | |
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * | |
@@ -16,7 +16,7 @@ | |
*********************************/ | |
// LICENSE | |
-// NounsDAOLogicV1.sol is a modified version of Compound Lab's GovernorBravoDelegate.sol: | |
+// NounsDAOLogicV2.sol is a modified version of Compound Lab's GovernorBravoDelegate.sol: | |
// https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/Governance/GovernorBravoDelegate.sol | |
// | |
// GovernorBravoDelegate.sol source code Copyright 2020 Compound Labs, Inc. licensed under the BSD-3-Clause license. | |
@@ -25,44 +25,36 @@ | |
// Additional conditions of BSD-3-Clause can be found here: https://opensource.org/licenses/BSD-3-Clause | |
// | |
// MODIFICATIONS | |
-// NounsDAOLogicV1 adds: | |
-// - Proposal Threshold basis points instead of fixed number | |
-// due to the Noun token's increasing supply | |
-// | |
-// - Quorum Votes basis points instead of fixed number | |
-// due to the Noun token's increasing supply | |
-// | |
-// - Per proposal storing of fixed `proposalThreshold` | |
-// and `quorumVotes` calculated using the Noun token's total supply | |
-// at the block the proposal was created and the basis point parameters | |
-// | |
-// - `ProposalCreatedWithRequirements` event that emits `ProposalCreated` parameters with | |
-// the addition of `proposalThreshold` and `quorumVotes` | |
-// | |
-// - Votes are counted from the block a proposal is created instead of | |
-// the proposal's voting start block to align with the parameters | |
-// stored with the proposal | |
-// | |
-// - Veto ability which allows `veteor` to halt any proposal at any stage unless | |
-// the proposal is executed. | |
-// The `veto(uint proposalId)` logic is a modified version of `cancel(uint proposalId)` | |
-// A `vetoed` flag was added to the `Proposal` struct to support this. | |
-// | |
-// NounsDAOLogicV1 removes: | |
-// - `initialProposalId` and `_initiate()` due to this being the | |
-// first instance of the governance contract unlike | |
-// GovernorBravo which upgrades GovernorAlpha | |
-// | |
-// - Value passed along using `timelock.executeTransaction{value: proposal.value}` | |
-// in `execute(uint proposalId)`. This contract should not hold funds and does not | |
-// implement `receive()` or `fallback()` functions. | |
+// See NounsDAOLogicV1 for initial GovernorBravoDelegate modifications. | |
+ | |
+// NounsDAOLogicV2 adds: | |
+// - `quorumParamsCheckpoints`, which store dynamic quorum parameters checkpoints | |
+// to be used when calculating the dynamic quorum. | |
+// - `_setDynamicQuorumParams(DynamicQuorumParams memory params)`, which allows the | |
+// DAO to update the dynamic quorum parameters' values. | |
+// - `getDynamicQuorumParamsAt(uint256 blockNumber_)` | |
+// - Individual setters of the DynamicQuorumParams members: | |
+// - `_setMinQuorumVotesBPS(uint16 newMinQuorumVotesBPS)` | |
+// - `_setMaxQuorumVotesBPS(uint16 newMaxQuorumVotesBPS)` | |
+// - `_setQuorumCoefficient(uint32 newQuorumCoefficient)` | |
+// - `minQuorumVotes` and `maxQuorumVotes`, which returns the current min and | |
+// max quorum votes using the current Noun supply. | |
+// - New `Proposal` struct member: | |
+// - `totalSupply` used in dynamic quorum calculation. | |
+// - `creationBlock` used for retrieving checkpoints of votes and dynamic quorum params. This now | |
+// allows changing `votingDelay` without affecting the checkpoints lookup. | |
+// - `quorumVotes(uint256 proposalId)`, which calculates and returns the dynamic | |
+// quorum for a specific proposal. | |
+// - `proposals(uint256 proposalId)` instead of the implicit getter, to avoid stack-too-deep error | |
// | |
+// NounsDAOLogicV2 removes: | |
+// - `quorumVotes()` has been replaced by `quorumVotes(uint256 proposalId)`. | |
pragma solidity ^0.8.6; | |
import './NounsDAOInterfaces.sol'; | |
-contract NounsDAOLogicV1 is NounsDAOStorageV1, NounsDAOEvents { | |
+contract NounsDAOLogicV2 is NounsDAOStorageV2, NounsDAOEventsV2 { | |
/// @notice The name of this contract | |
string public constant name = 'Nouns DAO'; | |
@@ -84,8 +76,14 @@ | |
/// @notice The max setable voting delay | |
uint256 public constant MAX_VOTING_DELAY = 40_320; // About 1 week | |
- /// @notice The minimum setable quorum votes basis points | |
- uint256 public constant MIN_QUORUM_VOTES_BPS = 200; // 200 basis points or 2% | |
+ /// @notice The lower bound of minimum quorum votes basis points | |
+ uint256 public constant MIN_QUORUM_VOTES_BPS_LOWER_BOUND = 200; // 200 basis points or 2% | |
+ | |
+ /// @notice The upper bound of minimum quorum votes basis points | |
+ uint256 public constant MIN_QUORUM_VOTES_BPS_UPPER_BOUND = 2_000; // 2,000 basis points or 20% | |
+ | |
+ /// @notice The upper bound of maximum quorum votes basis points | |
+ uint256 public constant MAX_QUORUM_VOTES_BPS_UPPER_BOUND = 6_000; // 4,000 basis points or 60% | |
/// @notice The maximum setable quorum votes basis points | |
uint256 public constant MAX_QUORUM_VOTES_BPS = 2_000; // 2,000 basis points or 20% | |
@@ -93,6 +91,12 @@ | |
/// @notice The maximum number of actions that can be included in a proposal | |
uint256 public constant proposalMaxOperations = 10; // 10 actions | |
+ /// @notice The maximum priority fee used to cap gas refunds in `castRefundableVote` | |
+ uint256 public constant MAX_REFUND_PRIORITY_FEE = 2 gwei; | |
+ | |
+ /// @notice The vote refund gas overhead, including 7K for ETH transfer and 29K for general transaction overhead | |
+ uint256 public constant REFUND_BASE_GAS = 36000; | |
+ | |
/// @notice The EIP-712 typehash for the contract's domain | |
bytes32 public constant DOMAIN_TYPEHASH = | |
keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); | |
@@ -100,6 +104,13 @@ | |
/// @notice The EIP-712 typehash for the ballot struct used by the contract | |
bytes32 public constant BALLOT_TYPEHASH = keccak256('Ballot(uint256 proposalId,uint8 support)'); | |
+ /// @dev Introduced these errors to reduce contract size, to avoid deployment failure | |
+ error AdminOnly(); | |
+ error InvalidMinQuorumVotesBPS(); | |
+ error InvalidMaxQuorumVotesBPS(); | |
+ error MinQuorumBPSGreaterThanMaxQuorumBPS(); | |
+ error UnsafeUint16Cast(); | |
+ | |
/** | |
* @notice Used to initialize the contract during delegator contructor | |
* @param timelock_ The address of the NounsDAOExecutor | |
@@ -108,7 +119,7 @@ | |
* @param votingPeriod_ The initial voting period | |
* @param votingDelay_ The initial voting delay | |
* @param proposalThresholdBPS_ The initial proposal threshold in basis points | |
- * * @param quorumVotesBPS_ The initial quorum votes threshold in basis points | |
+ * @param dynamicQuorumParams_ The initial dynamic quorum parameters | |
*/ | |
function initialize( | |
address timelock_, | |
@@ -117,7 +128,7 @@ | |
uint256 votingPeriod_, | |
uint256 votingDelay_, | |
uint256 proposalThresholdBPS_, | |
- uint256 quorumVotesBPS_ | |
+ DynamicQuorumParams calldata dynamicQuorumParams_ | |
) public virtual { | |
require(address(timelock) == address(0), 'NounsDAO::initialize: can only initialize once'); | |
require(msg.sender == admin, 'NounsDAO::initialize: admin only'); | |
@@ -133,17 +144,12 @@ | |
); | |
require( | |
proposalThresholdBPS_ >= MIN_PROPOSAL_THRESHOLD_BPS && proposalThresholdBPS_ <= MAX_PROPOSAL_THRESHOLD_BPS, | |
- 'NounsDAO::initialize: invalid proposal threshold' | |
- ); | |
- require( | |
- quorumVotesBPS_ >= MIN_QUORUM_VOTES_BPS && quorumVotesBPS_ <= MAX_QUORUM_VOTES_BPS, | |
- 'NounsDAO::initialize: invalid proposal threshold' | |
+ 'NounsDAO::initialize: invalid proposal threshold bps' | |
); | |
emit VotingPeriodSet(votingPeriod, votingPeriod_); | |
emit VotingDelaySet(votingDelay, votingDelay_); | |
emit ProposalThresholdBPSSet(proposalThresholdBPS, proposalThresholdBPS_); | |
- emit QuorumVotesBPSSet(quorumVotesBPS, quorumVotesBPS_); | |
timelock = INounsDAOExecutor(timelock_); | |
nouns = NounsTokenLike(nouns_); | |
@@ -151,7 +157,11 @@ | |
votingPeriod = votingPeriod_; | |
votingDelay = votingDelay_; | |
proposalThresholdBPS = proposalThresholdBPS_; | |
- quorumVotesBPS = quorumVotesBPS_; | |
+ _setDynamicQuorumParams( | |
+ dynamicQuorumParams_.minQuorumVotesBPS, | |
+ dynamicQuorumParams_.maxQuorumVotesBPS, | |
+ dynamicQuorumParams_.quorumCoefficient | |
+ ); | |
} | |
struct ProposalTemp { | |
@@ -214,12 +224,10 @@ | |
temp.endBlock = temp.startBlock + votingPeriod; | |
proposalCount++; | |
- Proposal storage newProposal = proposals[proposalCount]; | |
- | |
+ Proposal storage newProposal = _proposals[proposalCount]; | |
newProposal.id = proposalCount; | |
newProposal.proposer = msg.sender; | |
newProposal.proposalThreshold = temp.proposalThreshold; | |
- newProposal.quorumVotes = bps2Uint(quorumVotesBPS, temp.totalSupply); | |
newProposal.eta = 0; | |
newProposal.targets = targets; | |
newProposal.values = values; | |
@@ -233,6 +241,8 @@ | |
newProposal.canceled = false; | |
newProposal.executed = false; | |
newProposal.vetoed = false; | |
+ newProposal.totalSupply = temp.totalSupply; | |
+ newProposal.creationBlock = block.number; | |
latestProposalIds[newProposal.proposer] = newProposal.id; | |
@@ -249,7 +259,8 @@ | |
description | |
); | |
- /// @notice Updated event with `proposalThreshold` and `quorumVotes` | |
+ /// @notice Updated event with `proposalThreshold` and `minQuorumVotes` | |
+ /// @notice `minQuorumVotes` is always zero since V2 introduces dynamic quorum with checkpoints | |
emit ProposalCreatedWithRequirements( | |
newProposal.id, | |
msg.sender, | |
@@ -260,7 +271,7 @@ | |
newProposal.startBlock, | |
newProposal.endBlock, | |
newProposal.proposalThreshold, | |
- newProposal.quorumVotes, | |
+ minQuorumVotes(), | |
description | |
); | |
@@ -276,7 +287,7 @@ | |
state(proposalId) == ProposalState.Succeeded, | |
'NounsDAO::queue: proposal can only be queued if it is succeeded' | |
); | |
- Proposal storage proposal = proposals[proposalId]; | |
+ Proposal storage proposal = _proposals[proposalId]; | |
uint256 eta = block.timestamp + timelock.delay(); | |
for (uint256 i = 0; i < proposal.targets.length; i++) { | |
queueOrRevertInternal( | |
@@ -314,7 +325,7 @@ | |
state(proposalId) == ProposalState.Queued, | |
'NounsDAO::execute: proposal can only be executed if it is queued' | |
); | |
- Proposal storage proposal = proposals[proposalId]; | |
+ Proposal storage proposal = _proposals[proposalId]; | |
proposal.executed = true; | |
for (uint256 i = 0; i < proposal.targets.length; i++) { | |
timelock.executeTransaction( | |
@@ -335,7 +346,7 @@ | |
function cancel(uint256 proposalId) external { | |
require(state(proposalId) != ProposalState.Executed, 'NounsDAO::cancel: cannot cancel executed proposal'); | |
- Proposal storage proposal = proposals[proposalId]; | |
+ Proposal storage proposal = _proposals[proposalId]; | |
require( | |
msg.sender == proposal.proposer || | |
nouns.getPriorVotes(proposal.proposer, block.number - 1) < proposal.proposalThreshold, | |
@@ -365,7 +376,7 @@ | |
require(msg.sender == vetoer, 'NounsDAO::veto: only vetoer'); | |
require(state(proposalId) != ProposalState.Executed, 'NounsDAO::veto: cannot veto executed proposal'); | |
- Proposal storage proposal = proposals[proposalId]; | |
+ Proposal storage proposal = _proposals[proposalId]; | |
proposal.vetoed = true; | |
for (uint256 i = 0; i < proposal.targets.length; i++) { | |
@@ -399,7 +410,7 @@ | |
bytes[] memory calldatas | |
) | |
{ | |
- Proposal storage p = proposals[proposalId]; | |
+ Proposal storage p = _proposals[proposalId]; | |
return (p.targets, p.values, p.signatures, p.calldatas); | |
} | |
@@ -410,7 +421,7 @@ | |
* @return The voting receipt | |
*/ | |
function getReceipt(uint256 proposalId, address voter) external view returns (Receipt memory) { | |
- return proposals[proposalId].receipts[voter]; | |
+ return _proposals[proposalId].receipts[voter]; | |
} | |
/** | |
@@ -420,7 +431,7 @@ | |
*/ | |
function state(uint256 proposalId) public view returns (ProposalState) { | |
require(proposalCount >= proposalId, 'NounsDAO::state: invalid proposal id'); | |
- Proposal storage proposal = proposals[proposalId]; | |
+ Proposal storage proposal = _proposals[proposalId]; | |
if (proposal.vetoed) { | |
return ProposalState.Vetoed; | |
} else if (proposal.canceled) { | |
@@ -429,7 +440,7 @@ | |
return ProposalState.Pending; | |
} else if (block.number <= proposal.endBlock) { | |
return ProposalState.Active; | |
- } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { | |
+ } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes(proposal.id)) { | |
return ProposalState.Defeated; | |
} else if (proposal.eta == 0) { | |
return ProposalState.Succeeded; | |
@@ -443,6 +454,34 @@ | |
} | |
/** | |
+ * @notice Returns the proposal details given a proposal id. | |
+ * The `quorumVotes` member holds the *current* quorum, given the current votes. | |
+ * @param proposalId the proposal id to get the data for | |
+ * @return A `ProposalCondensed` struct with the proposal data | |
+ */ | |
+ function proposals(uint256 proposalId) external view returns (ProposalCondensed memory) { | |
+ Proposal storage proposal = _proposals[proposalId]; | |
+ return | |
+ ProposalCondensed({ | |
+ id: proposal.id, | |
+ proposer: proposal.proposer, | |
+ proposalThreshold: proposal.proposalThreshold, | |
+ quorumVotes: quorumVotes(proposal.id), | |
+ eta: proposal.eta, | |
+ startBlock: proposal.startBlock, | |
+ endBlock: proposal.endBlock, | |
+ forVotes: proposal.forVotes, | |
+ againstVotes: proposal.againstVotes, | |
+ abstainVotes: proposal.abstainVotes, | |
+ canceled: proposal.canceled, | |
+ vetoed: proposal.vetoed, | |
+ executed: proposal.executed, | |
+ totalSupply: proposal.totalSupply, | |
+ creationBlock: proposal.creationBlock | |
+ }); | |
+ } | |
+ | |
+ /** | |
* @notice Cast a vote for a proposal | |
* @param proposalId The id of the proposal to vote on | |
* @param support The support value for the vote. 0=against, 1=for, 2=abstain | |
@@ -452,6 +491,59 @@ | |
} | |
/** | |
+ * @notice Cast a vote for a proposal, asking the DAO to refund gas costs. | |
+ * Users with > 0 votes receive refunds. Refunds are partial when using a gas priority fee higher than the DAO's cap. | |
+ * Refunds are partial when the DAO's balance is insufficient. | |
+ * No refund is sent when the DAO's balance is empty. No refund is sent to users with no votes. | |
+ * Voting takes place regardless of refund success. | |
+ * @param proposalId The id of the proposal to vote on | |
+ * @param support The support value for the vote. 0=against, 1=for, 2=abstain | |
+ * @dev Reentrancy is defended against in `castVoteInternal` at the `receipt.hasVoted == false` require statement. | |
+ */ | |
+ function castRefundableVote(uint256 proposalId, uint8 support) external { | |
+ castRefundableVoteInternal(proposalId, support, ''); | |
+ } | |
+ | |
+ /** | |
+ * @notice Cast a vote for a proposal, asking the DAO to refund gas costs. | |
+ * Users with > 0 votes receive refunds. Refunds are partial when using a gas priority fee higher than the DAO's cap. | |
+ * Refunds are partial when the DAO's balance is insufficient. | |
+ * No refund is sent when the DAO's balance is empty. No refund is sent to users with no votes. | |
+ * Voting takes place regardless of refund success. | |
+ * @param proposalId The id of the proposal to vote on | |
+ * @param support The support value for the vote. 0=against, 1=for, 2=abstain | |
+ * @param reason The reason given for the vote by the voter | |
+ * @dev Reentrancy is defended against in `castVoteInternal` at the `receipt.hasVoted == false` require statement. | |
+ */ | |
+ function castRefundableVoteWithReason( | |
+ uint256 proposalId, | |
+ uint8 support, | |
+ string calldata reason | |
+ ) external { | |
+ castRefundableVoteInternal(proposalId, support, reason); | |
+ } | |
+ | |
+ /** | |
+ * @notice Internal function that carries out refundable voting logic | |
+ * @param proposalId The id of the proposal to vote on | |
+ * @param support The support value for the vote. 0=against, 1=for, 2=abstain | |
+ * @param reason The reason given for the vote by the voter | |
+ * @dev Reentrancy is defended against in `castVoteInternal` at the `receipt.hasVoted == false` require statement. | |
+ */ | |
+ function castRefundableVoteInternal( | |
+ uint256 proposalId, | |
+ uint8 support, | |
+ string memory reason | |
+ ) internal { | |
+ uint256 startGas = gasleft(); | |
+ uint96 votes = castVoteInternal(msg.sender, proposalId, support); | |
+ emit VoteCast(msg.sender, proposalId, support, votes, reason); | |
+ if (votes > 0) { | |
+ _refundGas(startGas); | |
+ } | |
+ } | |
+ | |
+ /** | |
* @notice Cast a vote for a proposal with a reason | |
* @param proposalId The id of the proposal to vote on | |
* @param support The support value for the vote. 0=against, 1=for, 2=abstain | |
@@ -500,12 +592,12 @@ | |
) internal returns (uint96) { | |
require(state(proposalId) == ProposalState.Active, 'NounsDAO::castVoteInternal: voting is closed'); | |
require(support <= 2, 'NounsDAO::castVoteInternal: invalid vote type'); | |
- Proposal storage proposal = proposals[proposalId]; | |
+ Proposal storage proposal = _proposals[proposalId]; | |
Receipt storage receipt = proposal.receipts[voter]; | |
require(receipt.hasVoted == false, 'NounsDAO::castVoteInternal: voter already voted'); | |
/// @notice: Unlike GovernerBravo, votes are considered from the block the proposal was created in order to normalize quorumVotes and proposalThreshold metrics | |
- uint96 votes = nouns.getPriorVotes(voter, proposal.startBlock - votingDelay); | |
+ uint96 votes = nouns.getPriorVotes(voter, proposalCreationBlock(proposal)); | |
if (support == 0) { | |
proposal.againstVotes = proposal.againstVotes + votes; | |
@@ -564,7 +656,7 @@ | |
require( | |
newProposalThresholdBPS >= MIN_PROPOSAL_THRESHOLD_BPS && | |
newProposalThresholdBPS <= MAX_PROPOSAL_THRESHOLD_BPS, | |
- 'NounsDAO::_setProposalThreshold: invalid proposal threshold' | |
+ 'NounsDAO::_setProposalThreshold: invalid proposal threshold bps' | |
); | |
uint256 oldProposalThresholdBPS = proposalThresholdBPS; | |
proposalThresholdBPS = newProposalThresholdBPS; | |
@@ -573,20 +665,130 @@ | |
} | |
/** | |
- * @notice Admin function for setting the quorum votes basis points | |
- * @dev newQuorumVotesBPS must be greater than the hardcoded min | |
- * @param newQuorumVotesBPS new proposal threshold | |
+ * @notice Admin function for setting the minimum quorum votes bps | |
+ * @param newMinQuorumVotesBPS minimum quorum votes bps | |
+ * Must be between `MIN_QUORUM_VOTES_BPS_LOWER_BOUND` and `MIN_QUORUM_VOTES_BPS_UPPER_BOUND` | |
+ * Must be lower than or equal to maxQuorumVotesBPS | |
+ */ | |
+ function _setMinQuorumVotesBPS(uint16 newMinQuorumVotesBPS) external { | |
+ require(msg.sender == admin, 'NounsDAO::_setMinQuorumVotesBPS: admin only'); | |
+ DynamicQuorumParams memory params = getDynamicQuorumParamsAt(block.number); | |
+ | |
+ require( | |
+ newMinQuorumVotesBPS >= MIN_QUORUM_VOTES_BPS_LOWER_BOUND && | |
+ newMinQuorumVotesBPS <= MIN_QUORUM_VOTES_BPS_UPPER_BOUND, | |
+ 'NounsDAO::_setMinQuorumVotesBPS: invalid min quorum votes bps' | |
+ ); | |
+ require( | |
+ newMinQuorumVotesBPS <= params.maxQuorumVotesBPS, | |
+ 'NounsDAO::_setMinQuorumVotesBPS: min quorum votes bps greater than max' | |
+ ); | |
+ | |
+ uint16 oldMinQuorumVotesBPS = params.minQuorumVotesBPS; | |
+ params.minQuorumVotesBPS = newMinQuorumVotesBPS; | |
+ | |
+ _writeQuorumParamsCheckpoint(params); | |
+ | |
+ emit MinQuorumVotesBPSSet(oldMinQuorumVotesBPS, newMinQuorumVotesBPS); | |
+ } | |
+ | |
+ /** | |
+ * @notice Admin function for setting the maximum quorum votes bps | |
+ * @param newMaxQuorumVotesBPS maximum quorum votes bps | |
+ * Must be lower than `MAX_QUORUM_VOTES_BPS_UPPER_BOUND` | |
+ * Must be higher than or equal to minQuorumVotesBPS | |
*/ | |
- function _setQuorumVotesBPS(uint256 newQuorumVotesBPS) external { | |
- require(msg.sender == admin, 'NounsDAO::_setQuorumVotesBPS: admin only'); | |
+ function _setMaxQuorumVotesBPS(uint16 newMaxQuorumVotesBPS) external { | |
+ require(msg.sender == admin, 'NounsDAO::_setMaxQuorumVotesBPS: admin only'); | |
+ DynamicQuorumParams memory params = getDynamicQuorumParamsAt(block.number); | |
+ | |
require( | |
- newQuorumVotesBPS >= MIN_QUORUM_VOTES_BPS && newQuorumVotesBPS <= MAX_QUORUM_VOTES_BPS, | |
- 'NounsDAO::_setProposalThreshold: invalid proposal threshold' | |
+ newMaxQuorumVotesBPS <= MAX_QUORUM_VOTES_BPS_UPPER_BOUND, | |
+ 'NounsDAO::_setMaxQuorumVotesBPS: invalid max quorum votes bps' | |
); | |
- uint256 oldQuorumVotesBPS = quorumVotesBPS; | |
- quorumVotesBPS = newQuorumVotesBPS; | |
+ require( | |
+ params.minQuorumVotesBPS <= newMaxQuorumVotesBPS, | |
+ 'NounsDAO::_setMaxQuorumVotesBPS: min quorum votes bps greater than max' | |
+ ); | |
+ | |
+ uint16 oldMaxQuorumVotesBPS = params.maxQuorumVotesBPS; | |
+ params.maxQuorumVotesBPS = newMaxQuorumVotesBPS; | |
- emit QuorumVotesBPSSet(oldQuorumVotesBPS, quorumVotesBPS); | |
+ _writeQuorumParamsCheckpoint(params); | |
+ | |
+ emit MaxQuorumVotesBPSSet(oldMaxQuorumVotesBPS, newMaxQuorumVotesBPS); | |
+ } | |
+ | |
+ /** | |
+ * @notice Admin function for setting the dynamic quorum coefficient | |
+ * @param newQuorumCoefficient the new coefficient, as a fixed point integer with 6 decimals | |
+ */ | |
+ function _setQuorumCoefficient(uint32 newQuorumCoefficient) external { | |
+ require(msg.sender == admin, 'NounsDAO::_setQuorumCoefficient: admin only'); | |
+ DynamicQuorumParams memory params = getDynamicQuorumParamsAt(block.number); | |
+ | |
+ uint32 oldQuorumCoefficient = params.quorumCoefficient; | |
+ params.quorumCoefficient = newQuorumCoefficient; | |
+ | |
+ _writeQuorumParamsCheckpoint(params); | |
+ | |
+ emit QuorumCoefficientSet(oldQuorumCoefficient, newQuorumCoefficient); | |
+ } | |
+ | |
+ /** | |
+ * @notice Admin function for setting all the dynamic quorum parameters | |
+ * @param newMinQuorumVotesBPS minimum quorum votes bps | |
+ * Must be between `MIN_QUORUM_VOTES_BPS_LOWER_BOUND` and `MIN_QUORUM_VOTES_BPS_UPPER_BOUND` | |
+ * Must be lower than or equal to maxQuorumVotesBPS | |
+ * @param newMaxQuorumVotesBPS maximum quorum votes bps | |
+ * Must be lower than `MAX_QUORUM_VOTES_BPS_UPPER_BOUND` | |
+ * Must be higher than or equal to minQuorumVotesBPS | |
+ * @param newQuorumCoefficient the new coefficient, as a fixed point integer with 6 decimals | |
+ */ | |
+ function _setDynamicQuorumParams( | |
+ uint16 newMinQuorumVotesBPS, | |
+ uint16 newMaxQuorumVotesBPS, | |
+ uint32 newQuorumCoefficient | |
+ ) public { | |
+ if (msg.sender != admin) { | |
+ revert AdminOnly(); | |
+ } | |
+ if ( | |
+ newMinQuorumVotesBPS < MIN_QUORUM_VOTES_BPS_LOWER_BOUND || | |
+ newMinQuorumVotesBPS > MIN_QUORUM_VOTES_BPS_UPPER_BOUND | |
+ ) { | |
+ revert InvalidMinQuorumVotesBPS(); | |
+ } | |
+ if (newMaxQuorumVotesBPS > MAX_QUORUM_VOTES_BPS_UPPER_BOUND) { | |
+ revert InvalidMaxQuorumVotesBPS(); | |
+ } | |
+ if (newMinQuorumVotesBPS > newMaxQuorumVotesBPS) { | |
+ revert MinQuorumBPSGreaterThanMaxQuorumBPS(); | |
+ } | |
+ | |
+ DynamicQuorumParams memory oldParams = getDynamicQuorumParamsAt(block.number); | |
+ | |
+ DynamicQuorumParams memory params = DynamicQuorumParams({ | |
+ minQuorumVotesBPS: newMinQuorumVotesBPS, | |
+ maxQuorumVotesBPS: newMaxQuorumVotesBPS, | |
+ quorumCoefficient: newQuorumCoefficient | |
+ }); | |
+ _writeQuorumParamsCheckpoint(params); | |
+ | |
+ emit MinQuorumVotesBPSSet(oldParams.minQuorumVotesBPS, params.minQuorumVotesBPS); | |
+ emit MaxQuorumVotesBPSSet(oldParams.maxQuorumVotesBPS, params.maxQuorumVotesBPS); | |
+ emit QuorumCoefficientSet(oldParams.quorumCoefficient, params.quorumCoefficient); | |
+ } | |
+ | |
+ function _withdraw() external { | |
+ if (msg.sender != admin) { | |
+ revert AdminOnly(); | |
+ } | |
+ | |
+ uint256 amount = address(this).balance; | |
+ (bool sent, ) = msg.sender.call{ value: amount }(''); | |
+ | |
+ emit Withdraw(amount, sent); | |
} | |
/** | |
@@ -661,12 +863,144 @@ | |
return bps2Uint(proposalThresholdBPS, nouns.totalSupply()); | |
} | |
+ function proposalCreationBlock(Proposal storage proposal) internal view returns (uint256) { | |
+ if (proposal.creationBlock == 0) { | |
+ return proposal.startBlock - votingDelay; | |
+ } | |
+ return proposal.creationBlock; | |
+ } | |
+ | |
/** | |
- * @notice Current quorum votes using Noun Total Supply | |
+ * @notice Quorum votes required for a specific proposal to succeed | |
* Differs from `GovernerBravo` which uses fixed amount | |
*/ | |
- function quorumVotes() public view returns (uint256) { | |
- return bps2Uint(quorumVotesBPS, nouns.totalSupply()); | |
+ function quorumVotes(uint256 proposalId) public view returns (uint256) { | |
+ Proposal storage proposal = _proposals[proposalId]; | |
+ if (proposal.totalSupply == 0) { | |
+ return proposal.quorumVotes; | |
+ } | |
+ | |
+ return | |
+ dynamicQuorumVotes( | |
+ proposal.againstVotes, | |
+ proposal.totalSupply, | |
+ getDynamicQuorumParamsAt(proposal.creationBlock) | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * @notice Calculates the required quorum of for-votes based on the amount of against-votes | |
+ * The more against-votes there are for a proposal, the higher the required quorum is. | |
+ * The quorum BPS is between `params.minQuorumVotesBPS` and params.maxQuorumVotesBPS. | |
+ * The additional quorum is calculated as: | |
+ * quorumCoefficient * againstVotesBPS | |
+ * @dev Note the coefficient is a fixed point integer with 6 decimals | |
+ * @param againstVotes Number of against-votes in the proposal | |
+ * @param totalSupply The total supply of Nouns at the time of proposal creation | |
+ * @param params Configurable parameters for calculating the quorum based on againstVotes. See `DynamicQuorumParams` definition for additional details. | |
+ * @return quorumVotes The required quorum | |
+ */ | |
+ function dynamicQuorumVotes( | |
+ uint256 againstVotes, | |
+ uint256 totalSupply, | |
+ DynamicQuorumParams memory params | |
+ ) public pure returns (uint256) { | |
+ uint256 againstVotesBPS = (10000 * againstVotes) / totalSupply; | |
+ uint256 quorumAdjustmentBPS = (params.quorumCoefficient * againstVotesBPS) / 1e6; | |
+ uint256 adjustedQuorumBPS = params.minQuorumVotesBPS + quorumAdjustmentBPS; | |
+ uint256 quorumBPS = min(params.maxQuorumVotesBPS, adjustedQuorumBPS); | |
+ return bps2Uint(quorumBPS, totalSupply); | |
+ } | |
+ | |
+ /** | |
+ * @notice returns the dynamic quorum parameters values at a certain block number | |
+ * @dev The checkpoints array must not be empty, and the block number must be higher than or equal to | |
+ * the block of the first checkpoint | |
+ * @param blockNumber_ the block number to get the params at | |
+ * @return The dynamic quorum parameters that were set at the given block number | |
+ */ | |
+ function getDynamicQuorumParamsAt(uint256 blockNumber_) public view returns (DynamicQuorumParams memory) { | |
+ uint32 blockNumber = safe32(blockNumber_, 'NounsDAO::getDynamicQuorumParamsAt: block number exceeds 32 bits'); | |
+ uint256 len = quorumParamsCheckpoints.length; | |
+ | |
+ if (len == 0) { | |
+ return | |
+ DynamicQuorumParams({ | |
+ minQuorumVotesBPS: safe16(quorumVotesBPS), | |
+ maxQuorumVotesBPS: safe16(quorumVotesBPS), | |
+ quorumCoefficient: 0 | |
+ }); | |
+ } | |
+ | |
+ if (quorumParamsCheckpoints[len - 1].fromBlock <= blockNumber) { | |
+ return quorumParamsCheckpoints[len - 1].params; | |
+ } | |
+ | |
+ if (quorumParamsCheckpoints[0].fromBlock > blockNumber) { | |
+ return | |
+ DynamicQuorumParams({ | |
+ minQuorumVotesBPS: safe16(quorumVotesBPS), | |
+ maxQuorumVotesBPS: safe16(quorumVotesBPS), | |
+ quorumCoefficient: 0 | |
+ }); | |
+ } | |
+ | |
+ uint256 lower = 0; | |
+ uint256 upper = len - 1; | |
+ while (upper > lower) { | |
+ uint256 center = upper - (upper - lower) / 2; | |
+ DynamicQuorumParamsCheckpoint memory cp = quorumParamsCheckpoints[center]; | |
+ if (cp.fromBlock == blockNumber) { | |
+ return cp.params; | |
+ } else if (cp.fromBlock < blockNumber) { | |
+ lower = center; | |
+ } else { | |
+ upper = center - 1; | |
+ } | |
+ } | |
+ return quorumParamsCheckpoints[lower].params; | |
+ } | |
+ | |
+ function _writeQuorumParamsCheckpoint(DynamicQuorumParams memory params) internal { | |
+ uint32 blockNumber = safe32(block.number, 'block number exceeds 32 bits'); | |
+ uint256 pos = quorumParamsCheckpoints.length; | |
+ if (pos > 0 && quorumParamsCheckpoints[pos - 1].fromBlock == blockNumber) { | |
+ quorumParamsCheckpoints[pos - 1].params = params; | |
+ } else { | |
+ quorumParamsCheckpoints.push(DynamicQuorumParamsCheckpoint({ fromBlock: blockNumber, params: params })); | |
+ } | |
+ } | |
+ | |
+ function _refundGas(uint256 startGas) internal { | |
+ unchecked { | |
+ uint256 balance = address(this).balance; | |
+ if (balance == 0) { | |
+ return; | |
+ } | |
+ uint256 gasPrice = min(tx.gasprice, block.basefee + MAX_REFUND_PRIORITY_FEE); | |
+ uint256 gasUsed = startGas - gasleft() + REFUND_BASE_GAS; | |
+ uint256 refundAmount = min(gasPrice * gasUsed, balance); | |
+ (bool refundSent, ) = msg.sender.call{ value: refundAmount }(''); | |
+ emit RefundableVote(msg.sender, refundAmount, refundSent); | |
+ } | |
+ } | |
+ | |
+ function min(uint256 a, uint256 b) internal pure returns (uint256) { | |
+ return a < b ? a : b; | |
+ } | |
+ | |
+ /** | |
+ * @notice Current min quorum votes using Noun total supply | |
+ */ | |
+ function minQuorumVotes() public view returns (uint256) { | |
+ return bps2Uint(getDynamicQuorumParamsAt(block.number).minQuorumVotesBPS, nouns.totalSupply()); | |
+ } | |
+ | |
+ /** | |
+ * @notice Current max quorum votes using Noun total supply | |
+ */ | |
+ function maxQuorumVotes() public view returns (uint256) { | |
+ return bps2Uint(getDynamicQuorumParamsAt(block.number).maxQuorumVotesBPS, nouns.totalSupply()); | |
} | |
function bps2Uint(uint256 bps, uint256 number) internal pure returns (uint256) { | |
@@ -680,4 +1014,18 @@ | |
} | |
return chainId; | |
} | |
+ | |
+ function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { | |
+ require(n <= type(uint32).max, errorMessage); | |
+ return uint32(n); | |
+ } | |
+ | |
+ function safe16(uint256 n) internal pure returns (uint16) { | |
+ if (n > type(uint16).max) { | |
+ revert UnsafeUint16Cast(); | |
+ } | |
+ return uint16(n); | |
+ } | |
+ | |
+ receive() external payable {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment