diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol b/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol index e605fc3e78..8fb0b4d3d4 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol @@ -696,8 +696,6 @@ contract NounsDAOStorageV3 { INounsDAOForkEscrow forkEscrow; /// @notice address of the DAO's fork deployer contract IForkDAODeployer forkDAODeployer; - /// @notice the age of a noun, in noun ids, for a noun to be allowed to fork - uint16 nounAgeRequiredToFork; /// @notice ERC20 tokens to include when sending funds to a deployed fork address[] erc20TokensToIncludeInFork; /// @notice The treasury contract of the last deployed fork diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol index 9f4df01f82..00c5ccdc3b 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol @@ -803,10 +803,6 @@ contract NounsDAOLogicV3 is NounsDAOStorageV3, NounsDAOEventsV3 { ds._setForkThresholdBPS(newForkThresholdBPS); } - function _setNounAgeRequiredToFork(uint16 newNounAgeRequiredToFork) external { - ds._setNounAgeRequiredToFork(newNounAgeRequiredToFork); - } - /** * @notice Admin function for setting the proposal id at which vote snapshots start using the voting start block * instead of the proposal creation block. diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol b/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol index 4a36a45604..16ec547fb7 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol @@ -559,15 +559,6 @@ library NounsDAOV3Admin { ds.forkThresholdBPS = newForkThresholdBPS; } - function _setNounAgeRequiredToFork(NounsDAOStorageV3.StorageV3 storage ds, uint16 newNounAgeRequiredToFork) - external - onlyAdmin(ds) - { - emit NounAgeRequiredToForkSet(ds.nounAgeRequiredToFork, newNounAgeRequiredToFork); - - ds.nounAgeRequiredToFork = newNounAgeRequiredToFork; - } - /** * @notice Admin function for setting the timelocks and admin * @param timelock the new timelock contract diff --git a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol index d0cb582f8f..d87ffc701a 100644 --- a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol +++ b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol @@ -20,7 +20,6 @@ pragma solidity ^0.8.19; import { NounsDAOStorageV3, INounsDAOForkEscrow, INounsDAOExecutorV2 } from '../NounsDAOInterfaces.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import { NounsTokenFork } from './newdao/token/NounsTokenFork.sol'; -import { INounsAuctionHouseV2 } from '../../interfaces/INounsAuctionHouseV2.sol'; library NounsDAOV3Fork { error ForkThresholdNotMet(); @@ -28,7 +27,6 @@ library NounsDAOV3Fork { error ForkPeriodActive(); error AdminOnly(); error UseAlternativeWithdrawFunction(); - error NounIdNotOldEnough(); /// @notice Emitted when someones adds nouns to the fork escrow event EscrowedToFork( @@ -83,7 +81,6 @@ library NounsDAOV3Fork { INounsDAOForkEscrow forkEscrow = ds.forkEscrow; for (uint256 i = 0; i < tokenIds.length; i++) { - checkNounIdIsAllowedToFork(ds, tokenIds[i]); ds.nouns.safeTransferFrom(msg.sender, address(forkEscrow), tokenIds[i]); } @@ -154,7 +151,6 @@ library NounsDAOV3Fork { sendProRataTreasury(ds, ds.forkDAOTreasury, tokenIds.length, adjustedTotalSupply(ds)); for (uint256 i = 0; i < tokenIds.length; i++) { - checkNounIdIsAllowedToFork(ds, tokenIds[i]); ds.nouns.transferFrom(msg.sender, timelock, tokenIds[i]); } @@ -220,13 +216,6 @@ library NounsDAOV3Fork { return ds.forkEscrow.numTokensInEscrow(); } - function checkNounIdIsAllowedToFork(NounsDAOStorageV3.StorageV3 storage ds, uint256 tokenId) internal view { - if (ds.nounAgeRequiredToFork == 0) return; - - uint256 auctionedNounId = INounsAuctionHouseV2(ds.nouns.minter()).auction().nounId; - if (tokenId < auctionedNounId - ds.nounAgeRequiredToFork) revert NounIdNotOldEnough(); - } - /** * @notice Returns the number of nouns in supply minus nouns owned by the DAO, i.e. held in the treasury or in an * escrow after it has closed. diff --git a/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOLogicV3p1.sol b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOLogicV3p1.sol new file mode 100644 index 0000000000..06c2626869 --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOLogicV3p1.sol @@ -0,0 +1,1041 @@ +// SPDX-License-Identifier: BSD-3-Clause + +/// @title The Nouns DAO logic version 3 + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +// LICENSE +// 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. +// With modifications by Nounders DAO. +// +// Additional conditions of BSD-3-Clause can be found here: https://opensource.org/licenses/BSD-3-Clause +// +// MODIFICATIONS +// See NounsDAOLogicV1 for initial GovernorBravoDelegate modifications. +// See NounsDAOLogicV2 for additional modifications +// +// NounsDAOLogicV3 adds: +// - Contract has been broken down to use libraries because of contract size limitations +// - Proposal editing: allowing proposers to update their proposal’s transactions and text description, +// during the Updatable period only, which is the state upon proposal creation. Editing also works with signatures, +// assuming the proposer is able to accumulate signatures from the same signers. +// - Propose by signature: allowing Nouners and delegates to pool their voting power towards submitting a proposal, +// by submitting their signature, instead of the current approach where sponsors must delegate their votes to help +// a proposer achieve threshold. +// - Objection-only Period: a conditional voting period that gets activated upon a last-minute proposal swing +// from defeated to successful, affording against voters more reaction time. +// Only against votes are possible during the objection period. +// - Votes snapshot after voting delay: moving votes snapshot up, to provide Nouners with reaction time per proposal, +// to get their votes ready (e.g. some might want to move their delegations around). +// In NounsDAOLogicV2 the vote snapshot block is the proposal creation block. +// - Nouns fork: any token holder can signal to fork (exit) in response to a governance proposal. +// If a quorum of a configured threshold amount of tokens signals to exit, the fork will succeed. +// This will deploy a new DAO and send part of the treasury to the new DAO. +// +// 2 new states have been added to the proposal state machine: Updatable, ObjectionPeriod +// +// Updated state machine: +// Updatable -> Pending -> Active -> ObjectionPeriod (conditional) -> Succeeded -> Queued -> Executed +// ┖> Defeated +// + +pragma solidity ^0.8.19; + +import { NounsDAOEventsV3, INounsDAOExecutor, NounsTokenLike, INounsDAOForkEscrow, IForkDAODeployer, INounsDAOExecutorV2, NounsDAOStorageV2 } from '../NounsDAOInterfaces.sol'; +import { NounsDAOStorageV3p1 } from './NounsDAOStorageV3p1.sol'; +import { NounsDAOV3AdminV1p1 } from './NounsDAOV3AdminV1p1.sol'; +import { NounsDAOV3DynamicQuorumV1p1 } from './NounsDAOV3DynamicQuorumV1p1.sol'; +import { NounsDAOV3VotesV1p1 } from './NounsDAOV3VotesV1p1.sol'; +import { NounsDAOV3ProposalsV1p1 } from './NounsDAOV3ProposalsV1p1.sol'; +import { NounsDAOV3ForkV1p1 } from './NounsDAOV3ForkV1p1.sol'; + +contract NounsDAOLogicV3p1 is NounsDAOStorageV3p1, NounsDAOEventsV3 { + using NounsDAOV3AdminV1p1 for StorageV3; + using NounsDAOV3DynamicQuorumV1p1 for StorageV3; + using NounsDAOV3VotesV1p1 for StorageV3; + using NounsDAOV3ProposalsV1p1 for StorageV3; + using NounsDAOV3ForkV1p1 for StorageV3; + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * CONSTANTS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /// @notice The minimum setable proposal threshold + function MIN_PROPOSAL_THRESHOLD_BPS() public pure returns (uint256) { + return NounsDAOV3AdminV1p1.MIN_PROPOSAL_THRESHOLD_BPS; + } + + /// @notice The maximum setable proposal threshold + function MAX_PROPOSAL_THRESHOLD_BPS() public pure returns (uint256) { + return NounsDAOV3AdminV1p1.MAX_PROPOSAL_THRESHOLD_BPS; + } + + /// @notice The minimum setable voting period in blocks + function MIN_VOTING_PERIOD() public pure returns (uint256) { + return NounsDAOV3AdminV1p1.MIN_VOTING_PERIOD_BLOCKS; + } + + /// @notice The max setable voting period in blocks + function MAX_VOTING_PERIOD() public pure returns (uint256) { + return NounsDAOV3AdminV1p1.MAX_VOTING_PERIOD_BLOCKS; + } + + /// @notice The min setable voting delay in blocks + function MIN_VOTING_DELAY() public pure returns (uint256) { + return NounsDAOV3AdminV1p1.MIN_VOTING_DELAY_BLOCKS; + } + + /// @notice The max setable voting delay in blocks + function MAX_VOTING_DELAY() public pure returns (uint256) { + return NounsDAOV3AdminV1p1.MAX_VOTING_DELAY_BLOCKS; + } + + /// @notice The maximum number of actions that can be included in a proposal + function proposalMaxOperations() public pure returns (uint256) { + return NounsDAOV3ProposalsV1p1.PROPOSAL_MAX_OPERATIONS; + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * ERRORS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + error AdminOnly(); + error CanOnlyInitializeOnce(); + error InvalidTimelockAddress(); + error InvalidNounsAddress(); + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * INITIALIZER + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Used to initialize the contract during delegator contructor + * @dev This will only be called for a newly deployed DAO, not as part of an upgrade from V2 to V3 + * @param timelock_ The address of the NounsDAOExecutor + * @param nouns_ The address of the NOUN tokens + * @param forkEscrow_ The escrow contract used for creating forks + * @param forkDAODeployer_ The contract used to deploy new forked DAOs + * @param vetoer_ The address allowed to unilaterally veto proposals + * @param daoParams_ Initial DAO parameters + * @param dynamicQuorumParams_ The initial dynamic quorum parameters + */ + function initialize( + address timelock_, + address nouns_, + address forkEscrow_, + address forkDAODeployer_, + address vetoer_, + NounsDAOParams calldata daoParams_, + DynamicQuorumParams calldata dynamicQuorumParams_ + ) public virtual { + if (address(ds.timelock) != address(0)) revert CanOnlyInitializeOnce(); + if (msg.sender != ds.admin) revert AdminOnly(); + if (timelock_ == address(0)) revert InvalidTimelockAddress(); + if (nouns_ == address(0)) revert InvalidNounsAddress(); + + ds._setVotingPeriod(daoParams_.votingPeriod); + ds._setVotingDelay(daoParams_.votingDelay); + ds._setProposalThresholdBPS(daoParams_.proposalThresholdBPS); + ds.timelock = INounsDAOExecutorV2(timelock_); + ds.nouns = NounsTokenLike(nouns_); + ds.forkEscrow = INounsDAOForkEscrow(forkEscrow_); + ds.forkDAODeployer = IForkDAODeployer(forkDAODeployer_); + ds.vetoer = vetoer_; + _setDynamicQuorumParams( + dynamicQuorumParams_.minQuorumVotesBPS, + dynamicQuorumParams_.maxQuorumVotesBPS, + dynamicQuorumParams_.quorumCoefficient + ); + + ds._setLastMinuteWindowInBlocks(daoParams_.lastMinuteWindowInBlocks); + ds._setObjectionPeriodDurationInBlocks(daoParams_.objectionPeriodDurationInBlocks); + ds._setProposalUpdatablePeriodInBlocks(daoParams_.proposalUpdatablePeriodInBlocks); + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * PROPOSALS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Function used to propose a new proposal. Sender must have delegates above the proposal threshold + * @param targets Target addresses for proposal calls + * @param values Eth values for proposal calls + * @param signatures Function signatures for proposal calls + * @param calldatas Calldatas for proposal calls + * @param description String description of the proposal + * @return uint256 Proposal id of new proposal + */ + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) public returns (uint256) { + return ds.propose(NounsDAOV3ProposalsV1p1.ProposalTxs(targets, values, signatures, calldatas), description); + } + + /** + * @notice Function used to propose a new proposal. Sender must have delegates above the proposal threshold. + * This proposal would be executed via the timelockV1 contract. This is meant to be used in case timelockV1 + * is still holding funds or has special permissions to execute on certain contracts. + * @param targets Target addresses for proposal calls + * @param values Eth values for proposal calls + * @param signatures Function signatures for proposal calls + * @param calldatas Calldatas for proposal calls + * @param description String description of the proposal + * @return uint256 Proposal id of new proposal + */ + function proposeOnTimelockV1( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) public returns (uint256) { + return + ds.proposeOnTimelockV1( + NounsDAOV3ProposalsV1p1.ProposalTxs(targets, values, signatures, calldatas), + description + ); + } + + /** + * @notice Function used to propose a new proposal. Sender and signers must have delegates above the proposal threshold + * Signers are regarded as co-proposers, and therefore have the ability to cancel the proposal at any time. + * @param proposerSignatures Array of signers who have signed the proposal and their signatures. + * @dev The signatures follow EIP-712. See `PROPOSAL_TYPEHASH` in NounsDAOV3ProposalsV1p1.sol + * @param targets Target addresses for proposal calls + * @param values Eth values for proposal calls + * @param signatures Function signatures for proposal calls + * @param calldatas Calldatas for proposal calls + * @param description String description of the proposal + * @return uint256 Proposal id of new proposal + */ + function proposeBySigs( + ProposerSignature[] memory proposerSignatures, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256) { + return + ds.proposeBySigs( + proposerSignatures, + NounsDAOV3ProposalsV1p1.ProposalTxs(targets, values, signatures, calldatas), + description + ); + } + + /** + * @notice Invalidates a signature that may be used for signing a new proposal. + * Once a signature is canceled, the sender can no longer use it again. + * If the sender changes their mind and want to sign the proposal, they can change the expiry timestamp + * in order to produce a new signature. + * The signature will only be invalidated when used by the sender. If used by a different account, it will + * not be invalidated. + * Cancelling a signature for an existing proposal will have no effect. Signers have the ability to cancel + * a proposal they signed if necessary. + * @param sig The signature to cancel + */ + function cancelSig(bytes calldata sig) external { + ds.cancelSig(sig); + } + + /** + * @notice Update a proposal transactions and description. + * Only the proposer can update it, and only during the updateable period. + * @param proposalId Proposal's id + * @param targets Updated target addresses for proposal calls + * @param values Updated eth values for proposal calls + * @param signatures Updated function signatures for proposal calls + * @param calldatas Updated calldatas for proposal calls + * @param description Updated description of the proposal + * @param updateMessage Short message to explain the update + */ + function updateProposal( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description, + string memory updateMessage + ) external { + ds.updateProposal(proposalId, targets, values, signatures, calldatas, description, updateMessage); + } + + /** + * @notice Updates the proposal's description. Only the proposer can update it, and only during the updateable period. + * @param proposalId Proposal's id + * @param description Updated description of the proposal + * @param updateMessage Short message to explain the update + */ + function updateProposalDescription( + uint256 proposalId, + string calldata description, + string calldata updateMessage + ) external { + ds.updateProposalDescription(proposalId, description, updateMessage); + } + + /** + * @notice Updates the proposal's transactions. Only the proposer can update it, and only during the updateable period. + * @param proposalId Proposal's id + * @param targets Updated target addresses for proposal calls + * @param values Updated eth values for proposal calls + * @param signatures Updated function signatures for proposal calls + * @param calldatas Updated calldatas for proposal calls + * @param updateMessage Short message to explain the update + */ + function updateProposalTransactions( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory updateMessage + ) external { + ds.updateProposalTransactions(proposalId, targets, values, signatures, calldatas, updateMessage); + } + + /** + * @notice Update a proposal's transactions and description that was created with proposeBySigs. + * Only the proposer can update it, during the updateable period. + * Requires the original signers to sign the update. + * @param proposalId Proposal's id + * @param proposerSignatures Array of signers who have signed the proposal and their signatures. + * @dev The signatures follow EIP-712. See `UPDATE_PROPOSAL_TYPEHASH` in NounsDAOV3ProposalsV1p1.sol + * @param targets Updated target addresses for proposal calls + * @param values Updated eth values for proposal calls + * @param signatures Updated function signatures for proposal calls + * @param calldatas Updated calldatas for proposal calls + * @param description Updated description of the proposal + * @param updateMessage Short message to explain the update + */ + function updateProposalBySigs( + uint256 proposalId, + ProposerSignature[] memory proposerSignatures, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description, + string memory updateMessage + ) external { + ds.updateProposalBySigs( + proposalId, + proposerSignatures, + NounsDAOV3ProposalsV1p1.ProposalTxs(targets, values, signatures, calldatas), + description, + updateMessage + ); + } + + /** + * @notice Queues a proposal of state succeeded + * @param proposalId The id of the proposal to queue + */ + function queue(uint256 proposalId) external { + ds.queue(proposalId); + } + + /** + * @notice Executes a queued proposal if eta has passed + * @param proposalId The id of the proposal to execute + */ + function execute(uint256 proposalId) external { + ds.execute(proposalId); + } + + /** + * @notice Executes a queued proposal on timelockV1 if eta has passed + * This is only required for proposal that were queued on timelockV1, but before the upgrade to DAO V3. + * These proposals will not have the `executeOnTimelockV1` bool turned on. + */ + function executeOnTimelockV1(uint256 proposalId) external { + ds.executeOnTimelockV1(proposalId); + } + + /** + * @notice Cancels a proposal only if sender is the proposer or a signer, or proposer & signers voting power + * dropped below proposal threshold + * @param proposalId The id of the proposal to cancel + */ + function cancel(uint256 proposalId) external { + ds.cancel(proposalId); + } + + /** + * @notice Gets the state of a proposal + * @param proposalId The id of the proposal + * @return Proposal state + */ + function state(uint256 proposalId) public view returns (ProposalState) { + return ds.state(proposalId); + } + + /** + * @notice Gets actions of a proposal + * @param proposalId the id of the proposal + * @return targets + * @return values + * @return signatures + * @return calldatas + */ + function getActions(uint256 proposalId) + external + view + returns ( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ) + { + return ds.getActions(proposalId); + } + + /** + * @notice Gets the receipt for a voter on a given proposal + * @param proposalId the id of proposal + * @param voter The address of the voter + * @return The voting receipt + */ + function getReceipt(uint256 proposalId, address voter) external view returns (Receipt memory) { + return ds.getReceipt(proposalId, voter); + } + + /** + * @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, backwards compatible with V1 and V2 + */ + function proposals(uint256 proposalId) external view returns (NounsDAOStorageV2.ProposalCondensed memory) { + return ds.proposals(proposalId); + } + + /** + * @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, not backwards compatible as it contains additional values + * like `objectionPeriodEndBlock` and `signers` + */ + function proposalsV3(uint256 proposalId) external view returns (ProposalCondensed memory) { + return ds.proposalsV3(proposalId); + } + + /** + * @notice Current proposal threshold using Noun Total Supply + * Differs from `GovernerBravo` which uses fixed amount + */ + function proposalThreshold() public view returns (uint256) { + return ds.proposalThreshold(ds.adjustedTotalSupply()); + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * DAO FORK + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Escrow Nouns to contribute to the fork threshold + * @dev Requires approving the tokenIds or the entire noun token to the DAO contract + * @param tokenIds the tokenIds to escrow. They will be sent to the DAO once the fork threshold is reached and the escrow is closed. + * @param proposalIds array of proposal ids which are the reason for wanting to fork. This will only be used to emit event. + * @param reason the reason for want to fork. This will only be used to emit event. + */ + function escrowToFork( + uint256[] calldata tokenIds, + uint256[] calldata proposalIds, + string calldata reason + ) external { + ds.escrowToFork(tokenIds, proposalIds, reason); + } + + /** + * @notice Withdraw Nouns from the fork escrow. Only possible if the fork has not been executed. + * Only allowed to withdraw tokens that the sender has escrowed. + * @param tokenIds the tokenIds to withdraw + */ + function withdrawFromForkEscrow(uint256[] calldata tokenIds) external { + ds.withdrawFromForkEscrow(tokenIds); + } + + /** + * @notice Execute the fork. Only possible if the fork threshold has been met. + * This will deploy a new DAO and send part of the treasury to the new DAO's treasury. + * This will also close the active escrow and all nouns in the escrow belong to the original DAO. + * @return forkTreasury The address of the new DAO's treasury + * @return forkToken The address of the new DAO's token + */ + function executeFork() external returns (address forkTreasury, address forkToken) { + return ds.executeFork(); + } + + /** + * @notice Joins a fork while a fork is active + * @param tokenIds the tokenIds to send to the DAO in exchange for joining the fork + * @param proposalIds array of proposal ids which are the reason for wanting to fork. This will only be used to emit event. + * @param reason the reason for want to fork. This will only be used to emit event. + */ + function joinFork( + uint256[] calldata tokenIds, + uint256[] calldata proposalIds, + string calldata reason + ) external { + ds.joinFork(tokenIds, proposalIds, reason); + } + + /** + * @notice Withdraws nouns from the fork escrow to the treasury after the fork has been executed + * @dev Only the DAO can call this function + * @param tokenIds the tokenIds to withdraw + */ + function withdrawDAONounsFromEscrowToTreasury(uint256[] calldata tokenIds) external { + ds.withdrawDAONounsFromEscrowToTreasury(tokenIds); + } + + /** + * @notice Withdraws nouns from the fork escrow after the fork has been executed to an address other than the treasury + * @dev Only the DAO can call this function + * @param tokenIds the tokenIds to withdraw + * @param to the address to send the nouns to + */ + function withdrawDAONounsFromEscrowIncreasingTotalSupply(uint256[] calldata tokenIds, address to) external { + ds.withdrawDAONounsFromEscrowIncreasingTotalSupply(tokenIds, to); + } + + /** + * @notice Returns the number of nouns in supply minus nouns owned by the DAO, i.e. held in the treasury or in an + * escrow after it has closed. + * This is used when calculating proposal threshold, quorum, fork threshold & treasury split. + */ + function adjustedTotalSupply() external view returns (uint256) { + return ds.adjustedTotalSupply(); + } + + /** + * @notice returns the required number of tokens to escrow to trigger a fork + */ + function forkThreshold() external view returns (uint256) { + return ds.forkThreshold(); + } + + /** + * @notice Returns the number of tokens currently in escrow, contributing to the fork threshold + */ + function numTokensInForkEscrow() external view returns (uint256) { + return ds.numTokensInForkEscrow(); + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * VOTES + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Vetoes a proposal only if sender is the vetoer and the proposal has not been executed. + * @param proposalId The id of the proposal to veto + */ + function veto(uint256 proposalId) external { + ds.veto(proposalId); + } + + /** + * @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 + */ + function castVote(uint256 proposalId, uint8 support) external { + ds.castVote(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 + * @dev Reentrancy is defended against in `castVoteInternal` at the `receipt.hasVoted == false` require statement. + */ + function castRefundableVote(uint256 proposalId, uint8 support) external { + ds.castRefundableVote(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 { + ds.castRefundableVoteWithReason(proposalId, support, reason); + } + + /** + * @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 + * @param reason The reason given for the vote by the voter + */ + function castVoteWithReason( + uint256 proposalId, + uint8 support, + string calldata reason + ) external { + ds.castVoteWithReason(proposalId, support, reason); + } + + /** + * @notice Cast a vote for a proposal by signature + * @dev External function that accepts EIP-712 signatures for voting on proposals. + */ + function castVoteBySig( + uint256 proposalId, + uint8 support, + uint8 v, + bytes32 r, + bytes32 s + ) external { + ds.castVoteBySig(proposalId, support, v, r, s); + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * ADMIN + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Admin function for setting the voting delay. Best to set voting delay to at least a few days, to give + * voters time to make sense of proposals, e.g. 21,600 blocks which should be at least 3 days. + * @param newVotingDelay new voting delay, in blocks + */ + function _setVotingDelay(uint256 newVotingDelay) external { + ds._setVotingDelay(newVotingDelay); + } + + /** + * @notice Admin function for setting the voting period + * @param newVotingPeriod new voting period, in blocks + */ + function _setVotingPeriod(uint256 newVotingPeriod) external { + ds._setVotingPeriod(newVotingPeriod); + } + + /** + * @notice Admin function for setting the proposal threshold basis points + * @dev newProposalThresholdBPS must be in [`MIN_PROPOSAL_THRESHOLD_BPS`,`MAX_PROPOSAL_THRESHOLD_BPS`] + * @param newProposalThresholdBPS new proposal threshold + */ + function _setProposalThresholdBPS(uint256 newProposalThresholdBPS) external { + ds._setProposalThresholdBPS(newProposalThresholdBPS); + } + + /** + * @notice Admin function for setting the objection period duration + * @param newObjectionPeriodDurationInBlocks new objection period duration, in blocks + */ + function _setObjectionPeriodDurationInBlocks(uint32 newObjectionPeriodDurationInBlocks) external { + ds._setObjectionPeriodDurationInBlocks(newObjectionPeriodDurationInBlocks); + } + + /** + * @notice Admin function for setting the objection period last minute window + * @param newLastMinuteWindowInBlocks new objection period last minute window, in blocks + */ + function _setLastMinuteWindowInBlocks(uint32 newLastMinuteWindowInBlocks) external { + ds._setLastMinuteWindowInBlocks(newLastMinuteWindowInBlocks); + } + + /** + * @notice Admin function for setting the proposal updatable period + * @param newProposalUpdatablePeriodInBlocks the new proposal updatable period, in blocks + */ + function _setProposalUpdatablePeriodInBlocks(uint32 newProposalUpdatablePeriodInBlocks) external { + ds._setProposalUpdatablePeriodInBlocks(newProposalUpdatablePeriodInBlocks); + } + + /** + * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @param newPendingAdmin New pending admin. + */ + function _setPendingAdmin(address newPendingAdmin) external { + ds._setPendingAdmin(newPendingAdmin); + } + + /** + * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin + * @dev Admin function for pending admin to accept role and update admin + */ + function _acceptAdmin() external { + ds._acceptAdmin(); + } + + /** + * @notice Begins transition of vetoer. The newPendingVetoer must call _acceptVetoer to finalize the transfer. + * @param newPendingVetoer New Pending Vetoer + */ + function _setPendingVetoer(address newPendingVetoer) public { + ds._setPendingVetoer(newPendingVetoer); + } + + /** + * @notice Called by the pendingVetoer to accept role and update vetoer + */ + function _acceptVetoer() external { + ds._acceptVetoer(); + } + + /** + * @notice Burns veto priviledges + * @dev Vetoer function destroying veto power forever + */ + function _burnVetoPower() public { + ds._burnVetoPower(); + } + + /** + * @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 { + ds._setMinQuorumVotesBPS(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 _setMaxQuorumVotesBPS(uint16 newMaxQuorumVotesBPS) external { + ds._setMaxQuorumVotesBPS(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 { + ds._setQuorumCoefficient(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 { + ds._setDynamicQuorumParams(newMinQuorumVotesBPS, newMaxQuorumVotesBPS, newQuorumCoefficient); + } + + /** + * @notice Withdraws all the ETH in the contract. This is callable only by the admin (timelock). + */ + function _withdraw() external returns (uint256, bool) { + return ds._withdraw(); + } + + /** + * @notice Admin function for setting the fork period + * @param newForkPeriod the new fork proposal period, in seconds + */ + function _setForkPeriod(uint256 newForkPeriod) external { + ds._setForkPeriod(newForkPeriod); + } + + /** + * @notice Admin function for setting the fork threshold + * @param newForkThresholdBPS the new fork proposal threshold, in basis points + */ + function _setForkThresholdBPS(uint256 newForkThresholdBPS) external { + ds._setForkThresholdBPS(newForkThresholdBPS); + } + + function _setNounAgeRequiredToFork(uint16 newNounAgeRequiredToFork) external { + ds._setNounAgeRequiredToFork(newNounAgeRequiredToFork); + } + + /** + * @notice Admin function for setting the proposal id at which vote snapshots start using the voting start block + * instead of the proposal creation block. + * Sets it to the next proposal id. + */ + function _setVoteSnapshotBlockSwitchProposalId() external { + ds._setVoteSnapshotBlockSwitchProposalId(); + } + + /** + * @notice Admin function for setting the fork DAO deployer contract + */ + function _setForkDAODeployer(address newForkDAODeployer) external { + ds._setForkDAODeployer(newForkDAODeployer); + } + + /** + * @notice Admin function for setting the ERC20 tokens that are used when splitting funds to a fork + */ + function _setErc20TokensToIncludeInFork(address[] calldata erc20tokens) external { + ds._setErc20TokensToIncludeInFork(erc20tokens); + } + + /** + * @notice Admin function for setting the fork escrow contract + */ + function _setForkEscrow(address newForkEscrow) external { + ds._setForkEscrow(newForkEscrow); + } + + /** + * @notice Admin function for setting the fork related parameters + * @param forkEscrow_ the fork escrow contract + * @param forkDAODeployer_ the fork dao deployer contract + * @param erc20TokensToIncludeInFork_ the ERC20 tokens used when splitting funds to a fork + * @param forkPeriod_ the period during which it's possible to join a fork after exeuction + * @param forkThresholdBPS_ the threshold required of escrowed nouns in order to execute a fork + */ + function _setForkParams( + address forkEscrow_, + address forkDAODeployer_, + address[] calldata erc20TokensToIncludeInFork_, + uint256 forkPeriod_, + uint256 forkThresholdBPS_ + ) external { + ds._setForkEscrow(forkEscrow_); + ds._setForkDAODeployer(forkDAODeployer_); + ds._setErc20TokensToIncludeInFork(erc20TokensToIncludeInFork_); + ds._setForkPeriod(forkPeriod_); + ds._setForkThresholdBPS(forkThresholdBPS_); + } + + /** + * @notice Admin function for setting the timelocks and admin + * @param newTimelock the new timelock contract + * @param newTimelockV1 the new timelockV1 contract + * @param newAdmin the new admin address + */ + function _setTimelocksAndAdmin( + address newTimelock, + address newTimelockV1, + address newAdmin + ) external { + ds._setTimelocksAndAdmin(newTimelock, newTimelockV1, newAdmin); + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * DYNAMIC QUORUM + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Quorum votes required for a specific proposal to succeed + * Differs from `GovernerBravo` which uses fixed amount + */ + function quorumVotes(uint256 proposalId) public view returns (uint256) { + return ds.quorumVotes(proposalId); + } + + /** + * @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 adjustedTotalSupply_ The adjusted 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 adjustedTotalSupply_, + DynamicQuorumParams memory params + ) public pure returns (uint256) { + return NounsDAOV3DynamicQuorumV1p1.dynamicQuorumVotes(againstVotes, adjustedTotalSupply_, params); + } + + /** + * @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) { + return ds.getDynamicQuorumParamsAt(blockNumber_); + } + + /** + * @notice Current min quorum votes using Nouns adjusted total supply + */ + function minQuorumVotes() public view returns (uint256) { + return ds.minQuorumVotes(ds.adjustedTotalSupply()); + } + + /** + * @notice Current max quorum votes using Nouns adjusted total supply + */ + function maxQuorumVotes() public view returns (uint256) { + return ds.maxQuorumVotes(ds.adjustedTotalSupply()); + } + + /** + * @notice Get all quorum params checkpoints + */ + function quorumParamsCheckpoints() public view returns (DynamicQuorumParamsCheckpoint[] memory) { + return ds.quorumParamsCheckpoints; + } + + /** + * @notice Get a quorum params checkpoint by its index + */ + function quorumParamsCheckpoints(uint256 index) public view returns (DynamicQuorumParamsCheckpoint memory) { + return ds.quorumParamsCheckpoints[index]; + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * STATE VARIABLE GETTERS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + function vetoer() public view returns (address) { + return ds.vetoer; + } + + function pendingVetoer() public view returns (address) { + return ds.pendingVetoer; + } + + function votingDelay() public view returns (uint256) { + return ds.votingDelay; + } + + function votingPeriod() public view returns (uint256) { + return ds.votingPeriod; + } + + function proposalThresholdBPS() public view returns (uint256) { + return ds.proposalThresholdBPS; + } + + function quorumVotesBPS() public view returns (uint256) { + return ds.quorumVotesBPS; + } + + function proposalCount() public view returns (uint256) { + return ds.proposalCount; + } + + function timelock() public view returns (INounsDAOExecutor) { + return ds.timelock; + } + + function nouns() public view returns (NounsTokenLike) { + return ds.nouns; + } + + function latestProposalIds(address account) public view returns (uint256) { + return ds.latestProposalIds[account]; + } + + function lastMinuteWindowInBlocks() public view returns (uint256) { + return ds.lastMinuteWindowInBlocks; + } + + function objectionPeriodDurationInBlocks() public view returns (uint256) { + return ds.objectionPeriodDurationInBlocks; + } + + function erc20TokensToIncludeInFork() public view returns (address[] memory) { + return ds.erc20TokensToIncludeInFork; + } + + function forkEscrow() public view returns (INounsDAOForkEscrow) { + return ds.forkEscrow; + } + + function forkDAODeployer() public view returns (IForkDAODeployer) { + return ds.forkDAODeployer; + } + + function forkEndTimestamp() public view returns (uint256) { + return ds.forkEndTimestamp; + } + + function forkPeriod() public view returns (uint256) { + return ds.forkPeriod; + } + + function forkThresholdBPS() public view returns (uint256) { + return ds.forkThresholdBPS; + } + + function proposalUpdatablePeriodInBlocks() public view returns (uint256) { + return ds.proposalUpdatablePeriodInBlocks; + } + + function timelockV1() public view returns (address) { + return address(ds.timelockV1); + } + + function voteSnapshotBlockSwitchProposalId() public view returns (uint256) { + return ds.voteSnapshotBlockSwitchProposalId; + } + + receive() external payable {} +} diff --git a/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOStorageV3p1.sol b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOStorageV3p1.sol new file mode 100644 index 0000000000..740740e155 --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOStorageV3p1.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Storage layout for DAO V3p1 + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { INounsDAOExecutorV2, NounsTokenLike, INounsDAOForkEscrow, IForkDAODeployer, INounsDAOExecutor } from '../NounsDAOInterfaces.sol'; + +contract NounsDAOStorageV3p1 { + StorageV3 ds; + + struct StorageV3 { + // ================ PROXY ================ // + /// @notice Administrator for this contract + address admin; + /// @notice Pending administrator for this contract + address pendingAdmin; + /// @notice Active brains of Governor + address implementation; + // ================ V1 ================ // + /// @notice Vetoer who has the ability to veto any proposal + address vetoer; + /// @notice The delay before voting on a proposal may take place, once proposed, in blocks + uint256 votingDelay; + /// @notice The duration of voting on a proposal, in blocks + uint256 votingPeriod; + /// @notice The basis point number of votes required in order for a voter to become a proposer. *DIFFERS from GovernerBravo + uint256 proposalThresholdBPS; + /// @notice The basis point number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed. *DIFFERS from GovernerBravo + uint256 quorumVotesBPS; + /// @notice The total number of proposals + uint256 proposalCount; + /// @notice The address of the Nouns DAO Executor NounsDAOExecutor + INounsDAOExecutorV2 timelock; + /// @notice The address of the Nouns tokens + NounsTokenLike nouns; + /// @notice The official record of all proposals ever proposed + mapping(uint256 => Proposal) _proposals; + /// @notice The latest proposal for each proposer + mapping(address => uint256) latestProposalIds; + // ================ V2 ================ // + DynamicQuorumParamsCheckpoint[] quorumParamsCheckpoints; + /// @notice Pending new vetoer + address pendingVetoer; + // ================ V3 ================ // + /// @notice user => sig => isCancelled: signatures that have been cancelled by the signer and are no longer valid + mapping(address => mapping(bytes32 => bool)) cancelledSigs; + /// @notice The number of blocks before voting ends during which the objection period can be initiated + uint32 lastMinuteWindowInBlocks; + /// @notice Length of the objection period in blocks + uint32 objectionPeriodDurationInBlocks; + /// @notice Length of proposal updatable period in block + uint32 proposalUpdatablePeriodInBlocks; + /// @notice address of the DAO's fork escrow contract + INounsDAOForkEscrow forkEscrow; + /// @notice address of the DAO's fork deployer contract + IForkDAODeployer forkDAODeployer; + /// @notice the age of a noun, in noun ids, for a noun to be allowed to fork + uint16 nounAgeRequiredToFork; + /// @notice ERC20 tokens to include when sending funds to a deployed fork + address[] erc20TokensToIncludeInFork; + /// @notice The treasury contract of the last deployed fork + address forkDAOTreasury; + /// @notice The token contract of the last deployed fork + address forkDAOToken; + /// @notice Timestamp at which the last fork period ends + uint256 forkEndTimestamp; + /// @notice Fork period in seconds + uint256 forkPeriod; + /// @notice Threshold defined in basis points (10,000 = 100%) required for forking + uint256 forkThresholdBPS; + /// @notice Address of the original timelock + INounsDAOExecutor timelockV1; + /// @notice The proposal at which to start using `startBlock` instead of `creationBlock` for vote snapshots + /// @dev Make sure this stays the last variable in this struct, so we can delete it in the next version + /// @dev To be zeroed-out and removed in a V3.1 fix version once the switch takes place + uint256 voteSnapshotBlockSwitchProposalId; + } + + struct Proposal { + /// @notice Unique id for looking up a proposal + uint256 id; + /// @notice Creator of the proposal + address proposer; + /// @notice The number of votes needed to create a proposal at the time of proposal creation. *DIFFERS from GovernerBravo + uint256 proposalThreshold; + /// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed at the time of proposal creation. *DIFFERS from GovernerBravo + uint256 quorumVotes; + /// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds + uint256 eta; + /// @notice the ordered list of target addresses for calls to be made + address[] targets; + /// @notice The ordered list of values (i.e. msg.value) to be passed to the calls to be made + uint256[] values; + /// @notice The ordered list of function signatures to be called + string[] signatures; + /// @notice The ordered list of calldata to be passed to each call + bytes[] calldatas; + /// @notice The block at which voting begins: holders must delegate their votes prior to this block + uint256 startBlock; + /// @notice The block at which voting ends: votes must be cast prior to this block + uint256 endBlock; + /// @notice Current number of votes in favor of this proposal + uint256 forVotes; + /// @notice Current number of votes in opposition to this proposal + uint256 againstVotes; + /// @notice Current number of votes for abstaining for this proposal + uint256 abstainVotes; + /// @notice Flag marking whether the proposal has been canceled + bool canceled; + /// @notice Flag marking whether the proposal has been vetoed + bool vetoed; + /// @notice Flag marking whether the proposal has been executed + bool executed; + /// @notice Receipts of ballots for the entire set of voters + mapping(address => Receipt) receipts; + /// @notice The total supply at the time of proposal creation + uint256 totalSupply; + /// @notice The block at which this proposal was created + uint64 creationBlock; + /// @notice The last block which allows updating a proposal's description and transactions + uint64 updatePeriodEndBlock; + /// @notice Starts at 0 and is set to the block at which the objection period ends when the objection period is initiated + uint64 objectionPeriodEndBlock; + /// @dev unused for now + uint64 placeholder; + /// @notice The signers of a proposal, when using proposeBySigs + address[] signers; + /// @notice When true, a proposal would be executed on timelockV1 instead of the current timelock + bool executeOnTimelockV1; + } + + /// @notice Ballot receipt record for a voter + struct Receipt { + /// @notice Whether or not a vote has been cast + bool hasVoted; + /// @notice Whether or not the voter supports the proposal or abstains + uint8 support; + /// @notice The number of votes the voter had, which were cast + uint96 votes; + } + + struct ProposerSignature { + /// @notice Signature of a proposal + bytes sig; + /// @notice The address of the signer + address signer; + /// @notice The timestamp until which the signature is valid + uint256 expirationTimestamp; + } + + struct ProposalCondensed { + /// @notice Unique id for looking up a proposal + uint256 id; + /// @notice Creator of the proposal + address proposer; + /// @notice The number of votes needed to create a proposal at the time of proposal creation. *DIFFERS from GovernerBravo + uint256 proposalThreshold; + /// @notice The minimum number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed at the time of proposal creation. *DIFFERS from GovernerBravo + uint256 quorumVotes; + /// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds + uint256 eta; + /// @notice The block at which voting begins: holders must delegate their votes prior to this block + uint256 startBlock; + /// @notice The block at which voting ends: votes must be cast prior to this block + uint256 endBlock; + /// @notice Current number of votes in favor of this proposal + uint256 forVotes; + /// @notice Current number of votes in opposition to this proposal + uint256 againstVotes; + /// @notice Current number of votes for abstaining for this proposal + uint256 abstainVotes; + /// @notice Flag marking whether the proposal has been canceled + bool canceled; + /// @notice Flag marking whether the proposal has been vetoed + bool vetoed; + /// @notice Flag marking whether the proposal has been executed + bool executed; + /// @notice The total supply at the time of proposal creation + uint256 totalSupply; + /// @notice The block at which this proposal was created + uint256 creationBlock; + /// @notice The signers of a proposal, when using proposeBySigs + address[] signers; + /// @notice The last block which allows updating a proposal's description and transactions + uint256 updatePeriodEndBlock; + /// @notice Starts at 0 and is set to the block at which the objection period ends when the objection period is initiated + uint256 objectionPeriodEndBlock; + /// @notice When true, a proposal would be executed on timelockV1 instead of the current timelock + bool executeOnTimelockV1; + } + + struct DynamicQuorumParams { + /// @notice The minimum basis point number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed. + uint16 minQuorumVotesBPS; + /// @notice The maximum basis point number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed. + uint16 maxQuorumVotesBPS; + /// @notice The dynamic quorum coefficient + /// @dev Assumed to be fixed point integer with 6 decimals, i.e 0.2 is represented as 0.2 * 1e6 = 200000 + uint32 quorumCoefficient; + } + + struct NounsDAOParams { + uint256 votingPeriod; + uint256 votingDelay; + uint256 proposalThresholdBPS; + uint32 lastMinuteWindowInBlocks; + uint32 objectionPeriodDurationInBlocks; + uint32 proposalUpdatablePeriodInBlocks; + } + + /// @notice A checkpoint for storing dynamic quorum params from a given block + struct DynamicQuorumParamsCheckpoint { + /// @notice The block at which the new values were set + uint32 fromBlock; + /// @notice The parameter values of this checkpoint + DynamicQuorumParams params; + } + + /// @notice Possible states that a proposal may be in + enum ProposalState { + Pending, + Active, + Canceled, + Defeated, + Succeeded, + Queued, + Expired, + Executed, + Vetoed, + ObjectionPeriod, + Updatable + } +} diff --git a/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3AdminV1p1.sol b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3AdminV1p1.sol new file mode 100644 index 0000000000..9a3a6f9a4c --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3AdminV1p1.sol @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Library for NounsDAOLogicV3 contract containing admin related functions + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { NounsDAOStorageV3p1 } from './NounsDAOStorageV3p1.sol'; +import { NounsDAOV3DynamicQuorumV1p1 } from './NounsDAOV3DynamicQuorumV1p1.sol'; +import { IForkDAODeployer, INounsDAOForkEscrow, INounsDAOExecutor, INounsDAOExecutorV2 } from '../NounsDAOInterfaces.sol'; + +library NounsDAOV3AdminV1p1 { + using NounsDAOV3DynamicQuorumV1p1 for NounsDAOStorageV3p1.StorageV3; + + error AdminOnly(); + error VetoerOnly(); + error PendingVetoerOnly(); + error InvalidMinQuorumVotesBPS(); + error InvalidMaxQuorumVotesBPS(); + error MinQuorumBPSGreaterThanMaxQuorumBPS(); + error ForkPeriodTooLong(); + error ForkPeriodTooShort(); + error InvalidObjectionPeriodDurationInBlocks(); + error InvalidProposalUpdatablePeriodInBlocks(); + error VoteSnapshotSwitchAlreadySet(); + error DuplicateTokenAddress(); + + /// @notice Emitted when proposal threshold basis points is set + event ProposalThresholdBPSSet(uint256 oldProposalThresholdBPS, uint256 newProposalThresholdBPS); + + /// @notice An event emitted when the voting delay is set + event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay); + + /// @notice An event emitted when the voting period is set + event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); + + /// @notice An event emitted when the objection period duration is set + event ObjectionPeriodDurationSet( + uint32 oldObjectionPeriodDurationInBlocks, + uint32 newObjectionPeriodDurationInBlocks + ); + + /// @notice An event emitted when the objection period last minute window is set + event LastMinuteWindowSet(uint32 oldLastMinuteWindowInBlocks, uint32 newLastMinuteWindowInBlocks); + + /// @notice An event emitted when the proposal updatable period is set + event ProposalUpdatablePeriodSet( + uint32 oldProposalUpdatablePeriodInBlocks, + uint32 newProposalUpdatablePeriodInBlocks + ); + + /// @notice Emitted when pendingAdmin is changed + event NewPendingAdmin(address oldPendingAdmin, address newPendingAdmin); + + /// @notice Emitted when pendingAdmin is accepted, which means admin is updated + event NewAdmin(address oldAdmin, address newAdmin); + + /// @notice Emitted when pendingVetoer is changed + event NewPendingVetoer(address oldPendingVetoer, address newPendingVetoer); + + /// @notice Emitted when vetoer is changed + event NewVetoer(address oldVetoer, address newVetoer); + + /// @notice Emitted when minQuorumVotesBPS is set + event MinQuorumVotesBPSSet(uint16 oldMinQuorumVotesBPS, uint16 newMinQuorumVotesBPS); + + /// @notice Emitted when maxQuorumVotesBPS is set + event MaxQuorumVotesBPSSet(uint16 oldMaxQuorumVotesBPS, uint16 newMaxQuorumVotesBPS); + + /// @notice Emitted when quorumCoefficient is set + event QuorumCoefficientSet(uint32 oldQuorumCoefficient, uint32 newQuorumCoefficient); + + /// @notice Emitted when admin withdraws the DAO's balance. + event Withdraw(uint256 amount, bool sent); + + /// @notice Emitted when the proposal id at which vote snapshot block changes is set + event VoteSnapshotBlockSwitchProposalIdSet( + uint256 oldVoteSnapshotBlockSwitchProposalId, + uint256 newVoteSnapshotBlockSwitchProposalId + ); + + /// @notice Emitted when the fork DAO deployer is set + event ForkDAODeployerSet(address oldForkDAODeployer, address newForkDAODeployer); + + /// @notice Emitted when the erc20 tokens to include in a fork are set + event ERC20TokensToIncludeInForkSet(address[] oldErc20Tokens, address[] newErc20tokens); + + /// @notice Emitted when the fork escrow contract address is set + event ForkEscrowSet(address oldForkEscrow, address newForkEscrow); + + /// @notice Emitted when the during of the forking period is set + event ForkPeriodSet(uint256 oldForkPeriod, uint256 newForkPeriod); + + /// @notice Emitted when the threhsold for forking is set + event ForkThresholdSet(uint256 oldForkThreshold, uint256 newForkThreshold); + + /// @notice Emitted when the main timelock, timelockV1 and admin are set + event TimelocksAndAdminSet(address timelock, address timelockV1, address admin); + + event NounAgeRequiredToForkSet(uint16 oldNounAgeRequiredToFork, uint16 newNounAgeRequiredToFork); + + /// @notice The minimum setable proposal threshold + uint256 public constant MIN_PROPOSAL_THRESHOLD_BPS = 1; // 1 basis point or 0.01% + + /// @notice The maximum setable proposal threshold + uint256 public constant MAX_PROPOSAL_THRESHOLD_BPS = 1_000; // 1,000 basis points or 10% + + /// @notice The minimum setable voting period in blocks + uint256 public constant MIN_VOTING_PERIOD_BLOCKS = 1 days / 12; + + /// @notice The max setable voting period in blocks + uint256 public constant MAX_VOTING_PERIOD_BLOCKS = 2 weeks / 12; + + /// @notice The min setable voting delay in blocks + uint256 public constant MIN_VOTING_DELAY_BLOCKS = 1; + + /// @notice The max setable voting delay in blocks + uint256 public constant MAX_VOTING_DELAY_BLOCKS = 2 weeks / 12; + + /// @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; // 6,000 basis points or 60% + + /// @notice Upper bound for forking period. If forking period is too high it can block proposals for too long. + uint256 public constant MAX_FORK_PERIOD = 14 days; + + /// @notice Lower bound for forking period + uint256 public constant MIN_FORK_PERIOD = 2 days; + + /// @notice Upper bound for objection period duration in blocks. + uint256 public constant MAX_OBJECTION_PERIOD_BLOCKS = 7 days / 12; + + /// @notice Upper bound for proposal updatable period duration in blocks. + uint256 public constant MAX_UPDATABLE_PERIOD_BLOCKS = 7 days / 12; + + modifier onlyAdmin(NounsDAOStorageV3p1.StorageV3 storage ds) { + if (msg.sender != ds.admin) { + revert AdminOnly(); + } + _; + } + + /** + * @notice Admin function for setting the voting delay. Best to set voting delay to at least a few days, to give + * voters time to make sense of proposals, e.g. 21,600 blocks which should be at least 3 days. + * @param newVotingDelay new voting delay, in blocks + */ + function _setVotingDelay(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 newVotingDelay) external onlyAdmin(ds) { + require( + newVotingDelay >= MIN_VOTING_DELAY_BLOCKS && newVotingDelay <= MAX_VOTING_DELAY_BLOCKS, + 'NounsDAO::_setVotingDelay: invalid voting delay' + ); + uint256 oldVotingDelay = ds.votingDelay; + ds.votingDelay = newVotingDelay; + + emit VotingDelaySet(oldVotingDelay, newVotingDelay); + } + + /** + * @notice Admin function for setting the voting period + * @param newVotingPeriod new voting period, in blocks + */ + function _setVotingPeriod(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 newVotingPeriod) + external + onlyAdmin(ds) + { + require( + newVotingPeriod >= MIN_VOTING_PERIOD_BLOCKS && newVotingPeriod <= MAX_VOTING_PERIOD_BLOCKS, + 'NounsDAO::_setVotingPeriod: invalid voting period' + ); + uint256 oldVotingPeriod = ds.votingPeriod; + ds.votingPeriod = newVotingPeriod; + + emit VotingPeriodSet(oldVotingPeriod, newVotingPeriod); + } + + /** + * @notice Admin function for setting the proposal threshold basis points + * @dev newProposalThresholdBPS must be in [`MIN_PROPOSAL_THRESHOLD_BPS`,`MAX_PROPOSAL_THRESHOLD_BPS`] + * @param newProposalThresholdBPS new proposal threshold + */ + function _setProposalThresholdBPS(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 newProposalThresholdBPS) + external + onlyAdmin(ds) + { + require( + newProposalThresholdBPS >= MIN_PROPOSAL_THRESHOLD_BPS && + newProposalThresholdBPS <= MAX_PROPOSAL_THRESHOLD_BPS, + 'NounsDAO::_setProposalThreshold: invalid proposal threshold bps' + ); + uint256 oldProposalThresholdBPS = ds.proposalThresholdBPS; + ds.proposalThresholdBPS = newProposalThresholdBPS; + + emit ProposalThresholdBPSSet(oldProposalThresholdBPS, newProposalThresholdBPS); + } + + /** + * @notice Admin function for setting the objection period duration + * @param newObjectionPeriodDurationInBlocks new objection period duration, in blocks + */ + function _setObjectionPeriodDurationInBlocks( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint32 newObjectionPeriodDurationInBlocks + ) external onlyAdmin(ds) { + if (newObjectionPeriodDurationInBlocks > MAX_OBJECTION_PERIOD_BLOCKS) + revert InvalidObjectionPeriodDurationInBlocks(); + + uint32 oldObjectionPeriodDurationInBlocks = ds.objectionPeriodDurationInBlocks; + ds.objectionPeriodDurationInBlocks = newObjectionPeriodDurationInBlocks; + + emit ObjectionPeriodDurationSet(oldObjectionPeriodDurationInBlocks, newObjectionPeriodDurationInBlocks); + } + + /** + * @notice Admin function for setting the objection period last minute window + * @param newLastMinuteWindowInBlocks new objection period last minute window, in blocks + */ + function _setLastMinuteWindowInBlocks(NounsDAOStorageV3p1.StorageV3 storage ds, uint32 newLastMinuteWindowInBlocks) + external + onlyAdmin(ds) + { + uint32 oldLastMinuteWindowInBlocks = ds.lastMinuteWindowInBlocks; + ds.lastMinuteWindowInBlocks = newLastMinuteWindowInBlocks; + + emit LastMinuteWindowSet(oldLastMinuteWindowInBlocks, newLastMinuteWindowInBlocks); + } + + /** + * @notice Admin function for setting the proposal updatable period + * @param newProposalUpdatablePeriodInBlocks the new proposal updatable period, in blocks + */ + function _setProposalUpdatablePeriodInBlocks( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint32 newProposalUpdatablePeriodInBlocks + ) external onlyAdmin(ds) { + if (newProposalUpdatablePeriodInBlocks > MAX_UPDATABLE_PERIOD_BLOCKS) + revert InvalidProposalUpdatablePeriodInBlocks(); + + uint32 oldProposalUpdatablePeriodInBlocks = ds.proposalUpdatablePeriodInBlocks; + ds.proposalUpdatablePeriodInBlocks = newProposalUpdatablePeriodInBlocks; + + emit ProposalUpdatablePeriodSet(oldProposalUpdatablePeriodInBlocks, newProposalUpdatablePeriodInBlocks); + } + + /** + * @notice Begins transfer of admin rights. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @dev Admin function to begin change of admin. The newPendingAdmin must call `_acceptAdmin` to finalize the transfer. + * @param newPendingAdmin New pending admin. + */ + function _setPendingAdmin(NounsDAOStorageV3p1.StorageV3 storage ds, address newPendingAdmin) + external + onlyAdmin(ds) + { + // Save current value, if any, for inclusion in log + address oldPendingAdmin = ds.pendingAdmin; + + // Store pendingAdmin with value newPendingAdmin + ds.pendingAdmin = newPendingAdmin; + + // Emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin) + emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); + } + + /** + * @notice Accepts transfer of admin rights. msg.sender must be pendingAdmin + * @dev Admin function for pending admin to accept role and update admin + */ + function _acceptAdmin(NounsDAOStorageV3p1.StorageV3 storage ds) external { + // Check caller is pendingAdmin and pendingAdmin ≠ address(0) + require( + msg.sender == ds.pendingAdmin && msg.sender != address(0), + 'NounsDAO::_acceptAdmin: pending admin only' + ); + + // Save current values for inclusion in log + address oldAdmin = ds.admin; + address oldPendingAdmin = ds.pendingAdmin; + + // Store admin with value pendingAdmin + ds.admin = ds.pendingAdmin; + + // Clear the pending value + ds.pendingAdmin = address(0); + + emit NewAdmin(oldAdmin, ds.admin); + emit NewPendingAdmin(oldPendingAdmin, address(0)); + } + + /** + * @notice Begins transition of vetoer. The newPendingVetoer must call _acceptVetoer to finalize the transfer. + * @param newPendingVetoer New Pending Vetoer + */ + function _setPendingVetoer(NounsDAOStorageV3p1.StorageV3 storage ds, address newPendingVetoer) public { + if (msg.sender != ds.vetoer) { + revert VetoerOnly(); + } + + emit NewPendingVetoer(ds.pendingVetoer, newPendingVetoer); + + ds.pendingVetoer = newPendingVetoer; + } + + /** + * @notice Called by the pendingVetoer to accept role and update vetoer + */ + function _acceptVetoer(NounsDAOStorageV3p1.StorageV3 storage ds) external { + if (msg.sender != ds.pendingVetoer) { + revert PendingVetoerOnly(); + } + + // Update vetoer + emit NewVetoer(ds.vetoer, ds.pendingVetoer); + ds.vetoer = ds.pendingVetoer; + + // Clear the pending value + emit NewPendingVetoer(ds.pendingVetoer, address(0)); + ds.pendingVetoer = address(0); + } + + /** + * @notice Burns veto priviledges + * @dev Vetoer function destroying veto power forever + */ + function _burnVetoPower(NounsDAOStorageV3p1.StorageV3 storage ds) public { + // Check caller is vetoer + require(msg.sender == ds.vetoer, 'NounsDAO::_burnVetoPower: vetoer only'); + + // Update vetoer to 0x0 + emit NewVetoer(ds.vetoer, address(0)); + ds.vetoer = address(0); + + // Clear the pending value + emit NewPendingVetoer(ds.pendingVetoer, address(0)); + ds.pendingVetoer = address(0); + } + + /** + * @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(NounsDAOStorageV3p1.StorageV3 storage ds, uint16 newMinQuorumVotesBPS) + external + onlyAdmin(ds) + { + NounsDAOStorageV3p1.DynamicQuorumParams memory params = ds.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(ds, 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 _setMaxQuorumVotesBPS(NounsDAOStorageV3p1.StorageV3 storage ds, uint16 newMaxQuorumVotesBPS) + external + onlyAdmin(ds) + { + NounsDAOStorageV3p1.DynamicQuorumParams memory params = ds.getDynamicQuorumParamsAt(block.number); + + require( + newMaxQuorumVotesBPS <= MAX_QUORUM_VOTES_BPS_UPPER_BOUND, + 'NounsDAO::_setMaxQuorumVotesBPS: invalid max quorum votes bps' + ); + require( + params.minQuorumVotesBPS <= newMaxQuorumVotesBPS, + 'NounsDAO::_setMaxQuorumVotesBPS: min quorum votes bps greater than max' + ); + + uint16 oldMaxQuorumVotesBPS = params.maxQuorumVotesBPS; + params.maxQuorumVotesBPS = newMaxQuorumVotesBPS; + + _writeQuorumParamsCheckpoint(ds, 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(NounsDAOStorageV3p1.StorageV3 storage ds, uint32 newQuorumCoefficient) + external + onlyAdmin(ds) + { + NounsDAOStorageV3p1.DynamicQuorumParams memory params = ds.getDynamicQuorumParamsAt(block.number); + + uint32 oldQuorumCoefficient = params.quorumCoefficient; + params.quorumCoefficient = newQuorumCoefficient; + + _writeQuorumParamsCheckpoint(ds, 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( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint16 newMinQuorumVotesBPS, + uint16 newMaxQuorumVotesBPS, + uint32 newQuorumCoefficient + ) public onlyAdmin(ds) { + 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(); + } + + NounsDAOStorageV3p1.DynamicQuorumParams memory oldParams = ds.getDynamicQuorumParamsAt(block.number); + + NounsDAOStorageV3p1.DynamicQuorumParams memory params = NounsDAOStorageV3p1.DynamicQuorumParams({ + minQuorumVotesBPS: newMinQuorumVotesBPS, + maxQuorumVotesBPS: newMaxQuorumVotesBPS, + quorumCoefficient: newQuorumCoefficient + }); + _writeQuorumParamsCheckpoint(ds, params); + + emit MinQuorumVotesBPSSet(oldParams.minQuorumVotesBPS, params.minQuorumVotesBPS); + emit MaxQuorumVotesBPSSet(oldParams.maxQuorumVotesBPS, params.maxQuorumVotesBPS); + emit QuorumCoefficientSet(oldParams.quorumCoefficient, params.quorumCoefficient); + } + + /** + * @notice Withdraws all the ETH in the contract. This is callable only by the admin (timelock). + */ + function _withdraw(NounsDAOStorageV3p1.StorageV3 storage ds) external onlyAdmin(ds) returns (uint256, bool) { + uint256 amount = address(this).balance; + (bool sent, ) = msg.sender.call{ value: amount }(''); + + emit Withdraw(amount, sent); + + return (amount, sent); + } + + /** + * @notice Admin function for setting the proposal id at which vote snapshots start using the voting start block + * instead of the proposal creation block. + * Sets it to the next proposal id. + */ + function _setVoteSnapshotBlockSwitchProposalId(NounsDAOStorageV3p1.StorageV3 storage ds) external onlyAdmin(ds) { + uint256 oldVoteSnapshotBlockSwitchProposalId = ds.voteSnapshotBlockSwitchProposalId; + if (oldVoteSnapshotBlockSwitchProposalId > 0) { + revert VoteSnapshotSwitchAlreadySet(); + } + + uint256 newVoteSnapshotBlockSwitchProposalId = ds.proposalCount + 1; + ds.voteSnapshotBlockSwitchProposalId = newVoteSnapshotBlockSwitchProposalId; + + emit VoteSnapshotBlockSwitchProposalIdSet( + oldVoteSnapshotBlockSwitchProposalId, + newVoteSnapshotBlockSwitchProposalId + ); + } + + /** + * @notice Admin function for setting the fork DAO deployer contract + */ + function _setForkDAODeployer(NounsDAOStorageV3p1.StorageV3 storage ds, address newForkDAODeployer) + external + onlyAdmin(ds) + { + address oldForkDAODeployer = address(ds.forkDAODeployer); + ds.forkDAODeployer = IForkDAODeployer(newForkDAODeployer); + + emit ForkDAODeployerSet(oldForkDAODeployer, newForkDAODeployer); + } + + /** + * @notice Admin function for setting the ERC20 tokens that are used when splitting funds to a fork + */ + function _setErc20TokensToIncludeInFork(NounsDAOStorageV3p1.StorageV3 storage ds, address[] calldata erc20tokens) + external + onlyAdmin(ds) + { + checkForDuplicates(erc20tokens); + + emit ERC20TokensToIncludeInForkSet(ds.erc20TokensToIncludeInFork, erc20tokens); + + ds.erc20TokensToIncludeInFork = erc20tokens; + } + + /** + * @notice Admin function for setting the fork escrow contract + */ + function _setForkEscrow(NounsDAOStorageV3p1.StorageV3 storage ds, address newForkEscrow) external onlyAdmin(ds) { + emit ForkEscrowSet(address(ds.forkEscrow), newForkEscrow); + + ds.forkEscrow = INounsDAOForkEscrow(newForkEscrow); + } + + function _setForkPeriod(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 newForkPeriod) external onlyAdmin(ds) { + if (newForkPeriod > MAX_FORK_PERIOD) { + revert ForkPeriodTooLong(); + } + + if (newForkPeriod < MIN_FORK_PERIOD) { + revert ForkPeriodTooShort(); + } + + emit ForkPeriodSet(ds.forkPeriod, newForkPeriod); + + ds.forkPeriod = newForkPeriod; + } + + /** + * @notice Admin function for setting the fork threshold + * @param newForkThresholdBPS the new fork proposal threshold, in basis points + */ + function _setForkThresholdBPS(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 newForkThresholdBPS) + external + onlyAdmin(ds) + { + emit ForkThresholdSet(ds.forkThresholdBPS, newForkThresholdBPS); + + ds.forkThresholdBPS = newForkThresholdBPS; + } + + function _setNounAgeRequiredToFork(NounsDAOStorageV3p1.StorageV3 storage ds, uint16 newNounAgeRequiredToFork) + external + onlyAdmin(ds) + { + emit NounAgeRequiredToForkSet(ds.nounAgeRequiredToFork, newNounAgeRequiredToFork); + + ds.nounAgeRequiredToFork = newNounAgeRequiredToFork; + } + + /** + * @notice Admin function for setting the timelocks and admin + * @param timelock the new timelock contract + * @param timelockV1 the new timelockV1 contract + * @param admin the new admin address + */ + function _setTimelocksAndAdmin( + NounsDAOStorageV3p1.StorageV3 storage ds, + address timelock, + address timelockV1, + address admin + ) external onlyAdmin(ds) { + ds.timelock = INounsDAOExecutorV2(timelock); + ds.timelockV1 = INounsDAOExecutor(timelockV1); + ds.admin = admin; + + emit TimelocksAndAdminSet(timelock, timelockV1, admin); + } + + function _writeQuorumParamsCheckpoint( + NounsDAOStorageV3p1.StorageV3 storage ds, + NounsDAOStorageV3p1.DynamicQuorumParams memory params + ) internal { + uint32 blockNumber = safe32(block.number, 'block number exceeds 32 bits'); + uint256 pos = ds.quorumParamsCheckpoints.length; + if (pos > 0 && ds.quorumParamsCheckpoints[pos - 1].fromBlock == blockNumber) { + ds.quorumParamsCheckpoints[pos - 1].params = params; + } else { + ds.quorumParamsCheckpoints.push( + NounsDAOStorageV3p1.DynamicQuorumParamsCheckpoint({ fromBlock: blockNumber, params: params }) + ); + } + } + + function safe32(uint256 n, string memory errorMessage) internal pure returns (uint32) { + require(n <= type(uint32).max, errorMessage); + return uint32(n); + } + + function checkForDuplicates(address[] calldata erc20tokens) internal pure { + if (erc20tokens.length == 0) return; + + for (uint256 i = 0; i < erc20tokens.length - 1; i++) { + for (uint256 j = i + 1; j < erc20tokens.length; j++) { + if (erc20tokens[i] == erc20tokens[j]) revert DuplicateTokenAddress(); + } + } + } +} diff --git a/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3DynamicQuorumV1p1.sol b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3DynamicQuorumV1p1.sol new file mode 100644 index 0000000000..4022abd896 --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3DynamicQuorumV1p1.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Library for NounsDAOLogicV3 contract containing functions related to quorum calculations + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { NounsDAOStorageV3p1 } from './NounsDAOStorageV3p1.sol'; +import { NounsDAOV3ForkV1p1 } from './NounsDAOV3ForkV1p1.sol'; + +library NounsDAOV3DynamicQuorumV1p1 { + using NounsDAOV3ForkV1p1 for NounsDAOStorageV3p1.StorageV3; + + error UnsafeUint16Cast(); + + /** + * @notice Quorum votes required for a specific proposal to succeed + * Differs from `GovernerBravo` which uses fixed amount + */ + function quorumVotes(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) internal view returns (uint256) { + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + if (proposal.totalSupply == 0) { + return proposal.quorumVotes; + } + + return + dynamicQuorumVotes( + proposal.againstVotes, + proposal.totalSupply, + getDynamicQuorumParamsAt(ds, 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, + NounsDAOStorageV3p1.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(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 blockNumber_) + internal + view + returns (NounsDAOStorageV3p1.DynamicQuorumParams memory) + { + uint32 blockNumber = safe32(blockNumber_, 'NounsDAO::getDynamicQuorumParamsAt: block number exceeds 32 bits'); + uint256 len = ds.quorumParamsCheckpoints.length; + + if (len == 0) { + return + NounsDAOStorageV3p1.DynamicQuorumParams({ + minQuorumVotesBPS: safe16(ds.quorumVotesBPS), + maxQuorumVotesBPS: safe16(ds.quorumVotesBPS), + quorumCoefficient: 0 + }); + } + + if (ds.quorumParamsCheckpoints[len - 1].fromBlock <= blockNumber) { + return ds.quorumParamsCheckpoints[len - 1].params; + } + + if (ds.quorumParamsCheckpoints[0].fromBlock > blockNumber) { + return + NounsDAOStorageV3p1.DynamicQuorumParams({ + minQuorumVotesBPS: safe16(ds.quorumVotesBPS), + maxQuorumVotesBPS: safe16(ds.quorumVotesBPS), + quorumCoefficient: 0 + }); + } + + uint256 lower = 0; + uint256 upper = len - 1; + while (upper > lower) { + uint256 center = upper - (upper - lower) / 2; + NounsDAOStorageV3p1.DynamicQuorumParamsCheckpoint memory cp = ds.quorumParamsCheckpoints[center]; + if (cp.fromBlock == blockNumber) { + return cp.params; + } else if (cp.fromBlock < blockNumber) { + lower = center; + } else { + upper = center - 1; + } + } + return ds.quorumParamsCheckpoints[lower].params; + } + + /** + * @notice Current min quorum votes using Nouns adjusted total supply + */ + function minQuorumVotes(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 adjustedTotalSupply) + internal + view + returns (uint256) + { + return bps2Uint(getDynamicQuorumParamsAt(ds, block.number).minQuorumVotesBPS, adjustedTotalSupply); + } + + /** + * @notice Current max quorum votes using Nouns adjusted total supply + */ + function maxQuorumVotes(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 adjustedTotalSupply) + internal + view + returns (uint256) + { + return bps2Uint(getDynamicQuorumParamsAt(ds, block.number).maxQuorumVotesBPS, adjustedTotalSupply); + } + + 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); + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + function bps2Uint(uint256 bps, uint256 number) internal pure returns (uint256) { + return (number * bps) / 10000; + } +} diff --git a/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3ForkV1p1.sol b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3ForkV1p1.sol new file mode 100644 index 0000000000..c3ca5ae544 --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3ForkV1p1.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Library for NounsDAOLogicV3 contract containing the dao fork logic + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { INounsDAOForkEscrow, INounsDAOExecutorV2 } from '../NounsDAOInterfaces.sol'; +import { NounsDAOStorageV3p1 } from './NounsDAOStorageV3p1.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { NounsTokenFork } from '../fork/newdao/token/NounsTokenFork.sol'; +import { INounsAuctionHouseV2 } from '../../interfaces/INounsAuctionHouseV2.sol'; + +library NounsDAOV3ForkV1p1 { + error ForkThresholdNotMet(); + error ForkPeriodNotActive(); + error ForkPeriodActive(); + error AdminOnly(); + error UseAlternativeWithdrawFunction(); + error NounIdNotOldEnough(); + + /// @notice Emitted when someones adds nouns to the fork escrow + event EscrowedToFork( + uint32 indexed forkId, + address indexed owner, + uint256[] tokenIds, + uint256[] proposalIds, + string reason + ); + + /// @notice Emitted when the owner withdraws their nouns from the fork escrow + event WithdrawFromForkEscrow(uint32 indexed forkId, address indexed owner, uint256[] tokenIds); + + /// @notice Emitted when the fork is executed and the forking period begins + event ExecuteFork( + uint32 indexed forkId, + address forkTreasury, + address forkToken, + uint256 forkEndTimestamp, + uint256 tokensInEscrow + ); + + /// @notice Emitted when someone joins a fork during the forking period + event JoinFork( + uint32 indexed forkId, + address indexed owner, + uint256[] tokenIds, + uint256[] proposalIds, + string reason + ); + + /// @notice Emitted when the DAO withdraws nouns from the fork escrow after a fork has been executed + event DAOWithdrawNounsFromEscrow(uint256[] tokenIds, address to); + + /// @notice Emitted when withdrawing nouns from escrow increases adjusted total supply + event DAONounsSupplyIncreasedFromEscrow(uint256 numTokens, address to); + + /** + * @notice Escrow Nouns to contribute to the fork threshold + * @dev Requires approving the tokenIds or the entire noun token to the DAO contract + * @param tokenIds the tokenIds to escrow. They will be sent to the DAO once the fork threshold is reached and the escrow is closed. + * @param proposalIds array of proposal ids which are the reason for wanting to fork. This will only be used to emit event. + * @param reason the reason for want to fork. This will only be used to emit event. + */ + function escrowToFork( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256[] calldata tokenIds, + uint256[] calldata proposalIds, + string calldata reason + ) external { + if (isForkPeriodActive(ds)) revert ForkPeriodActive(); + INounsDAOForkEscrow forkEscrow = ds.forkEscrow; + + for (uint256 i = 0; i < tokenIds.length; i++) { + checkNounIdIsAllowedToFork(ds, tokenIds[i]); + ds.nouns.safeTransferFrom(msg.sender, address(forkEscrow), tokenIds[i]); + } + + emit EscrowedToFork(forkEscrow.forkId(), msg.sender, tokenIds, proposalIds, reason); + } + + /** + * @notice Withdraw Nouns from the fork escrow. Only possible if the fork has not been executed. + * Only allowed to withdraw tokens that the sender has escrowed. + * @param tokenIds the tokenIds to withdraw + */ + function withdrawFromForkEscrow(NounsDAOStorageV3p1.StorageV3 storage ds, uint256[] calldata tokenIds) external { + if (isForkPeriodActive(ds)) revert ForkPeriodActive(); + + INounsDAOForkEscrow forkEscrow = ds.forkEscrow; + forkEscrow.returnTokensToOwner(msg.sender, tokenIds); + + emit WithdrawFromForkEscrow(forkEscrow.forkId(), msg.sender, tokenIds); + } + + /** + * @notice Execute the fork. Only possible if the fork threshold has been exceeded. + * This will deploy a new DAO and send the prorated part of the treasury to the new DAO's treasury. + * This will also close the active escrow and all nouns in the escrow will belong to the original DAO. + * @return forkTreasury The address of the new DAO's treasury + * @return forkToken The address of the new DAO's token + */ + function executeFork(NounsDAOStorageV3p1.StorageV3 storage ds) + external + returns (address forkTreasury, address forkToken) + { + if (isForkPeriodActive(ds)) revert ForkPeriodActive(); + INounsDAOForkEscrow forkEscrow = ds.forkEscrow; + + uint256 tokensInEscrow = forkEscrow.numTokensInEscrow(); + if (tokensInEscrow <= forkThreshold(ds)) revert ForkThresholdNotMet(); + + uint256 forkEndTimestamp = block.timestamp + ds.forkPeriod; + + (forkTreasury, forkToken) = ds.forkDAODeployer.deployForkDAO(forkEndTimestamp, forkEscrow); + sendProRataTreasury(ds, forkTreasury, tokensInEscrow, adjustedTotalSupply(ds)); + uint32 forkId = forkEscrow.closeEscrow(); + + ds.forkDAOTreasury = forkTreasury; + ds.forkDAOToken = forkToken; + ds.forkEndTimestamp = forkEndTimestamp; + + emit ExecuteFork(forkId, forkTreasury, forkToken, forkEndTimestamp, tokensInEscrow); + } + + /** + * @notice Joins a fork while a fork is active + * Sends the tokens to the timelock contract. + * Sends a prorated part of the treasury to the new fork DAO's treasury. + * Mints new tokens in the new fork DAO with the same token ids. + * @param tokenIds the tokenIds to send to the DAO in exchange for joining the fork + */ + function joinFork( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256[] calldata tokenIds, + uint256[] calldata proposalIds, + string calldata reason + ) external { + if (!isForkPeriodActive(ds)) revert ForkPeriodNotActive(); + + INounsDAOForkEscrow forkEscrow = ds.forkEscrow; + address timelock = address(ds.timelock); + sendProRataTreasury(ds, ds.forkDAOTreasury, tokenIds.length, adjustedTotalSupply(ds)); + + for (uint256 i = 0; i < tokenIds.length; i++) { + checkNounIdIsAllowedToFork(ds, tokenIds[i]); + ds.nouns.transferFrom(msg.sender, timelock, tokenIds[i]); + } + + NounsTokenFork(ds.forkDAOToken).claimDuringForkPeriod(msg.sender, tokenIds); + + emit JoinFork(forkEscrow.forkId() - 1, msg.sender, tokenIds, proposalIds, reason); + } + + /** + * @notice Withdraws nouns from the fork escrow to the treasury after the fork has been executed + * @dev Only the DAO can call this function + * @param tokenIds the tokenIds to withdraw + */ + function withdrawDAONounsFromEscrowToTreasury(NounsDAOStorageV3p1.StorageV3 storage ds, uint256[] calldata tokenIds) + external + { + withdrawDAONounsFromEscrow(ds, tokenIds, address(ds.timelock)); + } + + /** + * @notice Withdraws nouns from the fork escrow after the fork has been executed to an address other than the treasury + * @dev Only the DAO can call this function + * @param tokenIds the tokenIds to withdraw + * @param to the address to send the nouns to + */ + function withdrawDAONounsFromEscrowIncreasingTotalSupply( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256[] calldata tokenIds, + address to + ) external { + if (to == address(ds.timelock)) revert UseAlternativeWithdrawFunction(); + + withdrawDAONounsFromEscrow(ds, tokenIds, to); + + emit DAONounsSupplyIncreasedFromEscrow(tokenIds.length, to); + } + + function withdrawDAONounsFromEscrow( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256[] calldata tokenIds, + address to + ) private { + if (msg.sender != ds.admin) { + revert AdminOnly(); + } + + ds.forkEscrow.withdrawTokens(tokenIds, to); + + emit DAOWithdrawNounsFromEscrow(tokenIds, to); + } + + /** + * @notice Returns the required number of tokens to escrow to trigger a fork + */ + function forkThreshold(NounsDAOStorageV3p1.StorageV3 storage ds) public view returns (uint256) { + return (adjustedTotalSupply(ds) * ds.forkThresholdBPS) / 10_000; + } + + /** + * @notice Returns the number of tokens currently in escrow, contributing to the fork threshold + */ + function numTokensInForkEscrow(NounsDAOStorageV3p1.StorageV3 storage ds) public view returns (uint256) { + return ds.forkEscrow.numTokensInEscrow(); + } + + function checkNounIdIsAllowedToFork(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 tokenId) internal view { + if (ds.nounAgeRequiredToFork == 0) return; + + uint256 auctionedNounId = INounsAuctionHouseV2(ds.nouns.minter()).auction().nounId; + if (tokenId < auctionedNounId - ds.nounAgeRequiredToFork) revert NounIdNotOldEnough(); + } + + /** + * @notice Returns the number of nouns in supply minus nouns owned by the DAO, i.e. held in the treasury or in an + * escrow after it has closed. + * This is used when calculating proposal threshold, quorum, fork threshold & treasury split. + */ + function adjustedTotalSupply(NounsDAOStorageV3p1.StorageV3 storage ds) internal view returns (uint256) { + return ds.nouns.totalSupply() - ds.nouns.balanceOf(address(ds.timelock)) - ds.forkEscrow.numTokensOwnedByDAO(); + } + + /** + * @notice Returns true if noun holders can currently join a fork + */ + function isForkPeriodActive(NounsDAOStorageV3p1.StorageV3 storage ds) internal view returns (bool) { + return ds.forkEndTimestamp > block.timestamp; + } + + /** + * @notice Sends part of the DAO's treasury to the `newDAOTreasury` address. + * The amount sent is proportional to the `tokenCount` out of `totalSupply`. + * Sends ETH and ERC20 tokens listed in `ds.erc20TokensToIncludeInFork`. + */ + function sendProRataTreasury( + NounsDAOStorageV3p1.StorageV3 storage ds, + address newDAOTreasury, + uint256 tokenCount, + uint256 totalSupply + ) internal { + INounsDAOExecutorV2 timelock = ds.timelock; + uint256 ethToSend = (address(timelock).balance * tokenCount) / totalSupply; + + timelock.sendETH(newDAOTreasury, ethToSend); + + uint256 erc20Count = ds.erc20TokensToIncludeInFork.length; + for (uint256 i = 0; i < erc20Count; ++i) { + IERC20 erc20token = IERC20(ds.erc20TokensToIncludeInFork[i]); + uint256 tokensToSend = (erc20token.balanceOf(address(timelock)) * tokenCount) / totalSupply; + if (tokensToSend > 0) { + timelock.sendERC20(newDAOTreasury, address(erc20token), tokensToSend); + } + } + } +} diff --git a/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3ProposalsV1p1.sol b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3ProposalsV1p1.sol new file mode 100644 index 0000000000..c4cfb32d92 --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3ProposalsV1p1.sol @@ -0,0 +1,1018 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Library for NounsDAOLogicV3 contract containing the proposal lifecycle code + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { INounsDAOExecutor, NounsTokenLike, NounsDAOStorageV2 } from '../NounsDAOInterfaces.sol'; +import { NounsDAOStorageV3p1 } from './NounsDAOStorageV3p1.sol'; +import { NounsDAOV3DynamicQuorumV1p1 } from './NounsDAOV3DynamicQuorumV1p1.sol'; +import { NounsDAOV3ForkV1p1 } from './NounsDAOV3ForkV1p1.sol'; +import { SignatureChecker } from '../../external/openzeppelin/SignatureChecker.sol'; +import { ECDSA } from '../../external/openzeppelin/ECDSA.sol'; +import { SafeCast } from '@openzeppelin/contracts/utils/math/SafeCast.sol'; + +library NounsDAOV3ProposalsV1p1 { + using NounsDAOV3DynamicQuorumV1p1 for NounsDAOStorageV3p1.StorageV3; + using NounsDAOV3ForkV1p1 for NounsDAOStorageV3p1.StorageV3; + + error CantCancelProposalAtFinalState(); + error ProposalInfoArityMismatch(); + error MustProvideActions(); + error TooManyActions(); + error ProposerAlreadyHasALiveProposal(); + error InvalidSignature(); + error SignatureExpired(); + error CanOnlyEditUpdatableProposals(); + error OnlyProposerCanEdit(); + error SignerCountMismtach(); + error ProposerCannotUpdateProposalWithSigners(); + error MustProvideSignatures(); + error SignatureIsCancelled(); + error CannotExecuteDuringForkingPeriod(); + error VetoerBurned(); + error VetoerOnly(); + error CantVetoExecutedProposal(); + error VotesBelowProposalThreshold(); + + /// @notice An event emitted when a proposal has been vetoed by vetoAddress + event ProposalVetoed(uint256 id); + + /// @notice An event emitted when a new proposal is created + event ProposalCreated( + uint256 id, + address proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 startBlock, + uint256 endBlock, + string description + ); + + /// @notice An event emitted when a new proposal is created, which includes additional information + /// @dev V3 adds `signers`, `updatePeriodEndBlock` compared to the V1/V2 event. + event ProposalCreatedWithRequirements( + uint256 id, + address proposer, + address[] signers, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + uint256 startBlock, + uint256 endBlock, + uint256 updatePeriodEndBlock, + uint256 proposalThreshold, + uint256 quorumVotes, + string description + ); + + /// @notice Emitted when a proposal is created to be executed on timelockV1 + event ProposalCreatedOnTimelockV1(uint256 id); + + /// @notice Emitted when a proposal is updated + event ProposalUpdated( + uint256 indexed id, + address indexed proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + string description, + string updateMessage + ); + + /// @notice Emitted when a proposal's transactions are updated + event ProposalTransactionsUpdated( + uint256 indexed id, + address indexed proposer, + address[] targets, + uint256[] values, + string[] signatures, + bytes[] calldatas, + string updateMessage + ); + + /// @notice Emitted when a proposal's description is updated + event ProposalDescriptionUpdated( + uint256 indexed id, + address indexed proposer, + string description, + string updateMessage + ); + + /// @notice An event emitted when a proposal has been queued in the NounsDAOExecutor + event ProposalQueued(uint256 id, uint256 eta); + + /// @notice An event emitted when a proposal has been executed in the NounsDAOExecutor + event ProposalExecuted(uint256 id); + + /// @notice An event emitted when a proposal has been canceled + event ProposalCanceled(uint256 id); + + /// @notice Emitted when someone cancels a signature + event SignatureCancelled(address indexed signer, bytes sig); + + // Created to solve stack-too-deep errors + struct ProposalTxs { + address[] targets; + uint256[] values; + string[] signatures; + bytes[] calldatas; + } + + /// @notice The maximum number of actions that can be included in a proposal + uint256 public constant PROPOSAL_MAX_OPERATIONS = 10; // 10 actions + + bytes32 public constant DOMAIN_TYPEHASH = + keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); + + bytes32 public constant PROPOSAL_TYPEHASH = + keccak256( + 'Proposal(address proposer,address[] targets,uint256[] values,string[] signatures,bytes[] calldatas,string description,uint256 expiry)' + ); + + bytes32 public constant UPDATE_PROPOSAL_TYPEHASH = + keccak256( + 'UpdateProposal(uint256 proposalId,address proposer,address[] targets,uint256[] values,string[] signatures,bytes[] calldatas,string description,uint256 expiry)' + ); + + /** + * @notice Function used to propose a new proposal. Sender must have delegates above the proposal threshold + * @param txs Target addresses, eth values, function signatures and calldatas for proposal calls + * @param description String description of the proposal + * @return Proposal id of new proposal + */ + function propose( + NounsDAOStorageV3p1.StorageV3 storage ds, + ProposalTxs memory txs, + string memory description + ) internal returns (uint256) { + uint256 adjustedTotalSupply = ds.adjustedTotalSupply(); + uint256 proposalThreshold_ = checkPropThreshold( + ds, + ds.nouns.getPriorVotes(msg.sender, block.number - 1), + adjustedTotalSupply + ); + checkProposalTxs(txs); + checkNoActiveProp(ds, msg.sender); + + uint256 proposalId = ds.proposalCount = ds.proposalCount + 1; + NounsDAOStorageV3p1.Proposal storage newProposal = createNewProposal( + ds, + proposalId, + proposalThreshold_, + adjustedTotalSupply, + txs + ); + ds.latestProposalIds[msg.sender] = proposalId; + + emitNewPropEvents(newProposal, new address[](0), ds.minQuorumVotes(adjustedTotalSupply), txs, description); + + return proposalId; + } + + /** + * @notice Function used to propose a new proposal. Sender must have delegates above the proposal threshold. + * This proposal would be executed via the timelockV1 contract. This is meant to be used in case timelockV1 + * is still holding funds or has special permissions to execute on certain contracts. + * @param txs Target addresses, eth values, function signatures and calldatas for proposal calls + * @param description String description of the proposal + * @return uint256 Proposal id of new proposal + */ + function proposeOnTimelockV1( + NounsDAOStorageV3p1.StorageV3 storage ds, + ProposalTxs memory txs, + string memory description + ) internal returns (uint256) { + uint256 newProposalId = propose(ds, txs, description); + + NounsDAOStorageV3p1.Proposal storage newProposal = ds._proposals[newProposalId]; + newProposal.executeOnTimelockV1 = true; + + emit ProposalCreatedOnTimelockV1(newProposalId); + + return newProposalId; + } + + /** + * @notice Function used to propose a new proposal. Sender and signers must have delegates above the proposal threshold + * @param proposerSignatures Array of signers who have signed the proposal and their signatures. + * @dev The signatures follow EIP-712. See `PROPOSAL_TYPEHASH` in NounsDAOV3Proposals.sol + * @param txs Target addresses, eth values, function signatures and calldatas for proposal calls + * @param description String description of the proposal + * @return uint256 Proposal id of new proposal + */ + function proposeBySigs( + NounsDAOStorageV3p1.StorageV3 storage ds, + NounsDAOStorageV3p1.ProposerSignature[] memory proposerSignatures, + ProposalTxs memory txs, + string memory description + ) external returns (uint256) { + if (proposerSignatures.length == 0) revert MustProvideSignatures(); + checkProposalTxs(txs); + uint256 proposalId = ds.proposalCount = ds.proposalCount + 1; + + uint256 adjustedTotalSupply = ds.adjustedTotalSupply(); + + uint256 propThreshold = proposalThreshold(ds, adjustedTotalSupply); + + NounsDAOStorageV3p1.Proposal storage newProposal = createNewProposal( + ds, + proposalId, + propThreshold, + adjustedTotalSupply, + txs + ); + + // important that the proposal is created before the verification call in order to ensure + // the same signer is not trying to sign this proposal more than once + (uint256 votes, address[] memory signers) = verifySignersCanBackThisProposalAndCountTheirVotes( + ds, + proposerSignatures, + txs, + description, + proposalId + ); + if (signers.length == 0) revert MustProvideSignatures(); + if (votes <= propThreshold) revert VotesBelowProposalThreshold(); + + newProposal.signers = signers; + + emitNewPropEvents(newProposal, signers, ds.minQuorumVotes(adjustedTotalSupply), txs, description); + + return proposalId; + } + + /** + * @notice Invalidates a signature that may be used for signing a proposal. + * Once a signature is canceled, the sender can no longer use it again. + * If the sender changes their mind and want to sign the proposal, they can change the expiry timestamp + * in order to produce a new signature. + * The signature will only be invalidated when used by the sender. If used by a different account, it will + * not be invalidated. + * @param sig The signature to cancel + */ + function cancelSig(NounsDAOStorageV3p1.StorageV3 storage ds, bytes calldata sig) external { + bytes32 sigHash = keccak256(sig); + ds.cancelledSigs[msg.sender][sigHash] = true; + + emit SignatureCancelled(msg.sender, sig); + } + + /** + * @notice Update a proposal transactions and description. + * Only the proposer can update it, and only during the updateable period. + * @param proposalId Proposal's id + * @param targets Updated target addresses for proposal calls + * @param values Updated eth values for proposal calls + * @param signatures Updated function signatures for proposal calls + * @param calldatas Updated calldatas for proposal calls + * @param description Updated description of the proposal + * @param updateMessage Short message to explain the update + */ + function updateProposal( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description, + string memory updateMessage + ) external { + updateProposalTransactionsInternal(ds, proposalId, targets, values, signatures, calldatas); + + emit ProposalUpdated( + proposalId, + msg.sender, + targets, + values, + signatures, + calldatas, + description, + updateMessage + ); + } + + /** + * @notice Updates the proposal's transactions. Only the proposer can update it, and only during the updateable period. + * @param proposalId Proposal's id + * @param targets Updated target addresses for proposal calls + * @param values Updated eth values for proposal calls + * @param signatures Updated function signatures for proposal calls + * @param calldatas Updated calldatas for proposal calls + * @param updateMessage Short message to explain the update + */ + function updateProposalTransactions( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory updateMessage + ) external { + updateProposalTransactionsInternal(ds, proposalId, targets, values, signatures, calldatas); + + emit ProposalTransactionsUpdated(proposalId, msg.sender, targets, values, signatures, calldatas, updateMessage); + } + + function updateProposalTransactionsInternal( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ) internal { + checkProposalTxs(ProposalTxs(targets, values, signatures, calldatas)); + + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + checkProposalUpdatable(ds, proposalId, proposal); + + proposal.targets = targets; + proposal.values = values; + proposal.signatures = signatures; + proposal.calldatas = calldatas; + } + + /** + * @notice Updates the proposal's description. Only the proposer can update it, and only during the updateable period. + * @param proposalId Proposal's id + * @param description Updated description of the proposal + * @param updateMessage Short message to explain the update + */ + function updateProposalDescription( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + string calldata description, + string calldata updateMessage + ) external { + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + checkProposalUpdatable(ds, proposalId, proposal); + + emit ProposalDescriptionUpdated(proposalId, msg.sender, description, updateMessage); + } + + /** + * @notice Update a proposal's transactions and description that was created with proposeBySigs. + * Only the proposer can update it, during the updateable period. + * Requires the original signers to sign the update. + * @param proposalId Proposal's id + * @param proposerSignatures Array of signers who have signed the proposal and their signatures. + * @dev The signatures follow EIP-712. See `UPDATE_PROPOSAL_TYPEHASH` in NounsDAOV3Proposals.sol + * @param txs Updated transactions for the proposal + * @param description Updated description of the proposal + * @param updateMessage Short message to explain the update + */ + function updateProposalBySigs( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + NounsDAOStorageV3p1.ProposerSignature[] memory proposerSignatures, + ProposalTxs memory txs, + string memory description, + string memory updateMessage + ) external { + checkProposalTxs(txs); + // without this check it's possible to run through this function and update a proposal without signatures + // this problem doesn't exist in the propose function because we check for prop threshold there + if (proposerSignatures.length == 0) revert MustProvideSignatures(); + + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + if (stateInternal(ds, proposalId) != NounsDAOStorageV3p1.ProposalState.Updatable) + revert CanOnlyEditUpdatableProposals(); + if (msg.sender != proposal.proposer) revert OnlyProposerCanEdit(); + + address[] memory signers = proposal.signers; + if (proposerSignatures.length != signers.length) revert SignerCountMismtach(); + + bytes memory proposalEncodeData = abi.encodePacked( + proposalId, + calcProposalEncodeData(msg.sender, txs, description) + ); + + for (uint256 i = 0; i < proposerSignatures.length; ++i) { + verifyProposalSignature(ds, proposalEncodeData, proposerSignatures[i], UPDATE_PROPOSAL_TYPEHASH); + + // To avoid the gas cost of having to search signers in proposal.signers, we're assuming the sigs we get + // use the same amount of signers and the same order. + if (signers[i] != proposerSignatures[i].signer) revert OnlyProposerCanEdit(); + } + + proposal.targets = txs.targets; + proposal.values = txs.values; + proposal.signatures = txs.signatures; + proposal.calldatas = txs.calldatas; + + emit ProposalUpdated( + proposalId, + msg.sender, + txs.targets, + txs.values, + txs.signatures, + txs.calldatas, + description, + updateMessage + ); + } + + /** + * @notice Queues a proposal of state succeeded + * @param proposalId The id of the proposal to queue + */ + function queue(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) external { + require( + stateInternal(ds, proposalId) == NounsDAOStorageV3p1.ProposalState.Succeeded, + 'NounsDAO::queue: proposal can only be queued if it is succeeded' + ); + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + INounsDAOExecutor timelock = getProposalTimelock(ds, proposal); + uint256 eta = block.timestamp + timelock.delay(); + for (uint256 i = 0; i < proposal.targets.length; i++) { + queueOrRevertInternal( + timelock, + proposal.targets[i], + proposal.values[i], + proposal.signatures[i], + proposal.calldatas[i], + eta + ); + } + proposal.eta = eta; + emit ProposalQueued(proposalId, eta); + } + + function queueOrRevertInternal( + INounsDAOExecutor timelock, + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) internal { + require( + !timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), + 'NounsDAO::queueOrRevertInternal: identical proposal action already queued at eta' + ); + timelock.queueTransaction(target, value, signature, data, eta); + } + + /** + * @notice Executes a queued proposal if eta has passed + * @param proposalId The id of the proposal to execute + */ + function execute(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) external { + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + INounsDAOExecutor timelock = getProposalTimelock(ds, proposal); + executeInternal(ds, proposal, timelock); + } + + /** + * @notice Executes a queued proposal on timelockV1 if eta has passed + * This is only required for proposal that were queued on timelockV1, but before the upgrade to DAO V3. + * These proposals will not have the `executeOnTimelockV1` bool turned on. + */ + function executeOnTimelockV1(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) external { + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + executeInternal(ds, proposal, ds.timelockV1); + } + + function executeInternal( + NounsDAOStorageV3p1.StorageV3 storage ds, + NounsDAOStorageV3p1.Proposal storage proposal, + INounsDAOExecutor timelock + ) internal { + require( + stateInternal(ds, proposal.id) == NounsDAOStorageV3p1.ProposalState.Queued, + 'NounsDAO::execute: proposal can only be executed if it is queued' + ); + if (ds.isForkPeriodActive()) revert CannotExecuteDuringForkingPeriod(); + + proposal.executed = true; + + for (uint256 i = 0; i < proposal.targets.length; i++) { + timelock.executeTransaction( + proposal.targets[i], + proposal.values[i], + proposal.signatures[i], + proposal.calldatas[i], + proposal.eta + ); + } + emit ProposalExecuted(proposal.id); + } + + function getProposalTimelock( + NounsDAOStorageV3p1.StorageV3 storage ds, + NounsDAOStorageV3p1.Proposal storage proposal + ) internal view returns (INounsDAOExecutor) { + if (proposal.executeOnTimelockV1) { + return ds.timelockV1; + } else { + return ds.timelock; + } + } + + /** + * @notice Vetoes a proposal only if sender is the vetoer and the proposal has not been executed. + * @param proposalId The id of the proposal to veto + */ + function veto(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) external { + if (ds.vetoer == address(0)) { + revert VetoerBurned(); + } + + if (msg.sender != ds.vetoer) { + revert VetoerOnly(); + } + + if (stateInternal(ds, proposalId) == NounsDAOStorageV3p1.ProposalState.Executed) { + revert CantVetoExecutedProposal(); + } + + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + + proposal.vetoed = true; + INounsDAOExecutor timelock = getProposalTimelock(ds, proposal); + for (uint256 i = 0; i < proposal.targets.length; i++) { + timelock.cancelTransaction( + proposal.targets[i], + proposal.values[i], + proposal.signatures[i], + proposal.calldatas[i], + proposal.eta + ); + } + + emit ProposalVetoed(proposalId); + } + + /** + * @notice Cancels a proposal only if sender is the proposer or a signer, or proposer & signers voting power + * dropped below proposal threshold + * @param proposalId The id of the proposal to cancel + */ + function cancel(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) external { + NounsDAOStorageV3p1.ProposalState proposalState = stateInternal(ds, proposalId); + if ( + proposalState == NounsDAOStorageV3p1.ProposalState.Canceled || + proposalState == NounsDAOStorageV3p1.ProposalState.Defeated || + proposalState == NounsDAOStorageV3p1.ProposalState.Expired || + proposalState == NounsDAOStorageV3p1.ProposalState.Executed || + proposalState == NounsDAOStorageV3p1.ProposalState.Vetoed + ) { + revert CantCancelProposalAtFinalState(); + } + + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + address proposer = proposal.proposer; + NounsTokenLike nouns = ds.nouns; + + uint256 votes = nouns.getPriorVotes(proposer, block.number - 1); + bool msgSenderIsProposer = proposer == msg.sender; + address[] memory signers = proposal.signers; + for (uint256 i = 0; i < signers.length; ++i) { + msgSenderIsProposer = msgSenderIsProposer || msg.sender == signers[i]; + votes += nouns.getPriorVotes(signers[i], block.number - 1); + } + + require( + msgSenderIsProposer || votes <= proposal.proposalThreshold, + 'NounsDAO::cancel: proposer above threshold' + ); + + proposal.canceled = true; + INounsDAOExecutor timelock = getProposalTimelock(ds, proposal); + for (uint256 i = 0; i < proposal.targets.length; i++) { + timelock.cancelTransaction( + proposal.targets[i], + proposal.values[i], + proposal.signatures[i], + proposal.calldatas[i], + proposal.eta + ); + } + + emit ProposalCanceled(proposalId); + } + + /** + * @notice Gets the state of a proposal + * @param ds the DAO's state struct + * @param proposalId The id of the proposal + * @return Proposal state + */ + function state(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) + public + view + returns (NounsDAOStorageV3p1.ProposalState) + { + return stateInternal(ds, proposalId); + } + + /** + * @notice Gets the state of a proposal + * @dev This internal function is used by other libraries to embed in compile time and save the runtime gas cost of a delegate call + * @param ds the DAO's state struct + * @param proposalId The id of the proposal + * @return Proposal state + */ + function stateInternal(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) + internal + view + returns (NounsDAOStorageV3p1.ProposalState) + { + require(ds.proposalCount >= proposalId, 'NounsDAO::state: invalid proposal id'); + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + + if (proposal.vetoed) { + return NounsDAOStorageV3p1.ProposalState.Vetoed; + } else if (proposal.canceled) { + return NounsDAOStorageV3p1.ProposalState.Canceled; + } else if (block.number <= proposal.updatePeriodEndBlock) { + return NounsDAOStorageV3p1.ProposalState.Updatable; + } else if (block.number <= proposal.startBlock) { + return NounsDAOStorageV3p1.ProposalState.Pending; + } else if (block.number <= proposal.endBlock) { + return NounsDAOStorageV3p1.ProposalState.Active; + } else if (block.number <= proposal.objectionPeriodEndBlock) { + return NounsDAOStorageV3p1.ProposalState.ObjectionPeriod; + } else if (isDefeated(ds, proposal)) { + return NounsDAOStorageV3p1.ProposalState.Defeated; + } else if (proposal.eta == 0) { + return NounsDAOStorageV3p1.ProposalState.Succeeded; + } else if (proposal.executed) { + return NounsDAOStorageV3p1.ProposalState.Executed; + } else if (block.timestamp >= proposal.eta + getProposalTimelock(ds, proposal).GRACE_PERIOD()) { + return NounsDAOStorageV3p1.ProposalState.Expired; + } else { + return NounsDAOStorageV3p1.ProposalState.Queued; + } + } + + /** + * @notice Gets actions of a proposal + * @param proposalId the id of the proposal + * @return targets + * @return values + * @return signatures + * @return calldatas + */ + function getActions(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) + internal + view + returns ( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas + ) + { + NounsDAOStorageV3p1.Proposal storage p = ds._proposals[proposalId]; + return (p.targets, p.values, p.signatures, p.calldatas); + } + + /** + * @notice Gets the receipt for a voter on a given proposal + * @param proposalId the id of proposal + * @param voter The address of the voter + * @return The voting receipt + */ + function getReceipt( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + address voter + ) internal view returns (NounsDAOStorageV3p1.Receipt memory) { + return ds._proposals[proposalId].receipts[voter]; + } + + /** + * @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(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) + external + view + returns (NounsDAOStorageV2.ProposalCondensed memory) + { + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + return + NounsDAOStorageV2.ProposalCondensed({ + id: proposal.id, + proposer: proposal.proposer, + proposalThreshold: proposal.proposalThreshold, + quorumVotes: ds.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 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 proposalsV3(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 proposalId) + external + view + returns (NounsDAOStorageV3p1.ProposalCondensed memory) + { + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + return + NounsDAOStorageV3p1.ProposalCondensed({ + id: proposal.id, + proposer: proposal.proposer, + proposalThreshold: proposal.proposalThreshold, + quorumVotes: ds.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, + signers: proposal.signers, + updatePeriodEndBlock: proposal.updatePeriodEndBlock, + objectionPeriodEndBlock: proposal.objectionPeriodEndBlock, + executeOnTimelockV1: proposal.executeOnTimelockV1 + }); + } + + /** + * @notice Current proposal threshold using Noun Total Supply + * Differs from `GovernerBravo` which uses fixed amount + */ + function proposalThreshold(NounsDAOStorageV3p1.StorageV3 storage ds, uint256 adjustedTotalSupply) + internal + view + returns (uint256) + { + return bps2Uint(ds.proposalThresholdBPS, adjustedTotalSupply); + } + + function isDefeated(NounsDAOStorageV3p1.StorageV3 storage ds, NounsDAOStorageV3p1.Proposal storage proposal) + internal + view + returns (bool) + { + uint256 forVotes = proposal.forVotes; + return forVotes <= proposal.againstVotes || forVotes < ds.quorumVotes(proposal.id); + } + + /** + * @notice reverts if `proposer` is the proposer or signer of an active proposal. + * This is a spam protection mechanism to limit the number of proposals each noun can back. + */ + function checkNoActiveProp(NounsDAOStorageV3p1.StorageV3 storage ds, address proposer) internal view { + uint256 latestProposalId = ds.latestProposalIds[proposer]; + if (latestProposalId != 0) { + NounsDAOStorageV3p1.ProposalState proposersLatestProposalState = stateInternal(ds, latestProposalId); + if ( + proposersLatestProposalState == NounsDAOStorageV3p1.ProposalState.ObjectionPeriod || + proposersLatestProposalState == NounsDAOStorageV3p1.ProposalState.Active || + proposersLatestProposalState == NounsDAOStorageV3p1.ProposalState.Pending || + proposersLatestProposalState == NounsDAOStorageV3p1.ProposalState.Updatable + ) revert ProposerAlreadyHasALiveProposal(); + } + } + + /** + * @dev Extracted this function to fix the `Stack too deep` error `proposeBySigs` hit. + */ + function verifySignersCanBackThisProposalAndCountTheirVotes( + NounsDAOStorageV3p1.StorageV3 storage ds, + NounsDAOStorageV3p1.ProposerSignature[] memory proposerSignatures, + ProposalTxs memory txs, + string memory description, + uint256 proposalId + ) internal returns (uint256 votes, address[] memory signers) { + NounsTokenLike nouns = ds.nouns; + bytes memory proposalEncodeData = calcProposalEncodeData(msg.sender, txs, description); + + signers = new address[](proposerSignatures.length); + uint256 numSigners = 0; + for (uint256 i = 0; i < proposerSignatures.length; ++i) { + verifyProposalSignature(ds, proposalEncodeData, proposerSignatures[i], PROPOSAL_TYPEHASH); + + address signer = proposerSignatures[i].signer; + checkNoActiveProp(ds, signer); + + uint256 signerVotes = nouns.getPriorVotes(signer, block.number - 1); + if (signerVotes == 0) { + continue; + } + + signers[numSigners++] = signer; + ds.latestProposalIds[signer] = proposalId; + votes += signerVotes; + } + + if (numSigners < proposerSignatures.length) { + // this assembly trims the signer array, getting rid of unused cells + assembly { + mstore(signers, numSigners) + } + } + + checkNoActiveProp(ds, msg.sender); + ds.latestProposalIds[msg.sender] = proposalId; + votes += nouns.getPriorVotes(msg.sender, block.number - 1); + } + + function calcProposalEncodeData( + address proposer, + ProposalTxs memory txs, + string memory description + ) internal pure returns (bytes memory) { + bytes32[] memory signatureHashes = new bytes32[](txs.signatures.length); + for (uint256 i = 0; i < txs.signatures.length; ++i) { + signatureHashes[i] = keccak256(bytes(txs.signatures[i])); + } + + bytes32[] memory calldatasHashes = new bytes32[](txs.calldatas.length); + for (uint256 i = 0; i < txs.calldatas.length; ++i) { + calldatasHashes[i] = keccak256(txs.calldatas[i]); + } + + return + abi.encode( + proposer, + keccak256(abi.encodePacked(txs.targets)), + keccak256(abi.encodePacked(txs.values)), + keccak256(abi.encodePacked(signatureHashes)), + keccak256(abi.encodePacked(calldatasHashes)), + keccak256(bytes(description)) + ); + } + + function checkProposalUpdatable( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + NounsDAOStorageV3p1.Proposal storage proposal + ) internal view { + if (stateInternal(ds, proposalId) != NounsDAOStorageV3p1.ProposalState.Updatable) + revert CanOnlyEditUpdatableProposals(); + if (msg.sender != proposal.proposer) revert OnlyProposerCanEdit(); + if (proposal.signers.length > 0) revert ProposerCannotUpdateProposalWithSigners(); + } + + function createNewProposal( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + uint256 proposalThreshold_, + uint256 adjustedTotalSupply, + ProposalTxs memory txs + ) internal returns (NounsDAOStorageV3p1.Proposal storage newProposal) { + uint64 updatePeriodEndBlock = SafeCast.toUint64(block.number + ds.proposalUpdatablePeriodInBlocks); + uint256 startBlock = updatePeriodEndBlock + ds.votingDelay; + uint256 endBlock = startBlock + ds.votingPeriod; + + newProposal = ds._proposals[proposalId]; + newProposal.id = proposalId; + newProposal.proposer = msg.sender; + newProposal.proposalThreshold = proposalThreshold_; + newProposal.targets = txs.targets; + newProposal.values = txs.values; + newProposal.signatures = txs.signatures; + newProposal.calldatas = txs.calldatas; + newProposal.startBlock = startBlock; + newProposal.endBlock = endBlock; + newProposal.totalSupply = adjustedTotalSupply; + newProposal.creationBlock = SafeCast.toUint64(block.number); + newProposal.updatePeriodEndBlock = updatePeriodEndBlock; + } + + function emitNewPropEvents( + NounsDAOStorageV3p1.Proposal storage newProposal, + address[] memory signers, + uint256 minQuorumVotes, + ProposalTxs memory txs, + string memory description + ) internal { + /// @notice Maintains backwards compatibility with GovernorBravo events + emit ProposalCreated( + newProposal.id, + msg.sender, + txs.targets, + txs.values, + txs.signatures, + txs.calldatas, + newProposal.startBlock, + newProposal.endBlock, + description + ); + + /// @notice V1: Updated event with `proposalThreshold` and `quorumVotes` `minQuorumVotes` + /// @notice V2: `quorumVotes` changed to `minQuorumVotes` + /// @notice V3: Added signers and updatePeriodEndBlock + emit ProposalCreatedWithRequirements( + newProposal.id, + msg.sender, + signers, + txs.targets, + txs.values, + txs.signatures, + txs.calldatas, + newProposal.startBlock, + newProposal.endBlock, + newProposal.updatePeriodEndBlock, + newProposal.proposalThreshold, + minQuorumVotes, + description + ); + } + + function checkPropThreshold( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 votes, + uint256 adjustedTotalSupply + ) internal view returns (uint256 propThreshold) { + propThreshold = proposalThreshold(ds, adjustedTotalSupply); + if (votes <= propThreshold) revert VotesBelowProposalThreshold(); + } + + function checkProposalTxs(ProposalTxs memory txs) internal pure { + if ( + txs.targets.length != txs.values.length || + txs.targets.length != txs.signatures.length || + txs.targets.length != txs.calldatas.length + ) revert ProposalInfoArityMismatch(); + if (txs.targets.length == 0) revert MustProvideActions(); + if (txs.targets.length > PROPOSAL_MAX_OPERATIONS) revert TooManyActions(); + } + + function verifyProposalSignature( + NounsDAOStorageV3p1.StorageV3 storage ds, + bytes memory proposalEncodeData, + NounsDAOStorageV3p1.ProposerSignature memory proposerSignature, + bytes32 typehash + ) internal view { + bytes32 sigHash = keccak256(proposerSignature.sig); + if (ds.cancelledSigs[proposerSignature.signer][sigHash]) revert SignatureIsCancelled(); + + bytes32 digest = sigDigest(typehash, proposalEncodeData, proposerSignature.expirationTimestamp, address(this)); + if (!SignatureChecker.isValidSignatureNow(proposerSignature.signer, digest, proposerSignature.sig)) + revert InvalidSignature(); + + if (block.timestamp > proposerSignature.expirationTimestamp) revert SignatureExpired(); + } + + /** + * @notice Generate the digest (hash) used to verify proposal signatures. + * @param typehash the EIP 712 type hash of the signed message, e.g. `PROPOSAL_TYPEHASH` or `UPDATE_PROPOSAL_TYPEHASH`. + * @param proposalEncodeData the abi encoded proposal data, identical to the output of `calcProposalEncodeData`. + * @param expirationTimestamp the signature's expiration timestamp. + * @param verifyingContract the contract verifying the signature, e.g. the DAO proxy by default. + * @return bytes32 the signature's typed data hash. + */ + function sigDigest( + bytes32 typehash, + bytes memory proposalEncodeData, + uint256 expirationTimestamp, + address verifyingContract + ) internal view returns (bytes32) { + bytes32 structHash = keccak256(abi.encodePacked(typehash, proposalEncodeData, expirationTimestamp)); + + bytes32 domainSeparator = keccak256( + abi.encode(DOMAIN_TYPEHASH, keccak256(bytes('Nouns DAO')), block.chainid, verifyingContract) + ); + + return ECDSA.toTypedDataHash(domainSeparator, structHash); + } + + function bps2Uint(uint256 bps, uint256 number) internal pure returns (uint256) { + return (number * bps) / 10000; + } +} diff --git a/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3VotesV1p1.sol b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3VotesV1p1.sol new file mode 100644 index 0000000000..0902ac7a42 --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/v3p1/NounsDAOV3VotesV1p1.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Library for NounsDAOLogicV3 contract containing all the voting related code + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { NounsDAOStorageV3p1 } from './NounsDAOStorageV3p1.sol'; +import { NounsDAOV3ProposalsV1p1 } from './NounsDAOV3ProposalsV1p1.sol'; +import { SafeCast } from '@openzeppelin/contracts/utils/math/SafeCast.sol'; + +library NounsDAOV3VotesV1p1 { + using NounsDAOV3ProposalsV1p1 for NounsDAOStorageV3p1.StorageV3; + + error CanOnlyVoteAgainstDuringObjectionPeriod(); + + /// @notice An event emitted when a vote has been cast on a proposal + /// @param voter The address which casted a vote + /// @param proposalId The proposal id which was voted on + /// @param support Support value for the vote. 0=against, 1=for, 2=abstain + /// @param votes Number of votes which were cast by the voter + /// @param reason The reason given for the vote by the voter + event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 votes, string reason); + + /// @notice Emitted when a voter cast a vote requesting a gas refund. + event RefundableVote(address indexed voter, uint256 refundAmount, bool refundSent); + + /// @notice Emitted when a proposal is set to have an objection period + event ProposalObjectionPeriodSet(uint256 indexed id, uint256 objectionPeriodEndBlock); + + /// @notice The name of this contract + string public constant name = 'Nouns DAO'; + + /// @notice The EIP-712 typehash for the contract's domain + bytes32 public constant DOMAIN_TYPEHASH = + keccak256('EIP712Domain(string name,uint256 chainId,address verifyingContract)'); + + /// @notice The EIP-712 typehash for the ballot struct used by the contract + bytes32 public constant BALLOT_TYPEHASH = keccak256('Ballot(uint256 proposalId,uint8 support)'); + + /// @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 maximum gas units the DAO will refund voters on; supports about 9,190 characters + uint256 public constant MAX_REFUND_GAS_USED = 200_000; + + /// @notice The maximum basefee the DAO will refund voters on + uint256 public constant MAX_REFUND_BASE_FEE = 200 gwei; + + /** + * @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 + */ + function castVote( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + uint8 support + ) external { + emit VoteCast(msg.sender, proposalId, support, castVoteInternal(ds, msg.sender, 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 + * @dev Reentrancy is defended against in `castVoteInternal` at the `receipt.hasVoted == false` require statement. + */ + function castRefundableVote( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + uint8 support + ) external { + castRefundableVoteInternal(ds, 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( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + uint8 support, + string calldata reason + ) external { + castRefundableVoteInternal(ds, 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( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + uint8 support, + string memory reason + ) internal { + uint256 startGas = gasleft(); + uint96 votes = castVoteInternal(ds, 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 + * @param reason The reason given for the vote by the voter + */ + function castVoteWithReason( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + uint8 support, + string calldata reason + ) external { + emit VoteCast(msg.sender, proposalId, support, castVoteInternal(ds, msg.sender, proposalId, support), reason); + } + + /** + * @notice Cast a vote for a proposal by signature + * @dev External function that accepts EIP-712 signatures for voting on proposals. + */ + function castVoteBySig( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + uint8 support, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32 domainSeparator = keccak256( + abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), block.chainid, address(this)) + ); + bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); + bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); + address signatory = ecrecover(digest, v, r, s); + require(signatory != address(0), 'NounsDAO::castVoteBySig: invalid signature'); + emit VoteCast(signatory, proposalId, support, castVoteInternal(ds, signatory, proposalId, support), ''); + } + + /** + * @notice Internal function that caries out voting logic + * In case of a vote during the 'last minute window', which changes the proposal outcome from being defeated to + * passing, and objection period is adding to the proposal's voting period. + * During the objection period, only votes against a proposal can be cast. + * @param voter The voter that is casting their vote + * @param proposalId The id of the proposal to vote on + * @param support The support value for the vote. 0=against, 1=for, 2=abstain + * @return The number of votes cast + */ + function castVoteInternal( + NounsDAOStorageV3p1.StorageV3 storage ds, + address voter, + uint256 proposalId, + uint8 support + ) internal returns (uint96) { + NounsDAOStorageV3p1.ProposalState proposalState = ds.stateInternal(proposalId); + + if (proposalState == NounsDAOStorageV3p1.ProposalState.Active) { + return castVoteDuringVotingPeriodInternal(ds, proposalId, voter, support); + } else if (proposalState == NounsDAOStorageV3p1.ProposalState.ObjectionPeriod) { + if (support != 0) revert CanOnlyVoteAgainstDuringObjectionPeriod(); + return castObjectionInternal(ds, proposalId, voter); + } + + revert('NounsDAO::castVoteInternal: voting is closed'); + } + + /** + * @notice Internal function that handles voting logic during the voting period. + * @dev Assumes it's only called by `castVoteInternal` which ensures the proposal is active. + * @param proposalId The id of the proposal being voted on + * @param voter The address of the voter + * @param support The support value for the vote. 0=against, 1=for, 2=abstain + * @return The number of votes cast + */ + function castVoteDuringVotingPeriodInternal( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + address voter, + uint8 support + ) internal returns (uint96) { + require(support <= 2, 'NounsDAO::castVoteDuringVotingPeriodInternal: invalid vote type'); + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + NounsDAOStorageV3p1.Receipt storage receipt = proposal.receipts[voter]; + require(receipt.hasVoted == false, 'NounsDAO::castVoteDuringVotingPeriodInternal: 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 = ds.nouns.getPriorVotes(voter, proposalVoteSnapshotBlock(ds, proposalId, proposal)); + + bool isForVoteInLastMinuteWindow = false; + if (support == 1) { + isForVoteInLastMinuteWindow = (proposal.endBlock - block.number < ds.lastMinuteWindowInBlocks); + } + + bool isDefeatedBefore = false; + if (isForVoteInLastMinuteWindow) isDefeatedBefore = ds.isDefeated(proposal); + + if (support == 0) { + proposal.againstVotes = proposal.againstVotes + votes; + } else if (support == 1) { + proposal.forVotes = proposal.forVotes + votes; + } else if (support == 2) { + proposal.abstainVotes = proposal.abstainVotes + votes; + } + + if ( + // only for votes can trigger an objection period + // we're in the last minute window + isForVoteInLastMinuteWindow && + // first part of the vote flip check + // separated from the second part to optimize gas + isDefeatedBefore && + // haven't turn on objection yet + proposal.objectionPeriodEndBlock == 0 && + // second part of the vote flip check + !ds.isDefeated(proposal) + ) { + proposal.objectionPeriodEndBlock = SafeCast.toUint64( + proposal.endBlock + ds.objectionPeriodDurationInBlocks + ); + + emit ProposalObjectionPeriodSet(proposal.id, proposal.objectionPeriodEndBlock); + } + + receipt.hasVoted = true; + receipt.support = support; + receipt.votes = votes; + + return votes; + } + + /** + * @notice Internal function that handles against votes during an objection period. + * @dev Assumes it's being called by `castVoteInternal` which ensures: + * 1. The proposal is in the objection period state. + * 2. The vote is an against vote. + * @param proposalId The id of the proposal being voted on + * @param voter The address of the voter + * @return The number of votes cast + */ + function castObjectionInternal( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + address voter + ) internal returns (uint96) { + NounsDAOStorageV3p1.Proposal storage proposal = ds._proposals[proposalId]; + NounsDAOStorageV3p1.Receipt storage receipt = proposal.receipts[voter]; + require(receipt.hasVoted == false, 'NounsDAO::castVoteInternal: voter already voted'); + + uint96 votes = receipt.votes = ds.nouns.getPriorVotes( + voter, + proposalVoteSnapshotBlock(ds, proposalId, proposal) + ); + receipt.hasVoted = true; + receipt.support = 0; + proposal.againstVotes = proposal.againstVotes + votes; + + return votes; + } + + function _refundGas(uint256 startGas) internal { + unchecked { + uint256 balance = address(this).balance; + if (balance == 0) { + return; + } + uint256 basefee = min(block.basefee, MAX_REFUND_BASE_FEE); + uint256 gasPrice = min(tx.gasprice, basefee + MAX_REFUND_PRIORITY_FEE); + uint256 gasUsed = min(startGas - gasleft() + REFUND_BASE_GAS, MAX_REFUND_GAS_USED); + uint256 refundAmount = min(gasPrice * gasUsed, balance); + (bool refundSent, ) = msg.sender.call{ value: refundAmount }(''); + emit RefundableVote(msg.sender, refundAmount, refundSent); + } + } + + /** + * @notice Internal function that returns the snapshot block number to use given a proposalId. The choice is + * between the proposal's creation block and the proposal's voting start block, to allow a smooth migration from + * creation block to start block. + * @param proposalId The id of the proposal being voted on + * @param proposal The proposal storage reference, used to read `creationBlock` and `startBlock` + */ + function proposalVoteSnapshotBlock( + NounsDAOStorageV3p1.StorageV3 storage ds, + uint256 proposalId, + NounsDAOStorageV3p1.Proposal storage proposal + ) internal view returns (uint256) { + // The idea is to temporarily use this code that would still use `creationBlock` until all proposals are using + // `startBlock`, then we can deploy a quick DAO fix that removes this line and only uses `startBlock`. + // In that version upgrade we can also zero-out and remove this storage variable for max cleanup. + uint256 voteSnapshotBlockSwitchProposalId = ds.voteSnapshotBlockSwitchProposalId; + if (proposalId < voteSnapshotBlockSwitchProposalId || voteSnapshotBlockSwitchProposalId == 0) { + return proposal.creationBlock; + } + return proposal.startBlock; + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +}