Skip to content

Commit

Permalink
Merge pull request #124 from ourzora/v2-mitigrations-add-gov-timelock
Browse files Browse the repository at this point in the history
Add governance delay
  • Loading branch information
neokry authored Jan 2, 2024
2 parents bd2ec73 + 91c37b7 commit cb0da8d
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 22 deletions.
29 changes: 15 additions & 14 deletions .storage-layout
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,21 @@
➡ Governor
=======================

| Name | Type | Slot | Offset | Bytes | Contract |
|--------------------------|-----------------------------------------------------|------|--------|-------|-----------------------------------------------|
| _initialized | uint8 | 0 | 0 | 1 | src/governance/governor/Governor.sol:Governor |
| _initializing | bool | 0 | 1 | 1 | src/governance/governor/Governor.sol:Governor |
| _owner | address | 0 | 2 | 20 | src/governance/governor/Governor.sol:Governor |
| _pendingOwner | address | 1 | 0 | 20 | src/governance/governor/Governor.sol:Governor |
| HASHED_NAME | bytes32 | 2 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| HASHED_VERSION | bytes32 | 3 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| INITIAL_DOMAIN_SEPARATOR | bytes32 | 4 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| INITIAL_CHAIN_ID | uint256 | 5 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| nonces | mapping(address => uint256) | 6 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| settings | struct GovernorTypesV1.Settings | 7 | 0 | 96 | src/governance/governor/Governor.sol:Governor |
| proposals | mapping(bytes32 => struct GovernorTypesV1.Proposal) | 10 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| hasVoted | mapping(bytes32 => mapping(address => bool)) | 11 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| Name | Type | Slot | Offset | Bytes | Contract |
|--------------------------------------|-----------------------------------------------------|------|--------|-------|-----------------------------------------------|
| _initialized | uint8 | 0 | 0 | 1 | src/governance/governor/Governor.sol:Governor |
| _initializing | bool | 0 | 1 | 1 | src/governance/governor/Governor.sol:Governor |
| _owner | address | 0 | 2 | 20 | src/governance/governor/Governor.sol:Governor |
| _pendingOwner | address | 1 | 0 | 20 | src/governance/governor/Governor.sol:Governor |
| HASHED_NAME | bytes32 | 2 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| HASHED_VERSION | bytes32 | 3 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| INITIAL_DOMAIN_SEPARATOR | bytes32 | 4 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| INITIAL_CHAIN_ID | uint256 | 5 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| nonces | mapping(address => uint256) | 6 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| settings | struct GovernorTypesV1.Settings | 7 | 0 | 96 | src/governance/governor/Governor.sol:Governor |
| proposals | mapping(bytes32 => struct GovernorTypesV1.Proposal) | 10 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| hasVoted | mapping(bytes32 => mapping(address => bool)) | 11 | 0 | 32 | src/governance/governor/Governor.sol:Governor |
| delayedGovernanceExpirationTimestamp | uint256 | 12 | 0 | 32 | src/governance/governor/Governor.sol:Governor |

=======================
➡ Treasury
Expand Down
10 changes: 8 additions & 2 deletions src/deployers/L2MigrationDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.16;

import { IManager } from "../manager/IManager.sol";
import { IToken } from "../token/IToken.sol";
import { IGovernor } from "../governance/governor/IGovernor.sol";
import { IPropertyIPFSMetadataRenderer } from "../token/metadata/interfaces/IPropertyIPFSMetadataRenderer.sol";
import { MerkleReserveMinter } from "../minters/MerkleReserveMinter.sol";
import { TokenTypesV2 } from "../token/types/TokenTypesV2.sol";
Expand Down Expand Up @@ -89,19 +90,24 @@ contract L2MigrationDeployer {
/// @param _auctionParams The auction settings
/// @param _govParams The governance settings
/// @param _minterParams The minter settings
/// @param _delayedGovernanceAmount The amount of time to delay governance by
function deploy(
IManager.FounderParams[] calldata _founderParams,
IManager.TokenParams calldata _tokenParams,
IManager.AuctionParams calldata _auctionParams,
IManager.GovParams calldata _govParams,
MerkleReserveMinter.MerkleMinterSettings calldata _minterParams
MerkleReserveMinter.MerkleMinterSettings calldata _minterParams,
uint256 _delayedGovernanceAmount
) external returns (address token) {
if (_getTokenFromSender() != address(0)) {
revert DAO_ALREADY_DEPLOYED();
}

// Deploy the DAO
(address _token, , , , ) = IManager(manager).deploy(_founderParams, _tokenParams, _auctionParams, _govParams);
(address _token, , , , address _governor) = IManager(manager).deploy(_founderParams, _tokenParams, _auctionParams, _govParams);

// Set the governance expiration
IGovernor(_governor).updateDelayedGovernanceExpirationTimestamp(block.timestamp + _delayedGovernanceAmount);

// Setup minter settings to use the redeem minter
TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1);
Expand Down
35 changes: 34 additions & 1 deletion src/governance/governor/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EIP712 } from "../../lib/utils/EIP712.sol";
import { SafeCast } from "../../lib/utils/SafeCast.sol";

import { GovernorStorageV1 } from "./storage/GovernorStorageV1.sol";
import { GovernorStorageV2 } from "./storage/GovernorStorageV2.sol";
import { Token } from "../../token/Token.sol";
import { Treasury } from "../treasury/Treasury.sol";
import { IManager } from "../../manager/IManager.sol";
Expand All @@ -21,7 +22,7 @@ import { VersionedContract } from "../../VersionedContract.sol";
/// Modified from:
/// - OpenZeppelin Contracts v4.7.3 (governance/extensions/GovernorTimelockControl.sol)
/// - NounsDAOLogicV1.sol commit 2cbe6c7 - licensed under the BSD-3-Clause license.
contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, ProposalHasher, GovernorStorageV1 {
contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, ProposalHasher, GovernorStorageV1, GovernorStorageV2 {
/// ///
/// IMMUTABLES ///
/// ///
Expand Down Expand Up @@ -53,6 +54,9 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos
/// @notice The maximum voting period setting
uint256 public immutable MAX_VOTING_PERIOD = 24 weeks;

/// @notice The maximum delayed governance expiration setting
uint256 public immutable MAX_DELAYED_GOVERNANCE_EXPIRATION = 30 days;

/// @notice The basis points for 100%
uint256 private immutable BPS_PER_100_PERCENT = 10_000;

Expand Down Expand Up @@ -137,6 +141,11 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos
bytes[] memory _calldatas,
string memory _description
) external returns (bytes32) {
// Ensure governance is not delayed or all reserved tokens have been minted
if (block.timestamp < delayedGovernanceExpirationTimestamp && settings.token.remainingTokensInReserve() > 0) {
revert WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION();
}

// Get the current proposal threshold
uint256 currentProposalThreshold = proposalThreshold();

Expand Down Expand Up @@ -629,6 +638,30 @@ contract Governor is IGovernor, VersionedContract, UUPS, Ownable, EIP712, Propos
settings.quorumThresholdBps = uint16(_newQuorumVotesBps);
}

/// @notice Updates the delayed governance expiration timestamp
/// @param _newDelayedTimestamp The new delayed governance expiration timestamp
function updateDelayedGovernanceExpirationTimestamp(uint256 _newDelayedTimestamp) external {
// We want the founder to be able to set a governance delay if they are using a minter contract like MerkleReserveMinter
if (msg.sender != settings.token.owner()) {
revert ONLY_TOKEN_OWNER();
}

// Ensure the new timestamp is not too far in the future
if (_newDelayedTimestamp > block.timestamp + MAX_DELAYED_GOVERNANCE_EXPIRATION) {
revert INVALID_DELAYED_GOVERNANCE_EXPIRATION();
}

// Delay should only be set if no tokens have been minted to prevent active DAOs from accidentally or malicously enabling this funcationality
// Delay is only availible for DAOs that have reserved tokens
if (settings.token.totalSupply() > 0 || settings.token.reservedUntilTokenId() == 0) {
revert CANNOT_DELAY_GOVERNANCE();
}

emit DelayedGovernanceExpirationTimestampUpdated(delayedGovernanceExpirationTimestamp, _newDelayedTimestamp);

delayedGovernanceExpirationTimestamp = _newDelayedTimestamp;
}

/// @notice Updates the vetoer
/// @param _newVetoer The new vetoer address
function updateVetoer(address _newVetoer) external onlyOwner {
Expand Down
18 changes: 18 additions & 0 deletions src/governance/governor/IGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 {
//// @notice Emitted when the governor's vetoer is updated
event VetoerUpdated(address prevVetoer, address newVetoer);

/// @notice Emitted when the governor's delay is updated
event DelayedGovernanceExpirationTimestampUpdated(uint256 prevTimestamp, uint256 newTimestamp);

/// ///
/// ERRORS ///
/// ///
Expand All @@ -69,6 +72,8 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 {

error INVALID_VOTING_PERIOD();

error INVALID_DELAYED_GOVERNANCE_EXPIRATION();

/// @dev Reverts if a proposal already exists
/// @param proposalId The proposal id
error PROPOSAL_EXISTS(bytes32 proposalId);
Expand Down Expand Up @@ -110,6 +115,15 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 {
/// @dev Reverts if a vote was attempted to be casted incorrectly
error INVALID_VOTE();

/// @dev Reverts if a proposal was attempted to be created before expiration or all tokens have been claimed
error WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION();

/// @dev Reverts if governance cannot be delayed
error CANNOT_DELAY_GOVERNANCE();

/// @dev Reverts if the caller was not the token owner
error ONLY_TOKEN_OWNER();

/// @dev Reverts if the caller was not the contract manager
error ONLY_MANAGER();

Expand Down Expand Up @@ -285,6 +299,10 @@ interface IGovernor is IUUPS, IOwnable, IEIP712, GovernorTypesV1 {
/// @param newQuorumVotesBps The new quorum votes basis points
function updateQuorumThresholdBps(uint256 newQuorumVotesBps) external;

/// @notice Updates the delayed governance expiration timestamp
/// @param _newDelayedTimestamp The new delayed governance expiration timestamp
function updateDelayedGovernanceExpirationTimestamp(uint256 _newDelayedTimestamp) external;

/// @notice Updates the vetoer
/// @param newVetoer The new vetoer addresss
function updateVetoer(address newVetoer) external;
Expand Down
10 changes: 10 additions & 0 deletions src/governance/governor/storage/GovernorStorageV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;

/// @title GovernorStorageV2
/// @author Neokry
/// @notice The Governor storage contract
contract GovernorStorageV2 {
/// @notice The delayed governance expiration timestamp
uint256 public delayedGovernanceExpirationTimestamp;
}
3 changes: 3 additions & 0 deletions src/token/IToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ interface IToken is IUUPS, IERC721Votes, TokenTypesV1, TokenTypesV2 {
/// @param tokenId The ERC-721 token id
function getScheduledRecipient(uint256 tokenId) external view returns (Founder memory);

/// @notice The total number of tokens that can be claimed from the reserve
function remainingTokensInReserve() external view returns (uint256);

/// @notice The total supply of tokens
function totalSupply() external view returns (uint256);

Expand Down
9 changes: 9 additions & 0 deletions src/token/Token.sol
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,15 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC
return settings.totalSupply;
}

/// @notice The total number of tokens that can be claimed from the reserve
function remainingTokensInReserve() external view returns (uint256) {
// total supply - total minted from auctions and airdrops
uint256 totalMintedFromReserve = settings.totalSupply - settings.mintCount;

// reservedUntilTokenId is also the total number of tokens in the reserve since tokens 0 -> reservedUntilTokenId - 1 are reserved
return reservedUntilTokenId - totalMintedFromReserve;
}

/// @notice The address of the auction house
function auction() external view returns (address) {
return settings.auction;
Expand Down
134 changes: 132 additions & 2 deletions test/Gov.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol";
import { IManager } from "../src/manager/IManager.sol";
import { IGovernor } from "../src/governance/governor/IGovernor.sol";
import { GovernorTypesV1 } from "../src/governance/governor/types/GovernorTypesV1.sol";
import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol";

contract GovTest is NounsBuilderTest, GovernorTypesV1 {
uint256 internal constant AGAINST = 0;
Expand Down Expand Up @@ -80,6 +81,36 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 {
setMockMetadata();
}

function deployMockWithDelay(uint256 delay) internal {
address[] memory wallets = new address[](2);
uint256[] memory percents = new uint256[](2);
uint256[] memory vestingEnd = new uint256[](2);

wallets[0] = founder;
wallets[1] = founder2;

percents[0] = 1;
percents[1] = 1;

vestingEnd[0] = 4 weeks;
vestingEnd[1] = 4 weeks;

setFounderParams(wallets, percents, vestingEnd);

setMockTokenParamsWithReserve(2);

setAuctionParams(0, 1 days, address(0), 0);

setGovParams(2 days, 1 days, 1 weeks, 25, 1000, founder);

deploy(foundersArr, tokenParams, auctionParams, govParams);

vm.prank(founder);
governor.updateDelayedGovernanceExpirationTimestamp(block.timestamp + delay);

setMockMetadata();
}

function createVoter1() internal {
voter1PK = 0xABE;
voter1 = vm.addr(voter1PK);
Expand All @@ -98,17 +129,21 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 {
vm.prank(founder);
auction.unpause();

(uint256 tokenId, , , , , ) = auction.auction();

vm.prank(voter1);
auction.createBid{ value: 0.420 ether }(2);
auction.createBid{ value: 0.420 ether }(tokenId);

vm.warp(block.timestamp + auctionParams.duration + 1 seconds);
auction.settleCurrentAndCreateNewAuction();
vm.warp(block.timestamp + 20);
}

function mintVoter2() internal {
(uint256 tokenId, , , , , ) = auction.auction();

vm.prank(voter2);
auction.createBid{ value: 0.420 ether }(3);
auction.createBid{ value: 0.420 ether }(tokenId);

vm.warp(block.timestamp + auctionParams.duration + 1 seconds);
auction.settleCurrentAndCreateNewAuction();
Expand Down Expand Up @@ -1125,4 +1160,99 @@ contract GovTest is NounsBuilderTest, GovernorTypesV1 {

mock1155.mintBatch(address(governor), _tokenIds, _amounts);
}

function testRevert_GovernorOnlyDAOWithReserveCanAddDelay() public {
deployMock();

vm.prank(founder);
vm.expectRevert(abi.encodeWithSignature("CANNOT_DELAY_GOVERNANCE()"));
governor.updateDelayedGovernanceExpirationTimestamp(1 days);
}

function testRevert_GovernorOnlyTokenOwnerCanSetDelay() public {
deployMock();

vm.prank(voter1);
vm.expectRevert(abi.encodeWithSignature("ONLY_TOKEN_OWNER()"));
governor.updateDelayedGovernanceExpirationTimestamp(1 days);
}

function testRevert_GovernorCannotSetDelayPastMax() public {
deployMock();

vm.prank(founder);
vm.expectRevert(abi.encodeWithSignature("INVALID_DELAYED_GOVERNANCE_EXPIRATION()"));
governor.updateDelayedGovernanceExpirationTimestamp(31 days);
}

function testRevert_GovernorCannotSetDelayAfterTokensAreMinted() public {
deployMock();

vm.prank(founder);
token.setReservedUntilTokenId(4);

vm.prank(founder);
auction.unpause();

vm.prank(address(treasury));
vm.expectRevert(abi.encodeWithSignature("CANNOT_DELAY_GOVERNANCE()"));
governor.updateDelayedGovernanceExpirationTimestamp(1 days);
}

function testRevert_GovernorCannotProposeInDelayPeriod() public {
deployMockWithDelay(block.timestamp + 7 days);

mintVoter1();

(address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal();

vm.warp(block.timestamp + 1 days);

vm.prank(voter1);
vm.expectRevert(abi.encodeWithSignature("WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION()"));
governor.propose(targets, values, calldatas, "test");
}

function test_GovernorCanProposeAfterDelayPeriod() public {
deployMockWithDelay(block.timestamp + 7 days);

mintVoter1();

(address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal();

vm.prank(voter1);
vm.expectRevert(abi.encodeWithSignature("WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION()"));
governor.propose(targets, values, calldatas, "test");

vm.warp(block.timestamp + 8 days);

vm.prank(voter1);
governor.propose(targets, values, calldatas, "test");
}

function test_GovernorCanProposeAfterReserveIsMinted() public {
deployMockWithDelay(block.timestamp + 7 days);

mintVoter1();

(address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal();

vm.prank(voter1);
vm.expectRevert(abi.encodeWithSignature("WAITING_FOR_TOKENS_TO_CLAIM_OR_EXPIRATION()"));
governor.propose(targets, values, calldatas, "test");

TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1);
minters[0] = TokenTypesV2.MinterParams({ minter: founder, allowed: true });

vm.prank(token.owner());
token.updateMinters(minters);

vm.startPrank(address(founder));
token.mintFromReserveTo(address(founder), 0);
token.mintFromReserveTo(address(founder), 1);
vm.stopPrank();

vm.prank(voter1);
governor.propose(targets, values, calldatas, "test");
}
}
Loading

0 comments on commit cb0da8d

Please sign in to comment.