Skip to content

Instantly share code, notes, and snippets.

@eladmallel
Created August 5, 2022 17:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eladmallel/f8ab6b9e5a1bf664666a562b4f6429fd to your computer and use it in GitHub Desktop.
Save eladmallel/f8ab6b9e5a1bf664666a562b4f6429fd to your computer and use it in GitHub Desktop.
NounsDAOLogic V1-V2 Diff
--- 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