diff --git a/packages/nouns-contracts/.gas-snapshot b/packages/nouns-contracts/.gas-snapshot index 74f2b93661..21eb281055 100644 --- a/packages/nouns-contracts/.gas-snapshot +++ b/packages/nouns-contracts/.gas-snapshot @@ -1,11 +1,25 @@ -NounsDAOLogic_GasSnapshot_V2_propose:test_propose_longDescription() (gas: 528733) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createOneBid() (gas: 34941) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createTwoBids() (gas: 93380) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 232656) +NounsAuctionHouseV2_GasSnapshot:test_createOneBid() (gas: 34941) +NounsAuctionHouseV2_GasSnapshot:test_createTwoBids() (gas: 93380) +NounsAuctionHouseV2_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 249756) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getPrices_90() (gas: 309530) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getPrices_range_90() (gas: 299984) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getSettlements_90() (gas: 385118) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getSettlements_range_90() (gas: 377552) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_warmUp() (gas: 18716918) +NounsAuctionHouse_GasSnapshot:test_createOneBid() (gas: 81474) +NounsAuctionHouse_GasSnapshot:test_createTwoBids() (gas: 142736) +NounsAuctionHouse_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 243008) +NounsDAOLogic_GasSnapshot_V2_propose:test_propose_longDescription() (gas: 528755) NounsDAOLogic_GasSnapshot_V2_propose:test_propose_shortDescription() (gas: 398388) -NounsDAOLogic_GasSnapshot_V2_vote:test_castVoteWithReason() (gas: 83474) -NounsDAOLogic_GasSnapshot_V2_vote:test_castVote_against() (gas: 82886) -NounsDAOLogic_GasSnapshot_V2_vote:test_castVote_lastMinuteFor() (gas: 83459) -NounsDAOLogic_GasSnapshot_V3_propose:test_propose_longDescription() (gas: 538195) -NounsDAOLogic_GasSnapshot_V3_propose:test_propose_shortDescription() (gas: 404029) -NounsDAOLogic_GasSnapshot_V3_vote:test_castVoteWithReason() (gas: 89809) -NounsDAOLogic_GasSnapshot_V3_vote:test_castVote_against() (gas: 88733) -NounsDAOLogic_GasSnapshot_V3_vote:test_castVote_lastMinuteFor() (gas: 112249) -NounsDAOLogic_GasSnapshot_V3_voteDuringObjectionPeriod:test_castVote_duringObjectionPeriod_against() (gas: 88656) \ No newline at end of file +NounsDAOLogic_GasSnapshot_V2_vote:test_castVoteWithReason() (gas: 83585) +NounsDAOLogic_GasSnapshot_V2_vote:test_castVote_against() (gas: 82930) +NounsDAOLogic_GasSnapshot_V2_vote:test_castVote_lastMinuteFor() (gas: 83481) +NounsDAOLogic_GasSnapshot_V3_propose:test_propose_longDescription() (gas: 538397) +NounsDAOLogic_GasSnapshot_V3_propose:test_propose_shortDescription() (gas: 404209) +NounsDAOLogic_GasSnapshot_V3_vote:test_castVoteWithReason() (gas: 89882) +NounsDAOLogic_GasSnapshot_V3_vote:test_castVote_against() (gas: 88717) +NounsDAOLogic_GasSnapshot_V3_vote:test_castVote_lastMinuteFor() (gas: 112210) +NounsDAOLogic_GasSnapshot_V3_voteDuringObjectionPeriod:test_castVote_duringObjectionPeriod_against() (gas: 88702) \ No newline at end of file diff --git a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol new file mode 100644 index 0000000000..f2eb7582f8 --- /dev/null +++ b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title An interim contract for storage migration between V1 and V2 + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { PausableUpgradeable } from '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; +import { ReentrancyGuardUpgradeable } from '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol'; +import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; +import { INounsAuctionHouse } from './interfaces/INounsAuctionHouse.sol'; +import { INounsAuctionHouseV2 } from './interfaces/INounsAuctionHouseV2.sol'; + +contract NounsAuctionHousePreV2Migration is PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable { + struct OldLayout { + address nouns; + address weth; + uint256 timeBuffer; + uint256 reservePrice; + uint8 minBidIncrementPercentage; + uint256 duration; + INounsAuctionHouse.Auction auction; + } + + struct NewLayout { + uint192 reservePrice; + uint56 timeBuffer; + uint8 minBidIncrementPercentage; + INounsAuctionHouseV2.AuctionV2 auction; + } + + uint256 private startSlot; + + function migrate() public onlyOwner { + OldLayout storage oldLayout = _oldLayout(); + NewLayout storage newLayout = _newLayout(); + OldLayout memory oldLayoutCache = oldLayout; + + // Clear the old storage layout + oldLayout.nouns = address(0); + oldLayout.weth = address(0); + oldLayout.timeBuffer = 0; + oldLayout.reservePrice = 0; + oldLayout.minBidIncrementPercentage = 0; + oldLayout.duration = 0; + oldLayout.auction = INounsAuctionHouse.Auction(0, 0, 0, 0, payable(0), false); + + // Populate the new layout from the cache + newLayout.reservePrice = uint192(oldLayoutCache.reservePrice); + newLayout.timeBuffer = uint56(oldLayoutCache.timeBuffer); + newLayout.minBidIncrementPercentage = oldLayoutCache.minBidIncrementPercentage; + newLayout.auction = INounsAuctionHouseV2.AuctionV2({ + nounId: uint128(oldLayoutCache.auction.nounId), + amount: uint128(oldLayoutCache.auction.amount), + startTime: uint40(oldLayoutCache.auction.startTime), + endTime: uint40(oldLayoutCache.auction.endTime), + bidder: oldLayoutCache.auction.bidder, + settled: oldLayoutCache.auction.settled + }); + } + + function _oldLayout() internal pure returns (OldLayout storage layout) { + assembly { + layout.slot := startSlot.slot + } + } + + function _newLayout() internal pure returns (NewLayout storage layout) { + assembly { + layout.slot := startSlot.slot + } + } +} diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol new file mode 100644 index 0000000000..a335e7ec1d --- /dev/null +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -0,0 +1,517 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title The Nouns DAO auction house + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +// LICENSE +// NounsAuctionHouse.sol is a modified version of Zora's AuctionHouse.sol: +// https://github.com/ourzora/auction-house/blob/54a12ec1a6cf562e49f0a4917990474b11350a2d/contracts/AuctionHouse.sol +// +// AuctionHouse.sol source code Copyright Zora licensed under the GPL-3.0 license. +// With modifications by Nounders DAO. + +pragma solidity ^0.8.19; + +import { PausableUpgradeable } from '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; +import { ReentrancyGuardUpgradeable } from '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol'; +import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { INounsAuctionHouseV2 } from './interfaces/INounsAuctionHouseV2.sol'; +import { INounsToken } from './interfaces/INounsToken.sol'; +import { IWETH } from './interfaces/IWETH.sol'; + +/** + * @dev The contract inherits from PausableUpgradeable & ReentrancyGuardUpgradeable most of all the keep the same + * storage layout as the NounsAuctionHouse contract + */ +contract NounsAuctionHouseV2 is + INounsAuctionHouseV2, + PausableUpgradeable, + ReentrancyGuardUpgradeable, + OwnableUpgradeable +{ + /// @notice A hard-coded cap on time buffer to prevent accidental auction disabling if set with a very high value. + uint56 public constant MAX_TIME_BUFFER = 1 days; + + /// @notice The Nouns ERC721 token contract + INounsToken public immutable nouns; + + /// @notice The address of the WETH contract + address public immutable weth; + + /// @notice The duration of a single auction + uint256 public immutable duration; + + /// @notice The minimum price accepted in an auction + uint192 public reservePrice; + + /// @notice The minimum amount of time left in an auction after a new bid is created + uint56 public timeBuffer; + + /// @notice The minimum percentage difference between the last bid amount and the current bid + uint8 public minBidIncrementPercentage; + + /// @notice The active auction + INounsAuctionHouseV2.AuctionV2 public auctionStorage; + + /// @notice The Nouns price feed state + mapping(uint256 => SettlementState) settlementHistory; + + constructor( + INounsToken _nouns, + address _weth, + uint256 _duration + ) { + nouns = _nouns; + weth = _weth; + duration = _duration; + } + + /** + * @notice Initialize the auction house and base contracts, + * populate configuration values, and pause the contract. + * @dev This function can only be called once. + */ + function initialize( + uint192 _reservePrice, + uint56 _timeBuffer, + uint8 _minBidIncrementPercentage + ) external initializer { + __Pausable_init(); + __ReentrancyGuard_init(); + __Ownable_init(); + + _pause(); + + reservePrice = _reservePrice; + timeBuffer = _timeBuffer; + minBidIncrementPercentage = _minBidIncrementPercentage; + } + + /** + * @notice Settle the current auction, mint a new Noun, and put it up for auction. + */ + function settleCurrentAndCreateNewAuction() external override whenNotPaused { + _settleAuction(); + _createAuction(); + } + + /** + * @notice Settle the current auction. + * @dev This function can only be called when the contract is paused. + */ + function settleAuction() external override whenPaused { + _settleAuction(); + } + + /** + * @notice Create a bid for a Noun, with a given amount. + * @dev This contract only accepts payment in ETH. + */ + function createBid(uint256 nounId) external payable override { + INounsAuctionHouseV2.AuctionV2 memory _auction = auctionStorage; + + (uint192 _reservePrice, uint56 _timeBuffer, uint8 _minBidIncrementPercentage) = ( + reservePrice, + timeBuffer, + minBidIncrementPercentage + ); + + require(_auction.nounId == nounId, 'Noun not up for auction'); + require(block.timestamp < _auction.endTime, 'Auction expired'); + require(msg.value >= _reservePrice, 'Must send at least reservePrice'); + require( + msg.value >= _auction.amount + ((_auction.amount * _minBidIncrementPercentage) / 100), + 'Must send more than last bid by minBidIncrementPercentage amount' + ); + + auctionStorage.amount = uint128(msg.value); + auctionStorage.bidder = payable(msg.sender); + + // Extend the auction if the bid was received within `timeBuffer` of the auction end time + bool extended = _auction.endTime - block.timestamp < _timeBuffer; + + emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); + + if (extended) { + auctionStorage.endTime = _auction.endTime = uint40(block.timestamp + _timeBuffer); + emit AuctionExtended(_auction.nounId, _auction.endTime); + } + + address payable lastBidder = _auction.bidder; + + // Refund the last bidder, if applicable + if (lastBidder != address(0)) { + _safeTransferETHWithFallback(lastBidder, _auction.amount); + } + } + + /** + * @notice Get the current auction. + */ + function auction() external view returns (AuctionV2 memory) { + return auctionStorage; + } + + /** + * @notice Pause the Nouns auction house. + * @dev This function can only be called by the owner when the + * contract is unpaused. While no new auctions can be started when paused, + * anyone can settle an ongoing auction. + */ + function pause() external override onlyOwner { + _pause(); + } + + /** + * @notice Unpause the Nouns auction house. + * @dev This function can only be called by the owner when the + * contract is paused. If required, this function will start a new auction. + */ + function unpause() external override onlyOwner { + _unpause(); + + if (auctionStorage.startTime == 0 || auctionStorage.settled) { + _createAuction(); + } + } + + /** + * @notice Set the auction time buffer. + * @dev Only callable by the owner. + */ + function setTimeBuffer(uint56 _timeBuffer) external override onlyOwner { + require(_timeBuffer <= MAX_TIME_BUFFER, 'timeBuffer too large'); + + timeBuffer = _timeBuffer; + + emit AuctionTimeBufferUpdated(_timeBuffer); + } + + /** + * @notice Set the auction reserve price. + * @dev Only callable by the owner. + */ + function setReservePrice(uint192 _reservePrice) external override onlyOwner { + reservePrice = _reservePrice; + + emit AuctionReservePriceUpdated(_reservePrice); + } + + /** + * @notice Set the auction minimum bid increment percentage. + * @dev Only callable by the owner. + */ + function setMinBidIncrementPercentage(uint8 _minBidIncrementPercentage) external override onlyOwner { + minBidIncrementPercentage = _minBidIncrementPercentage; + + emit AuctionMinBidIncrementPercentageUpdated(_minBidIncrementPercentage); + } + + /** + * @notice Create an auction. + * @dev Store the auction details in the `auction` state variable and emit an AuctionCreated event. + * If the mint reverts, the minter was updated without pausing this contract first. To remedy this, + * catch the revert and pause this contract. + */ + function _createAuction() internal { + try nouns.mint() returns (uint256 nounId) { + uint40 startTime = uint40(block.timestamp); + uint40 endTime = startTime + uint40(duration); + + auctionStorage = AuctionV2({ + nounId: uint128(nounId), + amount: 0, + startTime: startTime, + endTime: endTime, + bidder: payable(0), + settled: false + }); + + emit AuctionCreated(nounId, startTime, endTime); + } catch Error(string memory) { + _pause(); + } + } + + /** + * @notice Settle an auction, finalizing the bid and paying out to the owner. + * @dev If there are no bids, the Noun is burned. + */ + function _settleAuction() internal { + INounsAuctionHouseV2.AuctionV2 memory _auction = auctionStorage; + + require(_auction.startTime != 0, "Auction hasn't begun"); + require(!_auction.settled, 'Auction has already been settled'); + require(block.timestamp >= _auction.endTime, "Auction hasn't completed"); + + auctionStorage.settled = true; + + if (_auction.bidder == address(0)) { + nouns.burn(_auction.nounId); + } else { + nouns.transferFrom(address(this), _auction.bidder, _auction.nounId); + } + + if (_auction.amount > 0) { + _safeTransferETHWithFallback(owner(), _auction.amount); + } + + settlementHistory[_auction.nounId] = SettlementState({ + blockTimestamp: uint32(block.timestamp), + amount: ethPriceToUint64(_auction.amount), + winner: _auction.bidder + }); + + emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount); + } + + /** + * @notice Transfer ETH. If the ETH transfer fails, wrap the ETH and try send it as WETH. + */ + function _safeTransferETHWithFallback(address to, uint256 amount) internal { + if (!_safeTransferETH(to, amount)) { + IWETH(weth).deposit{ value: amount }(); + IERC20(weth).transfer(to, amount); + } + } + + /** + * @notice Transfer ETH and return the success status. + * @dev This function only forwards 30,000 gas to the callee. + */ + function _safeTransferETH(address to, uint256 value) internal returns (bool) { + bool success; + assembly { + success := call(30000, to, value, 0, 0, 0, 0) + } + return success; + } + + /** + * @notice Set historic prices; only callable by the owner, which in Nouns is the treasury (timelock) contract. + * @dev This function lowers auction price accuracy from 18 decimals to 10 decimals, as part of the price history + * bit packing, to save gas. + * @param settlements The list of historic prices to set. + */ + function setPrices(Settlement[] memory settlements) external onlyOwner { + uint256[] memory nounIds = new uint256[](settlements.length); + uint256[] memory prices_ = new uint256[](settlements.length); + + for (uint256 i = 0; i < settlements.length; ++i) { + settlementHistory[settlements[i].nounId] = SettlementState({ + blockTimestamp: settlements[i].blockTimestamp, + amount: ethPriceToUint64(settlements[i].amount), + winner: settlements[i].winner + }); + + nounIds[i] = settlements[i].nounId; + prices_[i] = settlements[i].amount; + } + + emit HistoricPricesSet(nounIds, prices_); + } + + /** + * @notice Warm up the settlement state for a list of Noun IDs. + * @dev Helps lower the gas cost of auction settlement when storing settlement data + * thanks to the state slot being non-zero. + * @dev Only writes to slots where blockTimestamp is zero, meaning it will not overwrite existing data. + * @param nounIds The list of Noun IDs whose settlement slot to warm up. + */ + function warmUpSettlementState(uint256[] calldata nounIds) external { + uint256 nounIdCount = nounIds.length; + SettlementState storage settlementState; + for (uint256 i; i < nounIdCount; ) { + settlementState = settlementHistory[nounIds[i]]; + if (settlementState.blockTimestamp == 0) { + settlementState.blockTimestamp = 1; + } + unchecked { + ++i; + } + } + } + + /** + * @notice Get past auction settlements. + * @dev Returns settlements in reverse order, meaning settlements[0] will be the most recent auction price. + * Skips auctions where there was no winner, i.e. no bids. + * @param auctionCount The number of price observations to get. + * @return settlements An array of type `Settlement`, where each Settlement includes a timestamp, + * the Noun ID of that auction, the winning bid amount, and the winner's address. + */ + function getSettlements(uint256 auctionCount) external view returns (Settlement[] memory settlements) { + uint256 latestNounId = auctionStorage.nounId; + if (!auctionStorage.settled && latestNounId > 0) { + latestNounId -= 1; + } + + settlements = new Settlement[](auctionCount); + uint256 actualCount = 0; + while (actualCount < auctionCount && latestNounId > 0) { + SettlementState memory settlementState = settlementHistory[latestNounId]; + // Skip Nouner reward Nouns, they have no price + // Also skips IDs with no price data + if (settlementState.winner == address(0)) { + --latestNounId; + continue; + } + + settlements[actualCount] = Settlement({ + blockTimestamp: settlementState.blockTimestamp, + amount: uint64PriceToUint256(settlementState.amount), + winner: settlementState.winner, + nounId: latestNounId + }); + ++actualCount; + --latestNounId; + } + + if (auctionCount > actualCount) { + // this assembly trims the observations array, getting rid of unused cells + assembly { + mstore(settlements, actualCount) + } + } + } + + /** + * @notice Get past auction prices. + * @dev Returns prices in reverse order, meaning prices[0] will be the most recent auction price. + * Skips auctions where there was no winner, i.e. no bids. + * @param auctionCount The number of price observations to get. + * @return prices An array of uint256 prices. + */ + function getPrices(uint256 auctionCount) external view returns (uint256[] memory prices) { + uint256 latestNounId = auctionStorage.nounId; + if (!auctionStorage.settled && latestNounId > 0) { + latestNounId -= 1; + } + + prices = new uint256[](auctionCount); + uint256 actualCount = 0; + while (actualCount < auctionCount && latestNounId > 0) { + SettlementState memory settlementState = settlementHistory[latestNounId]; + // Skip Nouner reward Nouns, they have no price + // Also skips IDs with no price data + if (settlementState.winner == address(0)) { + --latestNounId; + continue; + } + + prices[actualCount] = uint64PriceToUint256(settlementState.amount); + ++actualCount; + --latestNounId; + } + + if (auctionCount > actualCount) { + // this assembly trims the observations array, getting rid of unused cells + assembly { + mstore(prices, actualCount) + } + } + } + + /** + * @notice Get a range of past auction settlements. + * @dev Returns prices in chronological order, as opposed to `getSettlements(count)` which returns prices in reverse order. + * Skips auctions where there was no winner, i.e. no bids. + * @param startId the first Noun ID to get prices for. + * @param endId end Noun ID (up to, but not including). + * @return settlements An array of type `Settlement`, where each Settlement includes a timestamp, + * the Noun ID of that auction, the winning bid amount, and the winner's address. + */ + function getSettlements(uint256 startId, uint256 endId) external view returns (Settlement[] memory settlements) { + settlements = new Settlement[](endId - startId); + uint256 actualCount = 0; + uint256 currentId = startId; + while (currentId < endId) { + SettlementState memory settlementState = settlementHistory[currentId]; + // Skip Nouner reward Nouns, they have no price + // Also skips IDs with no price data + if (settlementState.winner == address(0)) { + ++currentId; + continue; + } + + settlements[actualCount] = Settlement({ + blockTimestamp: settlementState.blockTimestamp, + amount: uint64PriceToUint256(settlementState.amount), + winner: settlementState.winner, + nounId: currentId + }); + ++actualCount; + ++currentId; + } + + if (settlements.length > actualCount) { + // this assembly trims the observations array, getting rid of unused cells + assembly { + mstore(settlements, actualCount) + } + } + } + + /** + * @notice Get a range of past auction prices. + * @dev Returns prices in chronological order, as opposed to `getPrices(count)` which returns prices in reverse order. + * Skips auctions where there was no winner, i.e. no bids. + * @param startId the first Noun ID to get prices for. + * @param endId end Noun ID (up to, but not including). + * @return prices An array of uint256 prices. + */ + function getPrices(uint256 startId, uint256 endId) external view returns (uint256[] memory prices) { + prices = new uint256[](endId - startId); + uint256 actualCount = 0; + uint256 currentId = startId; + while (currentId < endId) { + SettlementState memory settlementState = settlementHistory[currentId]; + // Skip Nouner reward Nouns, they have no price + // Also skips IDs with no price data + if (settlementState.winner == address(0)) { + ++currentId; + continue; + } + + prices[actualCount] = uint64PriceToUint256(settlementState.amount); + ++actualCount; + ++currentId; + } + + if (prices.length > actualCount) { + // this assembly trims the observations array, getting rid of unused cells + assembly { + mstore(prices, actualCount) + } + } + } + + /** + * @dev Convert an ETH price of 256 bits with 18 decimals, to 64 bits with 10 decimals. + * Max supported value is 1844674407.3709551615 ETH. + * + */ + function ethPriceToUint64(uint256 ethPrice) internal pure returns (uint64) { + return uint64(ethPrice / 1e8); + } + + /** + * @dev Convert a 64 bit 10 decimal price to a 256 bit 18 decimal price. + */ + function uint64PriceToUint256(uint64 price) internal pure returns (uint256) { + return uint256(price) * 1e8; + } +} diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol new file mode 100644 index 0000000000..b6835f4050 --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: GPL-3.0 + +/** + * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * + */ + +pragma solidity ^0.8.19; + +import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { INounsAuctionHouseV2 } from '../interfaces/INounsAuctionHouseV2.sol'; + +interface RocketETH { + function getEthValue(uint256 _rethAmount) external view returns (uint256); +} + +interface INounsDAOV3 { + function adjustedTotalSupply() external view returns (uint256); +} + +interface IExecutorV3 { + function burnExcessETH(uint256 amount) external; +} + +/** + * @title ExcessETH Burner + * @notice A helper contract for burning Nouns excess ETH with NounsDAOExecutorV3. + * @dev Owner is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. + */ +contract ExcessETHBurner is Ownable { + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * ERRORS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + error NotTimeToBurnYet(); + error NoExcessToBurn(); + error NotEnoughAuctionHistory(); + error PastAuctionCountTooLow(); + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * EVENTS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + event AuctionSet(address oldAuction, address newAuction); + event InitialBurnNounIdSet(uint128 initialBurnNounId, uint128 newInitialBurnNounId); + event NounIdsBetweenBurnsSet(uint128 nounIdsBetweenBurns, uint128 newNounIdsBetweenBurns); + event BurnWindowSizeSet(uint16 burnWindowSize, uint16 newBurnWindowSize); + event NumberOfPastAuctionsForMeanPriceSet( + uint16 oldNumberOfPastAuctionsForMeanPrice, + uint16 newNumberOfPastAuctionsForMeanPrice + ); + event Burn(uint256 amount, uint128 currentBurnWindowStart, uint128 currentNounId, uint128 newInitialBurnNounId); + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * STATE + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + uint256 public constant MIN_PAST_AUCTIONS = 2; + + INounsDAOV3 public immutable dao; + INounsAuctionHouseV2 public immutable auction; + IERC20 public immutable wETH; + IERC20 public immutable stETH; + IERC20 public immutable rETH; + uint64 public initialBurnNounId; + uint64 public nounIdsBetweenBurns; + uint16 public burnWindowSize; + uint16 public numberOfPastAuctionsForMeanPrice; + + /** + * @param owner_ A NounsDAOExecutorV3 instance + * @param dao_ The DAO proxy contract + * @param auction_ The Auction House proxy contract + * @param wETH_ Address of WETH token + * @param stETH_ Address of Lido stETH token + * @param rETH_ Address of RocketPool RETH token + * @param initialBurnNounId_ The lowest noun id at which a burn can be triggered + * @param nounIdsBetweenBurns_ Number of nouns that need to be minted between burn windows + * @param burnWindowSize_ Number of nouns in a burn window + * @param numberOfPastAuctionsForMeanPrice_ Number of past auctions to consider when calculating mean price + */ + constructor( + address owner_, + INounsDAOV3 dao_, + INounsAuctionHouseV2 auction_, + IERC20 wETH_, + IERC20 stETH_, + IERC20 rETH_, + uint64 initialBurnNounId_, + uint64 nounIdsBetweenBurns_, + uint16 burnWindowSize_, + uint16 numberOfPastAuctionsForMeanPrice_ + ) { + _transferOwnership(owner_); + + dao = dao_; + auction = auction_; + wETH = wETH_; + stETH = stETH_; + rETH = rETH_; + initialBurnNounId = initialBurnNounId_; + nounIdsBetweenBurns = nounIdsBetweenBurns_; + burnWindowSize = burnWindowSize_; + numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * PUBLIC FUNCTIONS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Burn excess ETH in the Nouns treasury. + * Allows the burn to occur every `minNewNounsBetweenBurns` new Nouns minted. + * For example, if `minNewNounsBetweenBurns` is 100, and the first `nextBurnNounID` is 1000, then the following + * Nouns IDs are allowed to burn excess ETH: 1100, 1200, 1300, etc. + * + * See `excessETH()` for more information on how excess ETH is defined. + * @dev Reverts when auction house has not yet minted the next Noun ID at which the burn is allowed. + */ + function burnExcessETH() public returns (uint256 amount) { + uint128 currentNounId = currentlyAuctionedNounId(); + + // Make sure this is a valid noun id. This will revert if this id doesn't exist + auction.nouns().ownerOf(currentNounId); + + uint64 nounIdsBetweenBurns_ = nounIdsBetweenBurns; + uint64 initialBurnNounId_ = initialBurnNounId; + if (!isInBurnWindow(currentNounId, initialBurnNounId_, nounIdsBetweenBurns_)) revert NotTimeToBurnYet(); + uint64 newInitialBurnNounId = nextBurnWindowStart( + uint64(currentNounId), + initialBurnNounId_, + nounIdsBetweenBurns_ + ); + initialBurnNounId = newInitialBurnNounId; + + amount = excessETH(); + if (amount == 0) revert NoExcessToBurn(); + + IExecutorV3(owner()).burnExcessETH(amount); + + emit Burn(amount, newInitialBurnNounId - nounIdsBetweenBurns_, currentNounId, newInitialBurnNounId); + } + + /** + * @notice Returns the id of the noun currently being auctioned + */ + function currentlyAuctionedNounId() public view returns (uint128) { + return auction.auction().nounId; + } + + /** + * @notice Returns true if `nounId` is within a burn window. + * A burn window can start at `initialBurnNounId_` + N * `nounIdsBetweenBurns_` for any N >= 0. + * The window size is defined by `burnWindowSize`. + */ + function isInBurnWindow( + uint256 nounId, + uint64 initialBurnNounId_, + uint64 nounIdsBetweenBurns_ + ) public view returns (bool) { + if (nounId < initialBurnNounId_) return false; + + uint256 distanceFromBurnWindowStart = (nounId - initialBurnNounId_) % nounIdsBetweenBurns_; + + return distanceFromBurnWindowStart <= burnWindowSize; + } + + /** + * @notice Returns the next burn window start id. + * For a given `currentNounId`, it returns the next id which can be represented by + * `initialBurnNounId_` + N * `nounIdsBetweenBurns_` for any N >= 0. + */ + function nextBurnWindowStart( + uint64 currentNounId, + uint64 initialBurnNounId_, + uint64 nounIdsBetweenBurns_ + ) public pure returns (uint64) { + // this could not happen during a burn. here as convenience. + if (currentNounId < initialBurnNounId_) return initialBurnNounId_; + + uint64 distanceFromBurnWindowStart = (currentNounId - initialBurnNounId_) % nounIdsBetweenBurns_; + return currentNounId - distanceFromBurnWindowStart + nounIdsBetweenBurns_; + } + + /** + * @notice Get the amount of excess ETH in the Nouns treasury. + * Excess ETH is defined as the difference between the current treasury value denominated in ETH, + * and the expected treasury value denominated in ETH, which is defined as mean auction price * adjusted total supply. + * It returns zero during the waiting period, and when expected treasury value is greater than current value. + * If treasury balance in native ETH is less than the excess ETH calculation, it returns the treasury ETH balance. + * @dev This version does not support burning excess other than in native ETH, e.g. stETH, rETH, etc. + * @dev Reverts if there is not enough auction history to calculate the mean auction price. + */ + function excessETH() public view returns (uint256) { + uint256 expectedTreasuryValue = expectedTreasuryValueInETH(); + uint256 treasuryValue = treasuryValueInETH(); + + if (expectedTreasuryValue >= treasuryValue) return 0; + + return min(treasuryValue - expectedTreasuryValue, owner().balance); + } + + /** + * @notice Get the expected treasury value denomiated in ETH. + * Expected value is defined as mean auction price * adjusted total supply. + */ + function expectedTreasuryValueInETH() public view returns (uint256) { + return meanAuctionPrice() * dao.adjustedTotalSupply(); + } + + /** + * @notice Get the current treasury value denomiated in ETH. + * In addition to native ETH, it also includes stETH, wETH, and rETH balances. + * @dev for rETH, it uses the getEthValue() function to convert rETH to ETH. + */ + function treasuryValueInETH() public view returns (uint256) { + address owner_ = owner(); + return owner_.balance + stETH.balanceOf(owner_) + wETH.balanceOf(owner_) + rETHBalanceInETH(owner_); + } + + /** + * @notice Get the mean auction price of the last `numberOfPastAuctionsForMeanPrice` auctions. + * @dev Reverts if there is not enough auction history to calculate the mean auction price. + */ + function meanAuctionPrice() public view returns (uint256) { + uint16 numberOfPastAuctionsForMeanPrice_ = numberOfPastAuctionsForMeanPrice; + uint256[] memory prices = auction.getPrices(numberOfPastAuctionsForMeanPrice_); + + if (prices.length < numberOfPastAuctionsForMeanPrice_) revert NotEnoughAuctionHistory(); + + uint256 sum = 0; + for (uint16 i = 0; i < numberOfPastAuctionsForMeanPrice_; i++) { + sum += prices[i]; + } + + return sum / numberOfPastAuctionsForMeanPrice_; + } + + /** + * @notice Get an account's rETH balance, denominated in ETH. + * @param account the account to get the rETH balance of. + */ + function rETHBalanceInETH(address account) public view returns (uint256) { + return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(account)); + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * OWNER FUNCTIONS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @notice Sets `initialBurnNounId` + * Can only be called by owner, which is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. + */ + function setInitialBurnNounId(uint64 newInitialBurnNounId) external onlyOwner { + emit InitialBurnNounIdSet(initialBurnNounId, newInitialBurnNounId); + + initialBurnNounId = newInitialBurnNounId; + } + + /** + * @notice Sets `nounIdsBetweenBurns` + * Can only be called by owner, which is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. + */ + function setNounIdsBetweenBurns(uint64 newNounIdsBetweenBurns) external onlyOwner { + emit NounIdsBetweenBurnsSet(nounIdsBetweenBurns, newNounIdsBetweenBurns); + + nounIdsBetweenBurns = newNounIdsBetweenBurns; + } + + function setBurnWindowSize(uint16 newBurnWindowSize) external onlyOwner { + emit BurnWindowSizeSet(burnWindowSize, newBurnWindowSize); + + burnWindowSize = newBurnWindowSize; + } + + /** + * @notice Set the number of past auctions to use for calculating the mean auction price. + * Can only be called by owner, which is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. + * @param newNumberOfPastAuctionsForMeanPrice The new number of past auctions to use for calculating the mean auction price. + */ + function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) external onlyOwner { + if (newNumberOfPastAuctionsForMeanPrice < MIN_PAST_AUCTIONS) revert PastAuctionCountTooLow(); + + emit NumberOfPastAuctionsForMeanPriceSet(numberOfPastAuctionsForMeanPrice, newNumberOfPastAuctionsForMeanPrice); + + numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * INTERNAL FUNCTIONS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @dev A helper function to return the minimum of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol new file mode 100644 index 0000000000..1d1412f33c --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: BSD-3-Clause + +/// @title The Nouns DAO executor and treasury, supporting DAO fork + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +// LICENSE +// NounsDAOExecutorV3.sol is a modified version of Compound Lab's Timelock.sol: +// https://github.com/compound-finance/compound-protocol/blob/20abad28055a2f91df48a90f8bb6009279a4cb35/contracts/Timelock.sol +// +// Timelock.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 +// NounsDAOExecutorV3.sol is a modified version of NounsDAOExecutor.sol +// +// NounsDAOExecutor.sol modifications: +// NounsDAOExecutor.sol modifies Timelock to use Solidity 0.8.x receive(), fallback(), and built-in over/underflow protection +// This contract acts as executor of Nouns DAO governance and its treasury, so it has been modified to accept ETH. +// +// NounsDAOExecutorV2.sol modifications: +// - `sendETH` and `sendERC20` functions used for DAO forks +// - is upgradable via UUPSUpgradeable. uses intializer instead of constructor. +// - `GRACE_PERIOD` has been increased from 14 days to 21 days to allow more time in case of a forking period +// +// NounsDAOExecutorV3.sol modifications: +// - added `setExcessETHBurner` and `burnExcessETH` functions to enable burning excess eth. See ExcessETHBurner. + +pragma solidity ^0.8.19; + +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { SafeERC20 } from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import { Initializable } from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; +import { UUPSUpgradeable } from '@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol'; +import { Address } from '@openzeppelin/contracts/utils/Address.sol'; +import { Burn } from '../libs/Burn.sol'; + +contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { + using SafeERC20 for IERC20; + using Address for address payable; + + error OnlyExcessETHBurner(); + + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint256 indexed newDelay); + event CancelTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event ExecuteTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event QueueTransaction( + bytes32 indexed txHash, + address indexed target, + uint256 value, + string signature, + bytes data, + uint256 eta + ); + event ETHSent(address indexed to, uint256 amount); + event ERC20Sent(address indexed to, address indexed erc20Token, uint256 amount); + event ExcessETHBurnerSet(address oldBurner, address newBurner); + event ETHBurned(uint256 amount); + + string public constant NAME = 'NounsDAOExecutorV3'; + + /// @dev increased grace period from 14 days to 21 days to allow more time in case of a forking period + uint256 public constant GRACE_PERIOD = 21 days; + uint256 public constant MINIMUM_DELAY = 2 days; + uint256 public constant MAXIMUM_DELAY = 30 days; + + address public admin; + address public pendingAdmin; + uint256 public delay; + + mapping(bytes32 => bool) public queuedTransactions; + + address public excessETHBurner; + uint256 public totalETHBurned; + + constructor() initializer {} + + function initialize(address admin_, uint256 delay_) public virtual initializer { + require(delay_ >= MINIMUM_DELAY, 'NounsDAOExecutor::constructor: Delay must exceed minimum delay.'); + require(delay_ <= MAXIMUM_DELAY, 'NounsDAOExecutor::setDelay: Delay must not exceed maximum delay.'); + + admin = admin_; + delay = delay_; + } + + function setDelay(uint256 delay_) public { + require(msg.sender == address(this), 'NounsDAOExecutor::setDelay: Call must come from NounsDAOExecutor.'); + require(delay_ >= MINIMUM_DELAY, 'NounsDAOExecutor::setDelay: Delay must exceed minimum delay.'); + require(delay_ <= MAXIMUM_DELAY, 'NounsDAOExecutor::setDelay: Delay must not exceed maximum delay.'); + delay = delay_; + + emit NewDelay(delay_); + } + + function acceptAdmin() public { + require(msg.sender == pendingAdmin, 'NounsDAOExecutor::acceptAdmin: Call must come from pendingAdmin.'); + admin = msg.sender; + pendingAdmin = address(0); + + emit NewAdmin(msg.sender); + } + + function setPendingAdmin(address pendingAdmin_) public { + require( + msg.sender == address(this), + 'NounsDAOExecutor::setPendingAdmin: Call must come from NounsDAOExecutor.' + ); + pendingAdmin = pendingAdmin_; + + emit NewPendingAdmin(pendingAdmin_); + } + + function setExcessETHBurner(address newExcessETHBurner) public { + require( + msg.sender == address(this), + 'NounsDAOExecutor::setExcessETHBurner: Call must come from NounsDAOExecutor.' + ); + + emit ExcessETHBurnerSet(excessETHBurner, newExcessETHBurner); + + excessETHBurner = newExcessETHBurner; + } + + function queueTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public returns (bytes32) { + require(msg.sender == admin, 'NounsDAOExecutor::queueTransaction: Call must come from admin.'); + require( + eta >= getBlockTimestamp() + delay, + 'NounsDAOExecutor::queueTransaction: Estimated execution block must satisfy delay.' + ); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public { + require(msg.sender == admin, 'NounsDAOExecutor::cancelTransaction: Call must come from admin.'); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction( + address target, + uint256 value, + string memory signature, + bytes memory data, + uint256 eta + ) public returns (bytes memory) { + require(msg.sender == admin, 'NounsDAOExecutor::executeTransaction: Call must come from admin.'); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require(queuedTransactions[txHash], "NounsDAOExecutor::executeTransaction: Transaction hasn't been queued."); + require( + getBlockTimestamp() >= eta, + "NounsDAOExecutor::executeTransaction: Transaction hasn't surpassed time lock." + ); + require( + getBlockTimestamp() <= eta + GRACE_PERIOD, + 'NounsDAOExecutor::executeTransaction: Transaction is stale.' + ); + + queuedTransactions[txHash] = false; + + bytes memory callData; + + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + // solium-disable-next-line security/no-call-value + (bool success, bytes memory returnData) = target.call{ value: value }(callData); + require(success, 'NounsDAOExecutor::executeTransaction: Transaction execution reverted.'); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + + return returnData; + } + + function getBlockTimestamp() internal view returns (uint256) { + // solium-disable-next-line security/no-block-members + return block.timestamp; + } + + receive() external payable {} + + fallback() external payable {} + + function sendETH(address payable recipient, uint256 ethToSend) external { + require(msg.sender == admin, 'NounsDAOExecutor::sendETH: Call must come from admin.'); + + recipient.sendValue(ethToSend); + + emit ETHSent(recipient, ethToSend); + } + + function sendERC20( + address recipient, + address erc20Token, + uint256 tokensToSend + ) external { + require(msg.sender == admin, 'NounsDAOExecutor::sendERC20: Call must come from admin.'); + + IERC20(erc20Token).safeTransfer(recipient, tokensToSend); + + emit ERC20Sent(recipient, erc20Token, tokensToSend); + } + + /** + * @notice Burn excess ETH in the treasury. + * Can only be called by `excessETHBurner`. Anyone can call the burner's `burnExcessETH` function. + * Excess ETH is defined as the difference between the total value of the treasury in ETH, and the expected value + * which is the mean auction price in the last N auctions multiplied by adjusted total supply (see DAO logic for + * more info on `adjustedTotalSupply`). + */ + function burnExcessETH(uint256 amount) public { + if (msg.sender != excessETHBurner) revert OnlyExcessETHBurner(); + + Burn.eth(amount); + totalETHBurned += amount; + + emit ETHBurned(amount); + } + + /** + * @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by + * {upgradeTo} and {upgradeToAndCall}. + * + * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. + * + * ```solidity + * function _authorizeUpgrade(address) internal override onlyOwner {} + * ``` + */ + function _authorizeUpgrade(address) internal view override { + require( + msg.sender == address(this), + 'NounsDAOExecutor::_authorizeUpgrade: Call must come from NounsDAOExecutor.' + ); + } +} diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol new file mode 100644 index 0000000000..e3ccf2f1be --- /dev/null +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Interface for Noun Auction Houses V2 + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +import { INounsToken } from './INounsToken.sol'; + +interface INounsAuctionHouseV2 { + struct AuctionV2 { + // ID for the Noun (ERC721 token ID) + uint128 nounId; + // The current highest bid amount + uint128 amount; + // The time that the auction started + uint40 startTime; + // The time that the auction is scheduled to end + uint40 endTime; + // The address of the current highest bid + address payable bidder; + // Whether or not the auction has been settled + bool settled; + } + + struct SettlementState { + // The block.timestamp when the auction was settled. + uint32 blockTimestamp; + // The winning bid amount, with 10 decimal places (reducing accuracy to save bits). + uint64 amount; + // The address of the auction winner. + address winner; + } + + struct Settlement { + // The block.timestamp when the auction was settled. + uint32 blockTimestamp; + // The winning bid amount, converted from 10 decimal places to 18, for better client UX. + uint256 amount; + // The address of the auction winner. + address winner; + // ID for the Noun (ERC721 token ID). + uint256 nounId; + } + + event AuctionCreated(uint256 indexed nounId, uint256 startTime, uint256 endTime); + + event AuctionBid(uint256 indexed nounId, address sender, uint256 value, bool extended); + + event AuctionExtended(uint256 indexed nounId, uint256 endTime); + + event AuctionSettled(uint256 indexed nounId, address winner, uint256 amount); + + event AuctionTimeBufferUpdated(uint256 timeBuffer); + + event AuctionReservePriceUpdated(uint256 reservePrice); + + event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage); + + event HistoricPricesSet(uint256[] nounIds, uint256[] prices); + + function settleAuction() external; + + function settleCurrentAndCreateNewAuction() external; + + function createBid(uint256 nounId) external payable; + + function pause() external; + + function unpause() external; + + function setTimeBuffer(uint56 timeBuffer) external; + + function setReservePrice(uint192 reservePrice) external; + + function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external; + + function auction() external view returns (AuctionV2 memory); + + function getSettlements(uint256 auctionCount) external view returns (Settlement[] memory settlements); + + function getPrices(uint256 auctionCount) external view returns (uint256[] memory prices); + + function getSettlements(uint256 startId, uint256 endId) external view returns (Settlement[] memory settlements); + + function getPrices(uint256 startId, uint256 endId) external view returns (uint256[] memory prices); + + function warmUpSettlementState(uint256[] calldata nounIds) external; + + function nouns() external view returns (INounsToken); +} diff --git a/packages/nouns-contracts/contracts/libs/Burn.sol b/packages/nouns-contracts/contracts/libs/Burn.sol new file mode 100644 index 0000000000..344f9699ba --- /dev/null +++ b/packages/nouns-contracts/contracts/libs/Burn.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/** + * @title Burn + * @notice Utilities for burning stuff. + * @dev Based on https://github.com/ethereum-optimism/optimism/blob/eb68d8395971bc4a125cd0fd07567547f5bc0c49/packages/contracts-bedrock/contracts/libraries/Burn.sol + */ +library Burn { + /** + * Burns a given amount of ETH. + * + * @param _amount Amount of ETH to burn. + */ + function eth(uint256 _amount) internal { + new Burner{ value: _amount }(); + } +} + +/** + * @title Burner + * @notice Burner self-destructs on creation and sends all ETH to itself, removing all ETH given to + * the contract from the circulating supply. Self-destructing is the only way to remove ETH + * from the circulating supply. + */ +contract Burner { + constructor() payable { + selfdestruct(payable(address(this))); + } +} diff --git a/packages/nouns-contracts/contracts/test/ERC20Testnet.sol b/packages/nouns-contracts/contracts/test/ERC20Testnet.sol new file mode 100644 index 0000000000..a2e92f68ce --- /dev/null +++ b/packages/nouns-contracts/contracts/test/ERC20Testnet.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; +import { ERC20 } from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +contract ERC20Testnet is ERC20, Ownable { + constructor( + address owner_, + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) { + _transferOwnership(owner_); + } + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } +} diff --git a/packages/nouns-contracts/contracts/test/RocketETHTestnet.sol b/packages/nouns-contracts/contracts/test/RocketETHTestnet.sol new file mode 100644 index 0000000000..246728e2f9 --- /dev/null +++ b/packages/nouns-contracts/contracts/test/RocketETHTestnet.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.19; + +import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; +import { ERC20 } from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +contract RocketETHTestnet is ERC20, Ownable { + uint256 rate; + + constructor(address owner_) ERC20('Rocket ETH Testnet', 'rETH') { + _transferOwnership(owner_); + } + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + function setRate(uint256 rate_) public onlyOwner { + rate = rate_; + } + + function getEthValue(uint256 _rethAmount) external view returns (uint256) { + return _rethAmount * rate; + } +} diff --git a/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Base.s.sol b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Base.s.sol new file mode 100644 index 0000000000..e298e33dcf --- /dev/null +++ b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Base.s.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; +import { NounsAuctionHouse } from '../../contracts/NounsAuctionHouse.sol'; +import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; +import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHousePreV2Migration.sol'; + +abstract contract DeployAuctionHouseV2Base is Script { + NounsAuctionHouse public immutable auctionV1; + + constructor(address _auctionHouseProxy) { + auctionV1 = NounsAuctionHouse(payable(_auctionHouseProxy)); + } + + function run() public returns (NounsAuctionHouseV2 newLogic, NounsAuctionHousePreV2Migration migratorLogic) { + uint256 deployerKey = vm.envUint('DEPLOYER_PRIVATE_KEY'); + + vm.startBroadcast(deployerKey); + + newLogic = new NounsAuctionHouseV2(auctionV1.nouns(), auctionV1.weth(), auctionV1.duration()); + migratorLogic = new NounsAuctionHousePreV2Migration(); + + vm.stopBroadcast(); + } +} diff --git a/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Mainnet.s.sol b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Mainnet.s.sol new file mode 100644 index 0000000000..b7cbbadb20 --- /dev/null +++ b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Mainnet.s.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { DeployAuctionHouseV2Base } from './DeployAuctionHouseV2Base.s.sol'; + +contract DeployAuctionHouseV2Mainnet is DeployAuctionHouseV2Base { + address constant AUCTION_HOUSE_PROXY_MAINNET = 0x830BD73E4184ceF73443C15111a1DF14e495C706; + + constructor() DeployAuctionHouseV2Base(AUCTION_HOUSE_PROXY_MAINNET) {} +} diff --git a/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Sepolia.s.sol b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Sepolia.s.sol new file mode 100644 index 0000000000..ad830108e2 --- /dev/null +++ b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Sepolia.s.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { DeployAuctionHouseV2Base } from './DeployAuctionHouseV2Base.s.sol'; + +contract DeployAuctionHouseV2Sepolia is DeployAuctionHouseV2Base { + address constant AUCTION_HOUSE_SEPOLIA = 0x45ebbdb0E66aC2a8339D98aDB6934C89f166A754; + + constructor() DeployAuctionHouseV2Base(AUCTION_HOUSE_SEPOLIA) {} +} diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol new file mode 100644 index 0000000000..4d2f23aa80 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; +import { NounsDAOExecutorV3 } from '../../contracts/governance/NounsDAOExecutorV3.sol'; +import { NounsTokenLike } from '../../contracts/governance/NounsDAOInterfaces.sol'; +import { ExcessETHBurner, INounsDAOV3 } from '../../contracts/governance/ExcessETHBurner.sol'; +import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; +import { IERC20 } from '@openzeppelin/contracts/interfaces/IERC20.sol'; + +interface INounsDAOLogicV3 { + function nouns() external view returns (NounsTokenLike); +} + +abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { + address public immutable executorProxy; + INounsDAOLogicV3 public immutable daoProxy; + INounsAuctionHouseV2 public immutable auction; + IERC20 public immutable wETH; + IERC20 public immutable stETH; + IERC20 public immutable rETH; + uint64 public immutable initialBurnNounId; + uint64 public immutable nounIdsBetweenBurns; + uint16 public immutable burnWindowSize; + uint16 public immutable numberOfPastAuctionsForMeanPrice; + + constructor( + address payable executorProxy_, + address wETH_, + address stETH_, + address rETH_, + uint64 initialBurnNounId_, + uint64 nounIdsBetweenBurns_, + uint16 burnWindowSize_, + uint16 numberOfPastAuctionsForMeanPrice_ + ) { + executorProxy = executorProxy_; + + daoProxy = INounsDAOLogicV3(payable(NounsDAOExecutorV3(executorProxy_).admin())); + auction = INounsAuctionHouseV2(daoProxy.nouns().minter()); + + wETH = IERC20(wETH_); + stETH = IERC20(stETH_); + rETH = IERC20(rETH_); + + initialBurnNounId = initialBurnNounId_; + nounIdsBetweenBurns = nounIdsBetweenBurns_; + burnWindowSize = burnWindowSize_; + numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; + } + + function run() public returns (NounsDAOExecutorV3 executorV3, ExcessETHBurner burner) { + uint256 deployerKey = vm.envUint('DEPLOYER_PRIVATE_KEY'); + + vm.startBroadcast(deployerKey); + + executorV3 = new NounsDAOExecutorV3(); + burner = new ExcessETHBurner( + executorProxy, + INounsDAOV3(address(daoProxy)), + auction, + wETH, + stETH, + rETH, + initialBurnNounId, + nounIdsBetweenBurns, + burnWindowSize, + numberOfPastAuctionsForMeanPrice + ); + + vm.stopBroadcast(); + } +} diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol new file mode 100644 index 0000000000..1dce784167 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { DeployExecutorV3AndExcessETHBurnerBase } from './DeployExecutorV3AndExcessETHBurnerBase.s.sol'; + +contract DeployExecutorV3AndExcessETHBurnerMainnet is DeployExecutorV3AndExcessETHBurnerBase { + address payable constant EXECUTOR_PROXY = payable(0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71); + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + address constant RETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; + + uint64 BURN_START_NOUN_ID = type(uint64).max; + uint64 NOUNS_BETWEEN_BURNS = 0; + uint16 BURN_WINDOW_SIZE = 3; + uint16 MEAN_AUCTION_COUNT = 1; + + constructor() + DeployExecutorV3AndExcessETHBurnerBase( + EXECUTOR_PROXY, + WETH, + STETH, + RETH, + BURN_START_NOUN_ID, + NOUNS_BETWEEN_BURNS, + BURN_WINDOW_SIZE, + MEAN_AUCTION_COUNT + ) + {} +} diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol new file mode 100644 index 0000000000..679725f03d --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { DeployExecutorV3AndExcessETHBurnerBase } from './DeployExecutorV3AndExcessETHBurnerBase.s.sol'; + +contract DeployExecutorV3AndExcessETHBurnerSepolia is DeployExecutorV3AndExcessETHBurnerBase { + address payable constant EXECUTOR_PROXY = payable(0x6c2dD53b8DbDD3af1209DeB9dA87D487EaE8E638); + address constant WETH = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; + address constant STETH = 0x7f96dAEF4A54F6A52613d6272560C2BD25e913B8; + address constant RETH = 0xf07dafCC49a9F5E1E73Df6bD6616d0a5bA19e502; + + uint64 BURN_START_NOUN_ID = 10; + uint64 NOUNS_BETWEEN_BURNS = 10; + uint16 BURN_WINDOW_SIZE = 3; + uint16 MEAN_AUCTION_COUNT = 10; + + constructor() + DeployExecutorV3AndExcessETHBurnerBase( + EXECUTOR_PROXY, + WETH, + STETH, + RETH, + BURN_START_NOUN_ID, + NOUNS_BETWEEN_BURNS, + BURN_WINDOW_SIZE, + MEAN_AUCTION_COUNT + ) + {} +} diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployTestnetTokens.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployTestnetTokens.s.sol new file mode 100644 index 0000000000..4012403dc6 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployTestnetTokens.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; +import { RocketETHTestnet } from '../../contracts/test/RocketETHTestnet.sol'; +import { ERC20Testnet } from '../../contracts/test/ERC20Testnet.sol'; + +contract DeployTestnetTokens is Script { + function run() public { + uint256 deployerKey = vm.envUint('DEPLOYER_PRIVATE_KEY'); + address owner = vm.addr(deployerKey); + + vm.startBroadcast(deployerKey); + + ERC20Testnet stETH = new ERC20Testnet(owner, 'Test Staked Ether', 'stETH'); + RocketETHTestnet rETH = new RocketETHTestnet(owner); + + console.log('Owner: %s', owner); + console.log('stETH: %s', address(stETH)); + console.log('rETH: %s', address(rETH)); + + vm.stopBroadcast(); + } +} diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol new file mode 100644 index 0000000000..bdf13e8b30 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Script.sol'; + +interface INounsDAOLogicV3 { + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); +} + +abstract contract ProposeExecutorV3UpgradeBase is Script { + uint256 proposerKey; + string description; + address daoProxy; + address executorProxy; + address executorV3Impl; + + function run() public returns (uint256 proposalId) { + vm.startBroadcast(proposerKey); + + uint8 numTxs = 1; + address[] memory targets = new address[](numTxs); + uint256[] memory values = new uint256[](numTxs); + string[] memory signatures = new string[](numTxs); + bytes[] memory calldatas = new bytes[](numTxs); + + // Upgrade to executor V3 + uint256 i = 0; + targets[i] = executorProxy; + values[i] = 0; + signatures[i] = 'upgradeTo(address)'; + calldatas[i] = abi.encode(executorV3Impl); + + proposalId = INounsDAOLogicV3(daoProxy).propose(targets, values, signatures, calldatas, description); + console.log('Proposed proposalId: %d', proposalId); + + vm.stopBroadcast(); + } +} diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeMainnet.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeMainnet.s.sol new file mode 100644 index 0000000000..022f995b00 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeMainnet.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import { ProposeExecutorV3UpgradeBase } from './ProposeExecutorV3UpgradeBase.s.sol'; + +contract ProposeDAOV3UpgradeMainnet is ProposeExecutorV3UpgradeBase { + address public constant NOUNS_DAO_PROXY_MAINNET = 0x6f3E6272A167e8AcCb32072d08E0957F9c79223d; + address public constant EXECUTOR_PROXY_MAINNET = 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71; + address public constant EXECUTOR_V3_IMPL = address(0); + + constructor() { + proposerKey = vm.envUint('PROPOSER_KEY'); + description = vm.readFile(vm.envString('PROPOSAL_DESCRIPTION_FILE')); + daoProxy = NOUNS_DAO_PROXY_MAINNET; + executorProxy = EXECUTOR_PROXY_MAINNET; + executorV3Impl = EXECUTOR_V3_IMPL; + } +} diff --git a/packages/nouns-contracts/tasks/utils/index.ts b/packages/nouns-contracts/tasks/utils/index.ts index 2e3a73a91e..03a067bd73 100644 --- a/packages/nouns-contracts/tasks/utils/index.ts +++ b/packages/nouns-contracts/tasks/utils/index.ts @@ -53,9 +53,7 @@ export async function printEstimatedCost(factory: ContractFactory, gasPrice: Big factory.getDeployTransaction({ gasPrice }), ); const deploymentCost = deploymentGas.mul(gasPrice); - console.log( - `Estimated cost to deploy NounsDAOLogicV2: ${utils.formatUnits(deploymentCost, 'ether')} ETH`, - ); + console.log(`Estimated cost to deploy: ${utils.formatUnits(deploymentCost, 'ether')} ETH`); } export function dataToDescriptorInput(data: string[]): { diff --git a/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol b/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol new file mode 100644 index 0000000000..8a1d61e053 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; +import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHousePreV2Migration.sol'; +import { DeployAuctionHouseV2Mainnet } from '../../script/AuctionHouseV2/DeployAuctionHouseV2Mainnet.s.sol'; +import { DeployExecutorV3AndExcessETHBurnerMainnet } from '../../script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol'; +import { NounsToken } from '../../contracts/NounsToken.sol'; +import { INounsDAOExecutor } from '../../contracts/governance/NounsDAOInterfaces.sol'; +import { NounsDAOExecutorV3 } from '../../contracts/governance/NounsDAOExecutorV3.sol'; +import { ExcessETHBurner } from '../../contracts/governance/ExcessETHBurner.sol'; + +interface INounsDAOLogicV3 { + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); + + function votingDelay() external view returns (uint256); + + function castVote(uint256 proposalId, uint8 support) external; + + function votingPeriod() external view returns (uint256); + + function proposalUpdatablePeriodInBlocks() external view returns (uint256); + + function queue(uint256 proposalId) external; + + function execute(uint256 proposalId) external; + + function adjustedTotalSupply() external view returns (uint256); +} + +contract BurnUpgradeForkMainnetTest is Test { + address public constant NOUNDERS = 0x2573C60a6D127755aA2DC85e342F7da2378a0Cc5; + INounsDAOLogicV3 public constant NOUNS_DAO_PROXY_MAINNET = + INounsDAOLogicV3(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d); + address public constant auctionHouseProxyAdmin = 0xC1C119932d78aB9080862C5fcb964029f086401e; + address public constant auctionHouseProxy = 0x830BD73E4184ceF73443C15111a1DF14e495C706; + NounsToken public constant nouns = NounsToken(0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03); + INounsDAOExecutor public constant NOUNS_TIMELOCK_V2_MAINNET = + INounsDAOExecutor(0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71); + + NounsAuctionHouseV2 newAuctionHouseLogic; + NounsDAOExecutorV3 executorV3Logic; + ExcessETHBurner burner; + + function setUp() public { + vm.createSelectFork(vm.envString('RPC_MAINNET'), 18291531); + vm.setEnv('DEPLOYER_PRIVATE_KEY', '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + + // 1. Deploy all new contracts + ( + NounsAuctionHouseV2 newAuctionHouseLogic_, + NounsAuctionHousePreV2Migration migrationLogic + ) = new DeployAuctionHouseV2Mainnet().run(); + (NounsDAOExecutorV3 executorV3_, ExcessETHBurner burner_) = new DeployExecutorV3AndExcessETHBurnerMainnet() + .run(); + newAuctionHouseLogic = newAuctionHouseLogic_; + executorV3Logic = executorV3_; + burner = burner_; + + // 2. Upgrade Auction House & Timelock + upgradeAuctionHouseAndTimelock( + address(newAuctionHouseLogic_), + address(migrationLogic), + address(executorV3_), + address(burner_) + ); + } + + function upgradeAuctionHouseAndTimelock( + address newAuctionHouseLogic_, + address migrationLogic_, + address executorV3Logic_, + address ethBurner_ + ) internal { + // give ourselves voting power + vm.prank(NOUNDERS); + nouns.delegate(address(this)); + vm.roll(block.number + 1); + + uint256 proposalId = proposeAuctionHouseV2AndTimelockUpgrade( + newAuctionHouseLogic_, + migrationLogic_, + executorV3Logic_, + ethBurner_ + ); + + // simulate vote & proposal execution + voteAndExecuteProposal(proposalId); + } + + function get1967Implementation(address proxy) internal returns (address) { + bytes32 slot = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1); + return address(uint160(uint256(vm.load(proxy, slot)))); + } + + function proposeAuctionHouseV2AndTimelockUpgrade( + address newLogic, + address migrationLogic, + address executorV3Logic, + address ethBurner + ) internal returns (uint256 proposalId) { + uint8 numTxs = 5; + address[] memory targets = new address[](numTxs); + uint256[] memory values = new uint256[](numTxs); + string[] memory signatures = new string[](numTxs); + bytes[] memory calldatas = new bytes[](numTxs); + + // Upgrade to migration logic + uint256 i = 0; + targets[i] = auctionHouseProxyAdmin; + values[i] = 0; + signatures[i] = 'upgrade(address,address)'; + calldatas[i] = abi.encode(auctionHouseProxy, migrationLogic); + + // Run migration logic + i = 1; + targets[i] = auctionHouseProxy; + values[i] = 0; + signatures[i] = 'migrate()'; + calldatas[i] = ''; + + // Upgrade to V2 logic + i = 2; + targets[i] = auctionHouseProxyAdmin; + values[i] = 0; + signatures[i] = 'upgrade(address,address)'; + calldatas[i] = abi.encode(auctionHouseProxy, newLogic); + + // Upgrade timelock to V3 + i = 3; + targets[i] = address(NOUNS_TIMELOCK_V2_MAINNET); + values[i] = 0; + signatures[i] = 'upgradeTo(address)'; + calldatas[i] = abi.encode(executorV3Logic); + + // Set Burner + i = 4; + targets[i] = address(NOUNS_TIMELOCK_V2_MAINNET); + values[i] = 0; + signatures[i] = 'setExcessETHBurner(address)'; + calldatas[i] = abi.encode(ethBurner); + + proposalId = NOUNS_DAO_PROXY_MAINNET.propose( + targets, + values, + signatures, + calldatas, + 'Upgrade to Auction House V2 & timelock V3' + ); + console.log('Proposed proposalId: %d', proposalId); + } + + function voteAndExecuteProposal(uint256 proposalId) internal { + vm.roll( + block.number + + NOUNS_DAO_PROXY_MAINNET.votingDelay() + + NOUNS_DAO_PROXY_MAINNET.proposalUpdatablePeriodInBlocks() + + 1 + ); + NOUNS_DAO_PROXY_MAINNET.castVote(proposalId, 1); + + vm.roll(block.number + NOUNS_DAO_PROXY_MAINNET.votingPeriod() + 1); + NOUNS_DAO_PROXY_MAINNET.queue(proposalId); + + vm.warp(block.timestamp + NOUNS_TIMELOCK_V2_MAINNET.delay()); + NOUNS_DAO_PROXY_MAINNET.execute(proposalId); + } + + function test_proxiesWereUpgraded() public { + assertEq(get1967Implementation(auctionHouseProxy), address(newAuctionHouseLogic), 'upgrade failed'); + assertEq( + get1967Implementation(address(NOUNS_TIMELOCK_V2_MAINNET)), + address(executorV3Logic), + 'timelock upgrade failed' + ); + } + + function test_burnerIsOffAtFirst() public { + // initial id is very high, i.e. noun minting won't reach it any time soon + assertGt(burner.initialBurnNounId(), 1000000); + + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + } + + function test_burn() public { + uint256 proposalId = proposeToTurnOnBurn(880, 10, 4, 20); + voteAndExecuteProposal(proposalId); + + assertEq(burner.initialBurnNounId(), 880); + assertEq(burner.nounIdsBetweenBurns(), 10); + assertEq(burner.burnWindowSize(), 4); + assertEq(burner.numberOfPastAuctionsForMeanPrice(), 20); + + NounsAuctionHouseV2 ah = NounsAuctionHouseV2(auctionHouseProxy); + vm.warp(ah.auction().endTime + 1); + ah.settleCurrentAndCreateNewAuction(); + + for (uint256 i; i < 10; i++) { + ah.createBid{ value: 20 ether }(ah.auction().nounId); + vm.warp(ah.auction().endTime + 1); + ah.settleCurrentAndCreateNewAuction(); + } + + vm.expectRevert(ExcessETHBurner.NotEnoughAuctionHistory.selector); + burner.burnExcessETH(); + + for (uint256 i; i < 10; i++) { + ah.createBid{ value: 30 ether }(ah.auction().nounId); + vm.warp(ah.auction().endTime + 1); + ah.settleCurrentAndCreateNewAuction(); + } + + assertEq(burner.meanAuctionPrice(), 25 ether); + assertEq(NOUNS_DAO_PROXY_MAINNET.adjustedTotalSupply(), 422); + assertEq(burner.expectedTreasuryValueInETH(), 10550 ether); + assertEq(burner.treasuryValueInETH(), 13899.047817579079479237 ether); + + // treasury has less ETH than 13899 - 25 * 422, therefore, only the available ETH in the treasury will be burned + assertEq(address(NOUNS_TIMELOCK_V2_MAINNET).balance, 2813.062865210366418892 ether); + + burner.burnExcessETH(); + + assertEq(address(NOUNS_TIMELOCK_V2_MAINNET).balance, 0); + } + + function proposeToTurnOnBurn( + uint64 initialBurnNounId, + uint64 nounIdsBetweenBurns, + uint16 burnWindowSize, + uint16 numberOfPastAuctionsForMeanPrice + ) internal returns (uint256 proposalId) { + uint8 numTxs = 4; + address[] memory targets = new address[](numTxs); + uint256[] memory values = new uint256[](numTxs); + string[] memory signatures = new string[](numTxs); + bytes[] memory calldatas = new bytes[](numTxs); + + uint256 i = 0; + targets[i] = address(burner); + values[i] = 0; + signatures[i] = 'setInitialBurnNounId(uint64)'; + calldatas[i] = abi.encode(initialBurnNounId); + + i = 1; + targets[i] = address(burner); + values[i] = 0; + signatures[i] = 'setNounIdsBetweenBurns(uint64)'; + calldatas[i] = abi.encode(nounIdsBetweenBurns); + + i = 2; + targets[i] = address(burner); + values[i] = 0; + signatures[i] = 'setBurnWindowSize(uint16)'; + calldatas[i] = abi.encode(burnWindowSize); + + i = 3; + targets[i] = address(burner); + values[i] = 0; + signatures[i] = 'setNumberOfPastAuctionsForMeanPrice(uint16)'; + calldatas[i] = abi.encode(numberOfPastAuctionsForMeanPrice); + + proposalId = NOUNS_DAO_PROXY_MAINNET.propose(targets, values, signatures, calldatas, 'Turn on burn'); + console.log('Proposed proposalId: %d', proposalId); + } +} diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol new file mode 100644 index 0000000000..2aaeec8633 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import { INounsAuctionHouse } from '../../contracts/interfaces/INounsAuctionHouse.sol'; +import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; +import { INounsToken } from '../../contracts/interfaces/INounsToken.sol'; +import { DeployUtils } from './helpers/DeployUtils.sol'; +import { AuctionHouseUpgrader } from './helpers/AuctionHouseUpgrader.sol'; +import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; + +abstract contract NounsAuctionHouseBaseTest is DeployUtils { + INounsAuctionHouse auctionHouse; + INounsToken nouns; + address noundersDAO = makeAddr('noundersDAO'); + address owner = makeAddr('owner'); + NounsAuctionHouseProxy auctionHouseProxy; + NounsAuctionHouseProxyAdmin proxyAdmin; + uint256[] nounIds; + + function setUp() public virtual { + ( + NounsAuctionHouseProxy auctionHouseProxy_, + NounsAuctionHouseProxyAdmin proxyAdmin_ + ) = _deployAuctionHouseV1AndToken(owner, noundersDAO, address(0)); + auctionHouseProxy = auctionHouseProxy_; + proxyAdmin = proxyAdmin_; + + auctionHouse = INounsAuctionHouse(address(auctionHouseProxy_)); + + vm.prank(owner); + auctionHouse.unpause(); + } +} + +contract NounsAuctionHouse_GasSnapshot is NounsAuctionHouseBaseTest { + function test_createOneBid() public { + auctionHouse.createBid{ value: 1 ether }(1); + } + + function test_createTwoBids() public { + auctionHouse.createBid{ value: 1 ether }(1); + auctionHouse.createBid{ value: 1.1 ether }(1); + } + + function test_settleCurrentAndCreateNewAuction() public { + vm.warp(block.timestamp + 1.1 days); + + auctionHouse.settleCurrentAndCreateNewAuction(); + } +} + +contract NounsAuctionHouseV2_GasSnapshot is NounsAuctionHouse_GasSnapshot { + function setUp() public virtual override { + super.setUp(); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionHouseProxy); + } +} + +contract NounsAuctionHouseV2WarmedUp_GasSnapshot is NounsAuctionHouseV2_GasSnapshot { + function setUp() public override { + super.setUp(); + nounIds = [1, 2, 3]; + INounsAuctionHouseV2(address(auctionHouse)).warmUpSettlementState(nounIds); + } +} + +contract NounsAuctionHouseV2_HistoricPrices_GasSnapshot is NounsAuctionHouseBaseTest { + INounsAuctionHouseV2 auctionHouseV2; + + function setUp() public virtual override { + super.setUp(); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionHouseProxy); + auctionHouseV2 = INounsAuctionHouseV2(address(auctionHouse)); + + for (uint256 i = 1; i <= 200; ++i) { + address bidder = makeAddr(vm.toString(i)); + bidAndWinCurrentAuction(bidder, i * 1e18); + } + } + + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { + uint128 nounId = auctionHouseV2.auction().nounId; + uint40 endTime = auctionHouseV2.auction().endTime; + vm.deal(bidder, bid); + vm.prank(bidder); + auctionHouseV2.createBid{ value: bid }(nounId); + vm.warp(endTime); + auctionHouseV2.settleCurrentAndCreateNewAuction(); + return block.timestamp; + } + + function test_getSettlements_90() public { + INounsAuctionHouseV2.Settlement[] memory prices = auctionHouseV2.getSettlements(90); + assertEq(prices.length, 90); + } + + function test_getPrices_90() public { + uint256[] memory prices = auctionHouseV2.getPrices(90); + assertEq(prices.length, 90); + } + + function test_getSettlements_range_90() public { + INounsAuctionHouseV2.Settlement[] memory prices = auctionHouseV2.getSettlements(1, 100); + assertEq(prices.length, 90); + } + + function test_getPrices_range_90() public { + uint256[] memory prices = auctionHouseV2.getPrices(1, 100); + assertEq(prices.length, 90); + } + + function test_warmUp() public { + uint256[] memory nounIds = new uint256[](1000); + for (uint256 i; i < 1000; ++i) { + nounIds[i] = i; + } + auctionHouseV2.warmUpSettlementState(nounIds); + } +} diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol new file mode 100644 index 0000000000..3635419162 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -0,0 +1,613 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Test.sol'; +import { DeployUtils } from './helpers/DeployUtils.sol'; +import { AuctionHouseUpgrader } from './helpers/AuctionHouseUpgrader.sol'; +import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import { NounsAuctionHouse } from '../../contracts/NounsAuctionHouse.sol'; +import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; +import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; +import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; + +contract NounsAuctionHouseV2TestBase is Test, DeployUtils { + event HistoricPricesSet(uint256[] nounIds, uint256[] prices); + + address owner = address(0x1111); + address noundersDAO = address(0x2222); + address minter = address(0x3333); + uint256[] nounIds; + + NounsAuctionHouseV2 auction; + + function setUp() public { + (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + owner, + noundersDAO, + minter + ); + + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + + auction = NounsAuctionHouseV2(address(auctionProxy)); + + vm.prank(owner); + auction.unpause(); + vm.roll(block.number + 1); + } + + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; + vm.deal(bidder, bid); + vm.prank(bidder); + auction.createBid{ value: bid }(nounId); + vm.warp(endTime); + auction.settleCurrentAndCreateNewAuction(); + return block.timestamp; + } + + function bidDontCreateNewAuction(address bidder, uint256 bid) internal returns (uint256) { + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; + vm.deal(bidder, bid); + vm.prank(bidder); + auction.createBid{ value: bid }(nounId); + vm.warp(endTime); + return block.timestamp; + } +} + +contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { + function test_createBid_revertsGivenWrongNounId() public { + uint128 nounId = auction.auction().nounId; + + vm.expectRevert('Noun not up for auction'); + auction.createBid(nounId - 1); + + vm.expectRevert('Noun not up for auction'); + auction.createBid(nounId + 1); + } + + function test_createBid_revertsPastEndTime() public { + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; + vm.warp(endTime + 1); + + vm.expectRevert('Auction expired'); + auction.createBid(nounId); + } + + function test_createBid_revertsGivenBidBelowReservePrice() public { + vm.prank(owner); + auction.setReservePrice(1 ether); + + uint128 nounId = auction.auction().nounId; + + vm.expectRevert('Must send at least reservePrice'); + auction.createBid{ value: 0.9 ether }(nounId); + } + + function test_createBid_revertsGivenBidLowerThanMinIncrement() public { + vm.prank(owner); + auction.setMinBidIncrementPercentage(50); + uint128 nounId = auction.auction().nounId; + auction.createBid{ value: 1 ether }(nounId); + + vm.expectRevert('Must send more than last bid by minBidIncrementPercentage amount'); + auction.createBid{ value: 1.49 ether }(nounId); + } + + function test_createBid_refundsPreviousBidder() public { + uint256 nounId = auction.auction().nounId; + address bidder1 = address(0x4444); + address bidder2 = address(0x5555); + + vm.deal(bidder1, 1.1 ether); + vm.prank(bidder1); + auction.createBid{ value: 1.1 ether }(nounId); + + assertEq(bidder1.balance, 0); + + vm.deal(bidder2, 2.2 ether); + vm.prank(bidder2); + auction.createBid{ value: 2.2 ether }(nounId); + + assertEq(bidder1.balance, 1.1 ether); + assertEq(bidder2.balance, 0); + } + + function test_createBid_preventsGasGriefingUponRefunding() public { + BidderWithGasGriefing badBidder = new BidderWithGasGriefing(); + uint256 nounId = auction.auction().nounId; + + badBidder.bid{ value: 1 ether }(auction, nounId); + + address bidder = address(0x4444); + vm.deal(bidder, 1.2 ether); + vm.prank(bidder); + uint256 gasBefore = gasleft(); + auction.createBid{ value: 1.2 ether }(nounId); + uint256 gasDiffWithGriefing = gasBefore - gasleft(); + + address bidder2 = address(0x5555); + vm.deal(bidder2, 2.2 ether); + vm.prank(bidder2); + gasBefore = gasleft(); + auction.createBid{ value: 2.2 ether }(nounId); + uint256 gasDiffNoGriefing = gasBefore - gasleft(); + + // Before the transfer with assembly fix this diff was greater + // closer to 50K + assertLt(gasDiffWithGriefing - gasDiffNoGriefing, 10_000); + } + + function test_settleAuction_revertsWhenAuctionInProgress() public { + vm.expectRevert("Auction hasn't completed"); + auction.settleCurrentAndCreateNewAuction(); + } + + function test_settleAuction_revertsWhenSettled() public { + uint40 endTime = auction.auction().endTime; + vm.warp(endTime + 1); + + vm.prank(owner); + auction.pause(); + auction.settleAuction(); + + vm.expectRevert('Auction has already been settled'); + auction.settleAuction(); + } + + function test_settleAuction_revertsWhenAuctionHasntBegunYet() public { + (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + owner, + noundersDAO, + minter + ); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + auction = NounsAuctionHouseV2(address(auctionProxy)); + + vm.expectRevert("Auction hasn't begun"); + auction.settleAuction(); + } + + function test_settleCurrentAndCreateNewAuction_revertsWhenPaused() public { + uint40 endTime = auction.auction().endTime; + vm.warp(endTime + 1); + + vm.prank(owner); + auction.pause(); + + vm.expectRevert('Pausable: paused'); + auction.settleCurrentAndCreateNewAuction(); + } + + function test_V2Migration_works() public { + (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + owner, + noundersDAO, + minter + ); + NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(auctionProxy)); + vm.prank(owner); + auctionV1.unpause(); + vm.roll(block.number + 1); + (uint256 nounId, , uint256 startTime, uint256 endTime, , ) = auctionV1.auction(); + + address payable bidder = payable(address(0x142)); + uint256 amount = 142 ether; + vm.deal(bidder, amount); + vm.prank(bidder); + auctionV1.createBid{ value: amount }(nounId); + + address nounsBefore = address(auctionV1.nouns()); + address wethBefore = address(auctionV1.weth()); + + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); + + INounsAuctionHouseV2.AuctionV2 memory auctionV2State = auctionV2.auction(); + + assertEq(auctionV2State.nounId, nounId); + assertEq(auctionV2State.amount, amount); + assertEq(auctionV2State.startTime, startTime); + assertEq(auctionV2State.endTime, endTime); + assertEq(auctionV2State.bidder, bidder); + assertEq(auctionV2State.settled, false); + + assertEq(address(auctionV2.nouns()), nounsBefore); + assertEq(address(auctionV2.weth()), wethBefore); + assertEq(auctionV2.timeBuffer(), AUCTION_TIME_BUFFER); + assertEq(auctionV2.reservePrice(), AUCTION_RESERVE_PRICE); + assertEq(auctionV2.minBidIncrementPercentage(), AUCTION_MIN_BID_INCREMENT_PRCT); + assertEq(auctionV2.duration(), AUCTION_DURATION); + assertEq(auctionV2.paused(), false); + assertEq(auctionV2.owner(), owner); + } + + function test_V2Migration_copiesPausedWhenTrue() public { + (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + owner, + noundersDAO, + minter + ); + NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(auctionProxy)); + vm.prank(owner); + auctionV1.unpause(); + vm.roll(block.number + 1); + (uint256 nounId, , , , , ) = auctionV1.auction(); + + address payable bidder = payable(address(0x142)); + uint256 amount = 142 ether; + vm.deal(bidder, amount); + vm.prank(bidder); + auctionV1.createBid{ value: amount }(nounId); + + vm.prank(owner); + auctionV1.pause(); + + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); + assertEq(auctionV2.paused(), true); + } + + function test_auctionGetter_compatibleWithV1() public { + address bidder = address(0x4242); + vm.deal(bidder, 1.1 ether); + vm.prank(bidder); + auction.createBid{ value: 1.1 ether }(1); + + NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(auction)); + + INounsAuctionHouseV2.AuctionV2 memory auctionV2 = auction.auction(); + + ( + uint256 nounIdV1, + uint256 amountV1, + uint256 startTimeV1, + uint256 endTimeV1, + address payable bidderV1, + bool settledV1 + ) = auctionV1.auction(); + + assertEq(auctionV2.nounId, nounIdV1); + assertEq(auctionV2.amount, amountV1); + assertEq(auctionV2.startTime, startTimeV1); + assertEq(auctionV2.endTime, endTimeV1); + assertEq(auctionV2.bidder, bidderV1); + assertEq(auctionV2.settled, settledV1); + } +} + +contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { + uint256[] expectedPrices; + + function test_prices_oneAuction_higherAuctionCountReturnsTheOneAuction() public { + address bidder = address(0x4444); + bidAndWinCurrentAuction(bidder, 1 ether); + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(2); + + assertEq(settlements.length, 1); + assertEq(settlements[0].blockTimestamp, uint32(block.timestamp)); + assertEq(settlements[0].nounId, 1); + assertEq(settlements[0].amount, 1 ether); + assertEq(settlements[0].winner, bidder); + + uint256[] memory prices = auction.getPrices(2); + assertEq(prices.length, 1); + assertEq(prices[0], 1 ether); + } + + function test_prices_preserves10DecimalsUnderUint64MaxValue() public { + // amount is uint64; maxValue - 1 = 18446744073709551615 + // at 10 decimal points it's 1844674407.3709551615 + bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551615999999 ether); + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(1); + + assertEq(settlements.length, 1); + assertEq(settlements[0].nounId, 1); + assertEq(settlements[0].amount, 1844674407.3709551615 ether); + assertEq(settlements[0].winner, makeAddr('bidder')); + + uint256[] memory prices = auction.getPrices(1); + assertEq(prices[0], 1844674407.3709551615 ether); + } + + function test_prices_overflowsGracefullyOverUint64MaxValue() public { + bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551617 ether); + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(1); + + assertEq(settlements.length, 1); + assertEq(settlements[0].nounId, 1); + assertEq(settlements[0].amount, 1 * 1e8); + assertEq(settlements[0].winner, makeAddr('bidder')); + + uint256[] memory prices = auction.getPrices(1); + assertEq(prices[0], 1 * 1e8); + } + + function test_prices_20Auctions_skipsNounerNounsAsExpected() public { + for (uint256 i = 1; i <= 20; ++i) { + address bidder = makeAddr(vm.toString(i)); + bidAndWinCurrentAuction(bidder, i * 1e18); + } + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(20); + assertEq(settlements[0].nounId, 22); + assertEq(settlements[1].nounId, 21); + assertEq(settlements[2].nounId, 19); + assertEq(settlements[10].nounId, 11); + assertEq(settlements[11].nounId, 9); + assertEq(settlements[19].nounId, 1); + + assertEq(settlements[0].amount, 20 ether); + assertEq(settlements[1].amount, 19 ether); + assertEq(settlements[2].amount, 18 ether); + assertEq(settlements[10].amount, 10 ether); + assertEq(settlements[11].amount, 9 ether); + assertEq(settlements[19].amount, 1 ether); + + uint256[] memory prices = auction.getPrices(20); + // prettier-ignore + expectedPrices = [20e18, 19e18, 18e18, 17e18, 16e18, 15e18, 14e18, 13e18, 12e18, 11e18, + 10e18, 9e18, 8e18, 7e18, 6e18, 5e18, 4e18, 3e18, 2e18, 1e18]; + assertEq(prices, expectedPrices); + } + + function test_prices_skipsEmptySettlementsPostWarmUp() public { + nounIds = [0, 1, 2, 10, 11, 20, 21]; + auction.warmUpSettlementState(nounIds); + + for (uint256 i = 1; i <= 20; ++i) { + address bidder = makeAddr(vm.toString(i)); + bidAndWinCurrentAuction(bidder, i * 1e18); + } + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(20); + assertEq(settlements[0].nounId, 22); + assertEq(settlements[1].nounId, 21); + assertEq(settlements[2].nounId, 19); + assertEq(settlements[10].nounId, 11); + assertEq(settlements[11].nounId, 9); + assertEq(settlements[19].nounId, 1); + + uint256[] memory prices = auction.getPrices(20); + // prettier-ignore + expectedPrices = [20e18, 19e18, 18e18, 17e18, 16e18, 15e18, 14e18, 13e18, 12e18, 11e18, + 10e18, 9e18, 8e18, 7e18, 6e18, 5e18, 4e18, 3e18, 2e18, 1e18]; + assertEq(prices, expectedPrices); + + settlements = auction.getSettlements(20, 21); + assertEq(settlements.length, 0); + + prices = auction.getPrices(20, 21); + assertEq(prices.length, 0); + } + + function test_prices_2AuctionsNoNewAuction_includesSettledNoun() public { + uint256 bid1Timestamp = bidAndWinCurrentAuction(makeAddr('bidder'), 1 ether); + uint256 bid2Timestamp = bidDontCreateNewAuction(makeAddr('bidder 2'), 2 ether); + + vm.prank(auction.owner()); + auction.pause(); + auction.settleAuction(); + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(2); + + assertEq(settlements.length, 2); + assertEq(settlements[0].blockTimestamp, uint32(bid2Timestamp)); + assertEq(settlements[0].nounId, 2); + assertEq(settlements[0].amount, 2 ether); + assertEq(settlements[0].winner, makeAddr('bidder 2')); + assertEq(settlements[1].blockTimestamp, uint32(bid1Timestamp)); + assertEq(settlements[1].nounId, 1); + assertEq(settlements[1].amount, 1 ether); + assertEq(settlements[1].winner, makeAddr('bidder')); + + uint256[] memory prices = auction.getPrices(2); + assertEq(prices[0], 2 ether); + assertEq(prices[1], 1 ether); + } + + function test_prices_givenMissingAuctionData_skipsMissingNounIDs() public { + address bidder = makeAddr('some bidder'); + bidAndWinCurrentAuction(bidder, 1 ether); + + vm.startPrank(address(auction)); + for (uint256 i = 0; i < 3; ++i) { + auction.nouns().mint(); + } + vm.stopPrank(); + + bidAndWinCurrentAuction(bidder, 2 ether); + bidAndWinCurrentAuction(bidder, 3 ether); + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(3); + assertEq(settlements.length, 3); + assertEq(settlements[0].nounId, 6); + assertEq(settlements[0].amount, 3 ether); + assertEq(settlements[0].winner, bidder); + assertEq(settlements[1].nounId, 2); + assertEq(settlements[1].amount, 2 ether); + assertEq(settlements[1].winner, bidder); + assertEq(settlements[2].nounId, 1); + assertEq(settlements[2].amount, 1 ether); + assertEq(settlements[2].winner, bidder); + + uint256[] memory prices = auction.getPrices(3); + assertEq(prices.length, 3); + assertEq(prices[0], 3 ether); + assertEq(prices[1], 2 ether); + assertEq(prices[2], 1 ether); + } + + function test_prices_withRange_givenBiggerRangeThanAuctionsReturnsAuctionsAndZeroObservations() public { + uint256 lastBidTime; + for (uint256 i = 1; i <= 3; ++i) { + address bidder = makeAddr(vm.toString(i)); + lastBidTime = bidAndWinCurrentAuction(bidder, i * 1e18); + } + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(0, 5); + // lastest ID 4 has no settlement data, so it's not included in the result + assertEq(settlements.length, 3); + assertEq(settlements[0].nounId, 1); + assertEq(settlements[0].amount, 1 ether); + assertEq(settlements[0].winner, makeAddr('1')); + assertEq(settlements[1].nounId, 2); + assertEq(settlements[1].amount, 2 ether); + assertEq(settlements[1].winner, makeAddr('2')); + assertEq(settlements[2].blockTimestamp, uint32(lastBidTime)); + assertEq(settlements[2].nounId, 3); + assertEq(settlements[2].amount, 3 ether); + assertEq(settlements[2].winner, makeAddr('3')); + + uint256[] memory prices = auction.getPrices(0, 5); + expectedPrices = [1 ether, 2 ether, 3 ether]; + assertEq(prices, expectedPrices); + } + + function test_prices_withRange_givenSmallerRangeThanAuctionsReturnsAuctions() public { + for (uint256 i = 1; i <= 20; ++i) { + address bidder = makeAddr(vm.toString(i)); + bidAndWinCurrentAuction(bidder, i * 1e18); + } + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(7, 12); + assertEq(settlements.length, 4); + assertEq(settlements[0].nounId, 7); + assertEq(settlements[0].amount, 7 ether); + assertEq(settlements[0].winner, makeAddr('7')); + assertEq(settlements[1].nounId, 8); + assertEq(settlements[1].amount, 8 ether); + assertEq(settlements[1].winner, makeAddr('8')); + assertEq(settlements[2].nounId, 9); + assertEq(settlements[2].amount, 9 ether); + assertEq(settlements[2].winner, makeAddr('9')); + assertEq(settlements[3].nounId, 11); + assertEq(settlements[3].amount, 10 ether); + assertEq(settlements[3].winner, makeAddr('10')); + + uint256[] memory prices = auction.getPrices(7, 12); + expectedPrices = [7 ether, 8 ether, 9 ether, 10 ether]; + assertEq(prices, expectedPrices); + } + + function test_prices_withRange_givenMissingAuctionData_skipsMissingNounIDs() public { + address bidder = makeAddr('some bidder'); + bidAndWinCurrentAuction(bidder, 1 ether); + + vm.startPrank(address(auction)); + for (uint256 i = 0; i < 3; ++i) { + auction.nouns().mint(); + } + vm.stopPrank(); + + bidAndWinCurrentAuction(bidder, 2 ether); + bidAndWinCurrentAuction(bidder, 3 ether); + + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(1, 7); + assertEq(settlements.length, 3); + assertEq(settlements[0].nounId, 1); + assertEq(settlements[0].amount, 1 ether); + assertEq(settlements[0].winner, bidder); + assertEq(settlements[1].nounId, 2); + assertEq(settlements[1].amount, 2 ether); + assertEq(settlements[1].winner, bidder); + assertEq(settlements[2].nounId, 6); + assertEq(settlements[2].amount, 3 ether); + assertEq(settlements[2].winner, bidder); + + uint256[] memory prices = auction.getPrices(1, 7); + expectedPrices = [1 ether, 2 ether, 3 ether]; + assertEq(prices, expectedPrices); + } + + function test_setPrices_revertsForNonOwner() public { + INounsAuctionHouseV2.Settlement[] memory settlements = new INounsAuctionHouseV2.Settlement[](1); + settlements[0] = INounsAuctionHouseV2.Settlement({ + blockTimestamp: uint32(block.timestamp), + amount: 42 ether, + winner: makeAddr('winner'), + nounId: 3 + }); + + vm.expectRevert('Ownable: caller is not the owner'); + auction.setPrices(settlements); + } + + function test_setPrices_worksForOwner() public { + INounsAuctionHouseV2.Settlement[] memory settlements = new INounsAuctionHouseV2.Settlement[](20); + uint256[] memory nounIds = new uint256[](20); + uint256[] memory prices = new uint256[](20); + + uint256 nounId = 0; + for (uint256 i = 0; i < 20; ++i) { + // skip Nouners + if (nounId <= 1820 && nounId % 10 == 0) { + nounId++; + } + + uint256 price = nounId * 1 ether; + + settlements[i] = INounsAuctionHouseV2.Settlement({ + blockTimestamp: uint32(nounId), + amount: price, + winner: makeAddr(vm.toString(nounId)), + nounId: nounId + }); + + nounIds[i] = nounId; + prices[i] = price; + + nounId++; + } + + vm.expectEmit(true, true, true, true); + emit HistoricPricesSet(nounIds, prices); + + vm.prank(auction.owner()); + auction.setPrices(settlements); + + INounsAuctionHouseV2.Settlement[] memory actualSettlements = auction.getSettlements(0, 23); + uint256[] memory actualPrices = auction.getPrices(0, 23); + assertEq(actualSettlements.length, 20); + assertEq(actualPrices.length, 20); + for (uint256 i = 0; i < 20; ++i) { + assertEq(settlements[i].blockTimestamp, actualSettlements[i].blockTimestamp); + assertEq(settlements[i].amount, actualSettlements[i].amount); + assertEq(settlements[i].winner, actualSettlements[i].winner); + assertEq(settlements[i].nounId, actualSettlements[i].nounId); + assertEq(settlements[i].amount, actualPrices[i]); + } + } +} + +contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { + function test_setTimeBuffer_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + auction.setTimeBuffer(1 days); + } + + function test_setTimeBuffer_revertsGivenValueAboveMax() public { + vm.prank(auction.owner()); + vm.expectRevert('timeBuffer too large'); + auction.setTimeBuffer(1 days + 1); + } + + function test_setTimeBuffer_worksForOwner() public { + assertEq(auction.timeBuffer(), 5 minutes); + + vm.prank(auction.owner()); + auction.setTimeBuffer(1 days); + + assertEq(auction.timeBuffer(), 1 days); + } +} diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol new file mode 100644 index 0000000000..ac689a3b7b --- /dev/null +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { DeployUtilsExcessETHBurner } from '../helpers/DeployUtilsExcessETHBurner.sol'; +import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; +import { INounsToken } from '../../../contracts/interfaces/INounsToken.sol'; +import { INounsDescriptorMinimal } from '../../../contracts/interfaces/INounsDescriptorMinimal.sol'; +import { INounsSeeder } from '../../../contracts/interfaces/INounsSeeder.sol'; +import { ExcessETHBurner, INounsAuctionHouseV2 } from '../../../contracts/governance/ExcessETHBurner.sol'; +import { ERC20Mock, RocketETHMock } from '../helpers/ERC20Mock.sol'; +import { WETH } from '../../../contracts/test/WETH.sol'; +import { ERC721Mock } from '../helpers/ERC721Mock.sol'; + +contract DAOMock { + uint256 adjustedSupply; + + function setAdjustedTotalSupply(uint256 adjustedSupply_) external { + adjustedSupply = adjustedSupply_; + } + + function adjustedTotalSupply() external view returns (uint256) { + return adjustedSupply; + } +} + +contract AuctionMock is INounsAuctionHouseV2 { + uint256[] pricesHistory; + uint128 nounId; + INounsToken nounsToken; + + constructor(INounsToken nounsToken_) { + nounsToken = nounsToken_; + } + + function setNounId(uint128 nounId_) external { + nounId = nounId_; + } + + function setPrices(uint256[] memory pricesHistory_) external { + pricesHistory = pricesHistory_; + } + + function getPrices(uint256) external view override returns (uint256[] memory) { + return pricesHistory; + } + + function auction() external view returns (INounsAuctionHouseV2.AuctionV2 memory) { + return INounsAuctionHouseV2.AuctionV2(nounId, 0, 0, 0, payable(address(0)), false); + } + + function settleAuction() external {} + + function settleCurrentAndCreateNewAuction() external {} + + function createBid(uint256) external payable {} + + function pause() external {} + + function unpause() external {} + + function setTimeBuffer(uint56 timeBuffer) external {} + + function setReservePrice(uint192 reservePrice) external {} + + function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external {} + + function getSettlements(uint256 auctionCount) external view returns (Settlement[] memory settlements) {} + + function getSettlements(uint256 startId, uint256 endId) external view returns (Settlement[] memory settlements) {} + + function getPrices(uint256 startId, uint256 endId) external view returns (uint256[] memory prices) {} + + function warmUpSettlementState(uint256[] calldata nounIds) external {} + + function nouns() external view returns (INounsToken) { + return nounsToken; + } +} + +contract NounsTokenMock is INounsToken, ERC721Mock { + function burn(uint256 tokenId) external {} + + function dataURI(uint256 tokenId) external returns (string memory) {} + + function lockDescriptor() external {} + + function lockMinter() external {} + + function lockSeeder() external {} + + function mint() external returns (uint256) {} + + function setDescriptor(INounsDescriptorMinimal descriptor) external {} + + function setMinter(address minter) external {} + + function setSeeder(INounsSeeder seeder) external {} +} + +contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { + DAOMock dao = new DAOMock(); + NounsTokenMock nounsToken = new NounsTokenMock(); + AuctionMock auction = new AuctionMock(nounsToken); + NounsDAOExecutorV3 treasury; + ExcessETHBurner burner; + + uint64 burnStartNounId; + uint64 nounsBetweenBurns; + uint16 burnWindowSize; + uint16 pastAuctionCount; + + event Burn(uint256 amount, uint128 currentBurnWindowStart, uint128 currentNounId, uint128 newInitialBurnNounId); + + function setUp() public { + burnStartNounId = 1; + nounsBetweenBurns = 100; + pastAuctionCount = 90; + burnWindowSize = 3; + + treasury = _deployExecutorV3(address(dao)); + burner = _deployExcessETHBurner( + treasury, + auction, + burnStartNounId, + nounsBetweenBurns, + burnWindowSize, + pastAuctionCount + ); + + vm.prank(address(treasury)); + treasury.setExcessETHBurner(address(burner)); + + auction.setNounId(burnStartNounId); + + for (uint256 i; i < 400; i++) { + nounsToken.mint(address(1), i); + } + } + + function test_burnExcessETH_beforeNextBurnNounID_reverts() public { + auction.setNounId(0); + + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + } + + function test_burnExcessETH_givenTreasuryBalanceZero_reverts() public { + setMeanPrice(1 ether); + dao.setAdjustedTotalSupply(1); + + vm.expectRevert(ExcessETHBurner.NoExcessToBurn.selector); + burner.burnExcessETH(); + } + + function test_burnExcessETH_givenExcess_burnsAndSetsNextBurnNounID() public { + setMeanPrice(1 ether); + dao.setAdjustedTotalSupply(1); + vm.deal(address(treasury), 100 ether); + + vm.expectEmit(true, true, true, true); + emit Burn(99 ether, 1, 1, 101); + uint256 burnedAmount = burner.burnExcessETH(); + + assertEq(burnedAmount, 99 ether); + assertEq(address(treasury).balance, 1 ether); + assertEq(burner.initialBurnNounId(), 101); + } + + function test_burnExcessETH_allowedOnlyWithinWindow() public { + setMeanPrice(1 ether); + dao.setAdjustedTotalSupply(1); + vm.deal(address(treasury), 100 ether); + + // window allows for noun id between 1 and 4 + auction.setNounId(5); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.setNounId(4); + burner.burnExcessETH(); + + // now window allows for noun id between 101 and 104 + vm.deal(address(treasury), 100 ether); + + auction.setNounId(100); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.setNounId(105); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.setNounId(102); + burner.burnExcessETH(); + } + + function test_burnExcessETH_canSkipBurnWindows() public { + setMeanPrice(1 ether); + dao.setAdjustedTotalSupply(1); + vm.deal(address(treasury), 100 ether); + + auction.setNounId(303); + vm.expectEmit(true, true, true, true); + emit Burn(99 ether, 301, 303, 401); + burner.burnExcessETH(); + } + + function test_burnExcessETH_revertsIfAuctionReturnsInvalidNounId() public { + auction.setNounId(100000000); + + vm.expectRevert('ERC721: owner query for nonexistent token'); + burner.burnExcessETH(); + } + + function test_burnExcessETH_givenABurn_allowsBurnOnlyAfterEnoughNounMints() public { + setMeanPrice(1 ether); + dao.setAdjustedTotalSupply(1); + vm.deal(address(treasury), 100 ether); + + assertEq(burner.burnExcessETH(), 99 ether); + assertEq(address(treasury).balance, 1 ether); + assertEq(burner.initialBurnNounId(), 101); + + vm.deal(address(treasury), 100 ether); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.setNounId(2); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.setNounId(101); + assertEq(burner.burnExcessETH(), 99 ether); + assertEq(burner.initialBurnNounId(), 201); + } + + function test_burnExcessETH_givenExpectedValueGreaterThanTreasury_reverts() public { + setMeanPrice(1 ether); + dao.setAdjustedTotalSupply(100); + vm.deal(address(treasury), 10 ether); + + vm.expectRevert(ExcessETHBurner.NoExcessToBurn.selector); + burner.burnExcessETH(); + } + + function test_burnExcessETH_givenExcessGreaterThanBalance_burnsBalance() public { + vm.deal(address(treasury), 1 ether); + dao.setAdjustedTotalSupply(1); + setMeanPrice(1 ether); + ERC20Mock(address(burner.stETH())).mint(address(treasury), 100 ether); + + assertEq(burner.burnExcessETH(), 1 ether); + assertEq(address(treasury).balance, 0); + } + + function test_burnExcessETH_givenBalancesInAllERC20s_takesThemIntoAccount() public { + // expected value = 10 ETH + dao.setAdjustedTotalSupply(10); + setMeanPrice(1 ether); + + // giving treasury 11 ETH -> Excess grows to 1 ETH + vm.deal(address(treasury), 11 ether); + + // giving 1 stETH -> excess grows to 2 ETH + ERC20Mock(address(burner.stETH())).mint(address(treasury), 1 ether); + + // giving 1 WETH -> excess grows to 3 ETH + WETH weth = WETH(payable(address(burner.wETH()))); + weth.deposit{ value: 1 ether }(); + weth.transfer(address(treasury), 1 ether); + + // giving 1 rETH at a rate of 2 -> excess grows to 5 ETH + RocketETHMock reth = RocketETHMock(address(burner.rETH())); + reth.setRate(2); + reth.mint(address(treasury), 1 ether); + + assertEq(burner.burnExcessETH(), 5 ether); + } + + function test_burnExcessETH_givenRecentAuctionPriceChange_expectedTreasuryValueDropsAsExpected() public { + vm.deal(address(treasury), 100 ether); + + dao.setAdjustedTotalSupply(1); + setMeanPrice(100 ether); + + assertEq(burner.excessETH(), 0); + + // (100 * 88 + 10 * 2) / 90 = 98 + // with 1 noun in supply, expected value is 98 ETH + uint256[] memory recentPrices = new uint256[](2); + recentPrices[0] = 10 ether; + recentPrices[1] = 10 ether; + setUniformPastAuctionsWithDifferentRecentPrices(100 ether, recentPrices); + + assertEq(burner.burnExcessETH(), 2 ether); + } + + function test_burnExcessETH_givenInsufficientAuctionHistory_reverts() public { + vm.deal(address(treasury), 100 ether); + dao.setAdjustedTotalSupply(1); + setPriceHistory(1 ether, pastAuctionCount - 1); + + vm.expectRevert(ExcessETHBurner.NotEnoughAuctionHistory.selector); + burner.burnExcessETH(); + } + + function test_setInitialBurnNounId_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + burner.setInitialBurnNounId(1); + } + + function test_setInitialBurnNounId_worksForOwner() public { + assertTrue(burner.initialBurnNounId() != 142); + + vm.prank(address(treasury)); + burner.setInitialBurnNounId(142); + + assertEq(burner.initialBurnNounId(), 142); + } + + function test_setNounIdsBetweenBurns_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + burner.setNounIdsBetweenBurns(1); + } + + function test_setNounIdsBetweenBurns_worksForOwner() public { + assertTrue(burner.nounIdsBetweenBurns() != 142); + + vm.prank(address(treasury)); + burner.setNounIdsBetweenBurns(142); + + assertEq(burner.nounIdsBetweenBurns(), 142); + } + + function test_setNumberOfPastAuctionsForMeanPrice_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + burner.setNumberOfPastAuctionsForMeanPrice(1); + } + + function test_setNumberOfPastAuctionsForMeanPrice_worksForOwner() public { + assertTrue(burner.numberOfPastAuctionsForMeanPrice() != 142); + + vm.prank(address(treasury)); + burner.setNumberOfPastAuctionsForMeanPrice(142); + + assertEq(burner.numberOfPastAuctionsForMeanPrice(), 142); + } + + function test_setNumberOfPastAuctionsForMeanPrice_revertsIfValueIsTooLow() public { + vm.prank(address(treasury)); + vm.expectRevert(ExcessETHBurner.PastAuctionCountTooLow.selector); + burner.setNumberOfPastAuctionsForMeanPrice(1); + } + + function test_setBurnWindowSize_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + burner.setBurnWindowSize(5); + } + + function test_setBurnWindowSize_worksForOwner() public { + assertTrue(burner.burnWindowSize() != 5); + + vm.prank(address(treasury)); + burner.setBurnWindowSize(5); + + assertEq(burner.burnWindowSize(), 5); + } + + function setMeanPrice(uint256 meanPrice) internal { + setPriceHistory(meanPrice, pastAuctionCount); + } + + function setPriceHistory(uint256 meanPrice, uint256 count) internal { + uint256[] memory prices = new uint256[](count); + for (uint256 i = 0; i < count; i++) { + prices[i] = meanPrice; + } + auction.setPrices(prices); + } + + function setUniformPastAuctionsWithDifferentRecentPrices(uint256 meanPrice, uint256[] memory recent) internal { + uint256[] memory prices = new uint256[](pastAuctionCount + recent.length); + for (uint256 i = 0; i < recent.length; i++) { + prices[i] = recent[i]; + } + for (uint256 i = 0; i < pastAuctionCount; i++) { + prices[i + recent.length] = meanPrice; + } + auction.setPrices(prices); + } +} diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol new file mode 100644 index 0000000000..d0046e96f9 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { DeployUtilsExcessETHBurner } from '../helpers/DeployUtilsExcessETHBurner.sol'; +import { AuctionHouseUpgrader } from '../helpers/AuctionHouseUpgrader.sol'; +import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; +import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; +import { NounsDAOExecutorV2 } from '../../../contracts/governance/NounsDAOExecutorV2.sol'; +import { ExcessETHBurner, INounsAuctionHouseV2 } from '../../../contracts/governance/ExcessETHBurner.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseV2 } from '../../../contracts/NounsAuctionHouseV2.sol'; + +contract NounsDAOExecutorV3Test is DeployUtilsExcessETHBurner { + event ETHBurned(uint256 amount); + + NounsDAOExecutorV3 treasury; + address burner = makeAddr('burner'); + + address dao = makeAddr('dao'); + + function setUp() public { + treasury = _deployExecutorV3(dao); + + vm.prank(address(treasury)); + treasury.setExcessETHBurner(burner); + } + + function test_setExcessETHBurner_revertsForNonTreasury() public { + vm.expectRevert('NounsDAOExecutor::setExcessETHBurner: Call must come from NounsDAOExecutor.'); + treasury.setExcessETHBurner(burner); + } + + function test_setExcessETHBurner_worksForTreasury() public { + address newBurner = makeAddr('newBurner'); + + assertTrue(treasury.excessETHBurner() != newBurner); + + vm.prank(address(treasury)); + treasury.setExcessETHBurner(newBurner); + + assertEq(treasury.excessETHBurner(), newBurner); + } + + function test_burnExcessETH_revertsForNonBurner() public { + vm.expectRevert(NounsDAOExecutorV3.OnlyExcessETHBurner.selector); + treasury.burnExcessETH(1); + } + + function test_burnExcessETH_worksForBurner() public { + vm.deal(address(treasury), 142 ether); + + vm.expectEmit(true, true, true, true); + emit ETHBurned(142 ether); + + vm.prank(burner); + treasury.burnExcessETH(142 ether); + + assertEq(treasury.totalETHBurned(), 142 ether); + assertEq(address(treasury).balance, 0); + } + + function test_burnExcessETH_addsToTotalETHBurned() public { + vm.deal(address(treasury), 142 ether); + + vm.prank(burner); + treasury.burnExcessETH(100 ether); + assertEq(treasury.totalETHBurned(), 100 ether); + + vm.prank(burner); + treasury.burnExcessETH(42 ether); + assertEq(treasury.totalETHBurned(), 142 ether); + } +} + +contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { + event ETHBurned(uint256 amount); + + NounsDAOLogicV3 dao; + address payable treasury; + NounsAuctionHouseV2 auction; + + address nouner = makeAddr('nouner'); + + function setUp() public { + dao = _deployDAOV3(); + treasury = payable(address(dao.timelock())); + + upgradeAuction(); + auction = NounsAuctionHouseV2(payable(dao.nouns().minter())); + vm.prank(auction.owner()); + auction.unpause(); + + vm.deal(nouner, 1 ether); + + // After this auction total supply is 3: + // ID 0 went to nounders + // 1 going to nouner now + // 2 born after this auction is settled, in this same tx + bidAndWinCurrentAuction(nouner, 1 ether); + vm.roll(block.number + 1); + } + + function test_upgradeViaProposal_andBurnHappyFlow() public { + uint256 meanPrice = 0.1 ether; + generateAuctionHistory(90, meanPrice); + + NounsDAOExecutorV3 newImpl = new NounsDAOExecutorV3(); + + vm.startPrank(nouner); + uint256 proposalId = propose(treasury, 0, 'upgradeTo(address)', abi.encode(address(newImpl))); + getProposalToExecution(proposalId); + vm.stopPrank(); + + assertEq(auction.auction().nounId, 102); + + ExcessETHBurner burner = _deployExcessETHBurner( + NounsDAOExecutorV3(treasury), + INounsAuctionHouseV2(dao.nouns().minter()), + 202, // burnStartNounID + 100, // nounsBetweenBurns + 3, // burnWindowSize + 90 // pastAuctionCount + ); + + vm.startPrank(nouner); + proposalId = propose(treasury, 0, 'setExcessETHBurner(address)', abi.encode(address(burner))); + getProposalToExecution(proposalId); + vm.stopPrank(); + + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.settleCurrentAndCreateNewAuction(); + generateAuctionHistory(89, meanPrice); + assertEq(auction.auction().nounId, 202); + + vm.expectRevert(ExcessETHBurner.NoExcessToBurn.selector); + burner.burnExcessETH(); + + vm.deal(address(treasury), 100 ether); + + // adjustedSupply: 202 + // meanPrice: 0.1 ether + // expected value: 202 * 0.1 = 20.2 ETH + // treasury size: 100 ETH + // excess: 100 - 20.2 = 79.8 ETH + vm.expectEmit(true, true, true, true); + emit ETHBurned(79.8 ether); + uint256 burnedETH = burner.burnExcessETH(); + assertEq(burnedETH, 79.8 ether); + } + + function getProposalToExecution(uint256 proposalId) internal { + vm.roll(block.number + dao.proposalUpdatablePeriodInBlocks() + dao.votingDelay() + 1); + dao.castVote(proposalId, 1); + vm.roll(block.number + dao.votingPeriod() + 1); + dao.queue(proposalId); + vm.warp(block.timestamp + NounsDAOExecutorV2(treasury).delay() + 1); + dao.execute(proposalId); + } + + function propose( + address target, + uint256 value, + string memory signature, + bytes memory data + ) internal returns (uint256 proposalId) { + address[] memory targets = new address[](1); + targets[0] = target; + uint256[] memory values = new uint256[](1); + values[0] = value; + string[] memory signatures = new string[](1); + signatures[0] = signature; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = data; + proposalId = dao.propose(targets, values, signatures, calldatas, 'my proposal'); + } + + bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + function upgradeAuction() internal { + bytes32 proxyAdminBytes = vm.load(address(dao.nouns().minter()), _ADMIN_SLOT); + address proxyAdminAddress = address(uint160(uint256(proxyAdminBytes))); + AuctionHouseUpgrader.upgradeAuctionHouse( + treasury, + NounsAuctionHouseProxyAdmin(proxyAdminAddress), + NounsAuctionHouseProxy(payable(dao.nouns().minter())) + ); + } + + function generateAuctionHistory(uint256 count, uint256 meanPrice) internal { + vm.deal(nouner, count * meanPrice); + for (uint256 i = 0; i < count; ++i) { + bidAndWinCurrentAuction(nouner, meanPrice); + } + } + + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { + INounsAuctionHouseV2.AuctionV2 memory auction_ = auction.auction(); + vm.deal(bidder, bid); + vm.prank(bidder); + auction.createBid{ value: bid }(auction_.nounId); + vm.warp(auction_.endTime); + auction.settleCurrentAndCreateNewAuction(); + return block.timestamp; + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol b/packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol new file mode 100644 index 0000000000..6c70290b5c --- /dev/null +++ b/packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { NounsAuctionHouse } from '../../../contracts/NounsAuctionHouse.sol'; +import { NounsAuctionHouseV2 } from '../../../contracts/NounsAuctionHouseV2.sol'; +import { NounsAuctionHousePreV2Migration } from '../../../contracts/NounsAuctionHousePreV2Migration.sol'; +import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import 'forge-std/Vm.sol'; + +library AuctionHouseUpgrader { + Vm private constant vm = Vm(address(uint160(uint256(keccak256('hevm cheat code'))))); + + function upgradeAuctionHouse( + address owner, + NounsAuctionHouseProxyAdmin proxyAdmin, + NounsAuctionHouseProxy proxy + ) internal { + NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(proxy)); + + NounsAuctionHouseV2 newLogic = new NounsAuctionHouseV2( + auctionV1.nouns(), + auctionV1.weth(), + auctionV1.duration() + ); + NounsAuctionHousePreV2Migration migratorLogic = new NounsAuctionHousePreV2Migration(); + + vm.startPrank(owner); + + // not using upgradeAndCall because the call must come from the auction house owner + // which is owner, not the proxy admin + + proxyAdmin.upgrade(proxy, address(migratorLogic)); + NounsAuctionHousePreV2Migration migrator = NounsAuctionHousePreV2Migration(address(proxy)); + migrator.migrate(); + proxyAdmin.upgrade(proxy, address(newLogic)); + + vm.stopPrank(); + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/BidderWithGasGriefing.sol b/packages/nouns-contracts/test/foundry/helpers/BidderWithGasGriefing.sol new file mode 100644 index 0000000000..50a59e1bca --- /dev/null +++ b/packages/nouns-contracts/test/foundry/helpers/BidderWithGasGriefing.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.6; + +import { NounsAuctionHouseV2 } from '../../../contracts/NounsAuctionHouseV2.sol'; + +contract BidderWithGasGriefing { + function bid(NounsAuctionHouseV2 auctionHouse, uint256 nounId) public payable { + auctionHouse.createBid{ value: msg.value }(nounId); + } + + receive() external payable { + assembly { + return(0, 107744) + } + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index ad73761ed5..3166412427 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -17,9 +17,10 @@ import { NounsDAOProxy } from '../../../contracts/governance/NounsDAOProxy.sol'; import { NounsDAOStorageV2 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; import { NounsDAOProxyV2 } from '../../../contracts/governance/NounsDAOProxyV2.sol'; import { Inflator } from '../../../contracts/Inflator.sol'; -import { NounsAuctionHouse } from '../../../contracts/NounsAuctionHouse.sol'; import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionHouseProxy.sol'; import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import { NounsAuctionHouse } from '../../../contracts/NounsAuctionHouse.sol'; +import { WETH } from '../../../contracts/test/WETH.sol'; abstract contract DeployUtils is Test, DescriptorHelpers { uint256 constant TIMELOCK_DELAY = 2 days; @@ -27,6 +28,40 @@ abstract contract DeployUtils is Test, DescriptorHelpers { uint256 constant VOTING_DELAY = 1; uint256 constant PROPOSAL_THRESHOLD = 1; uint256 constant QUORUM_VOTES_BPS = 2000; + uint256 constant AUCTION_TIME_BUFFER = 5 minutes; + uint256 constant AUCTION_RESERVE_PRICE = 1; + uint8 constant AUCTION_MIN_BID_INCREMENT_PRCT = 2; + uint256 constant AUCTION_DURATION = 24 hours; + + function _deployAuctionHouseV1AndToken( + address owner, + address noundersDAO, + address minter + ) internal returns (NounsAuctionHouseProxy, NounsAuctionHouseProxyAdmin) { + NounsAuctionHouse logic = new NounsAuctionHouse(); + NounsToken token = deployToken(noundersDAO, minter); + WETH weth = new WETH(); + NounsAuctionHouseProxyAdmin admin = new NounsAuctionHouseProxyAdmin(); + admin.transferOwnership(owner); + + bytes memory data = abi.encodeWithSelector( + NounsAuctionHouse.initialize.selector, + address(token), + address(weth), + AUCTION_TIME_BUFFER, + AUCTION_RESERVE_PRICE, + AUCTION_MIN_BID_INCREMENT_PRCT, + AUCTION_DURATION + ); + NounsAuctionHouseProxy proxy = new NounsAuctionHouseProxy(address(logic), address(admin), data); + NounsAuctionHouse auction = NounsAuctionHouse(address(proxy)); + + auction.transferOwnership(owner); + token.setMinter(address(proxy)); + + return (proxy, admin); + } + uint32 constant LAST_MINUTE_BLOCKS = 10; uint32 constant OBJECTION_PERIOD_BLOCKS = 10; uint32 constant UPDATABLE_PERIOD_BLOCKS = 10; @@ -93,6 +128,13 @@ abstract contract DeployUtils is Test, DescriptorHelpers { return (address(nounsToken), address(proxy)); } + function deployToken(address noundersDAO, address minter) internal returns (NounsToken nounsToken) { + IProxyRegistry proxyRegistry = IProxyRegistry(address(3)); + NounsDescriptorV2 descriptor = _deployAndPopulateV2(); + + nounsToken = new NounsToken(noundersDAO, minter, descriptor, new NounsSeeder(), proxyRegistry); + } + function _createDAOV2Proxy( address timelock, address nounsToken, diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol new file mode 100644 index 0000000000..098808843c --- /dev/null +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { DeployUtilsV3 } from './DeployUtilsV3.sol'; +import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; +import { ExcessETHBurner, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETHBurner.sol'; +import { WETH } from '../../../contracts/test/WETH.sol'; +import { ERC20Mock, RocketETHMock } from './ERC20Mock.sol'; +import { ERC1967Proxy } from '@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +abstract contract DeployUtilsExcessETHBurner is DeployUtilsV3 { + function _deployExecutorV3(address dao) internal returns (NounsDAOExecutorV3) { + NounsDAOExecutorV3 executor = NounsDAOExecutorV3( + payable(address(new ERC1967Proxy(address(new NounsDAOExecutorV3()), ''))) + ); + executor.initialize(dao, TIMELOCK_DELAY); + return executor; + } + + function _deployExcessETHBurner( + NounsDAOExecutorV3 owner, + INounsAuctionHouseV2 auction, + uint64 burnStartNounID, + uint64 nounsBetweenBurns, + uint16 burnWindowSize, + uint16 pastAuctionCount + ) internal returns (ExcessETHBurner burner) { + WETH weth = new WETH(); + ERC20Mock stETH = new ERC20Mock(); + RocketETHMock rETH = new RocketETHMock(); + + burner = new ExcessETHBurner( + address(owner), + INounsDAOV3(owner.admin()), + auction, + IERC20(address(weth)), + stETH, + rETH, + burnStartNounID, + nounsBetweenBurns, + burnWindowSize, + pastAuctionCount + ); + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol index 0c2176d682..45c8d60703 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol @@ -20,6 +20,7 @@ import { NounsTokenFork } from '../../../contracts/governance/fork/newdao/token/ import { NounsAuctionHouseFork } from '../../../contracts/governance/fork/newdao/NounsAuctionHouseFork.sol'; import { NounsDAOLogicV1Fork } from '../../../contracts/governance/fork/newdao/governance/NounsDAOLogicV1Fork.sol'; import { NounsDAOStorageV3 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; +import { AuctionHouseUpgrader } from './AuctionHouseUpgrader.sol'; abstract contract DeployUtilsV3 is DeployUtils { function _createDAOV3Proxy( @@ -133,6 +134,9 @@ abstract contract DeployUtilsV3 is DeployUtils { vm.prank(address(timelock)); NounsAuctionHouse(address(auctionProxy)).initialize(nounsToken, makeAddr('weth'), 2, 0, 1, 10 minutes); + vm.prank(address(timelock)); + AuctionHouseUpgrader.upgradeAuctionHouse(address(timelock), auctionAdmin, auctionProxy); + vm.prank(address(timelock)); timelock.setPendingAdmin(address(dao)); vm.prank(address(dao)); diff --git a/packages/nouns-contracts/test/foundry/helpers/ERC20Mock.sol b/packages/nouns-contracts/test/foundry/helpers/ERC20Mock.sol index 79cafa7214..e949c77034 100644 --- a/packages/nouns-contracts/test/foundry/helpers/ERC20Mock.sol +++ b/packages/nouns-contracts/test/foundry/helpers/ERC20Mock.sol @@ -44,3 +44,19 @@ contract ERC20Mock is ERC20 { wasTransferCalled = wasTransferCalled_; } } + +contract RocketETHMock is ERC20Mock { + uint256 rate; + + constructor() ERC20Mock() { + rate = 1; + } + + function setRate(uint256 rate_) public { + rate = rate_; + } + + function getEthValue(uint256 _rethAmount) external view returns (uint256) { + return _rethAmount * rate; + } +} diff --git a/packages/nouns-sdk/src/contract/addresses.json b/packages/nouns-sdk/src/contract/addresses.json index 72acf59950..f27b823c28 100644 --- a/packages/nouns-sdk/src/contract/addresses.json +++ b/packages/nouns-sdk/src/contract/addresses.json @@ -54,17 +54,16 @@ "nounsDAOData": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" }, "11155111": { - "nounsToken": "0x4C4674bb72a096855496a7204962297bd7e12b85", - "nounsSeeder": "0xe99b8Ee07B28C587B755f348649f3Ee45aDA5E7D", - "nounsDescriptor": "0x5319dbcb313738aD70a3D945E61ceB8b84691928", - "nftDescriptor": "0xF5A7A2f948b6b2B1BD6E25C6ddE4dA892301caB5", - "nounsAuctionHouse": "0x44FeBD884Abf796d2d198974A768CBD882a959a8", - "nounsAuctionHouseProxy": "0x488609b7113FCf3B761A05956300d605E8f6BcAf", - "nounsAuctionHouseProxyAdmin": "0x9A19E520d9cd6c40eCc79623f16390a68962b7E9", - "nounsDaoExecutor": "0x332db58b51393f3a6b28d4DD8964234967e1aD33", - "nounsDaoExecutorProxy": "0x07e5D6a1550aD5E597A9b0698A474AA080A2fB28", - "nounsDAOProxy": "0x35d2670d7C8931AACdd37C89Ddcb0638c3c44A57", - "nounsDAOLogicV2": "0x1634D5abB2c0BBF7B817b791C8355a39f2EcEF0A", - "nounsDAOData": "0x9040f720AA8A693F950B9cF94764b4b06079D002" + "nounsToken": "0x2824dcE6253476cBfAB91764F5715763d6e451a3", + "nounsSeeder": "0x8CfedAb19379615f76f31F8c8dCaDf7cE883e7A4", + "nounsDescriptor": "0x163EC071da05E49F49c44961CA4Db90Fb15711BE", + "nftDescriptor": "0x503FE998a52dCbe908A7572ed548c222423B753d", + "nounsAuctionHouse": "0x59A4C36d2c4e1942433a21649e7d0e9ad8E5EdFD", + "nounsAuctionHouseProxy": "0x45ebbdb0E66aC2a8339D98aDB6934C89f166A754", + "nounsAuctionHouseProxyAdmin": "0x7aC3910999565801A0F2ad8623933B3fb1c488ab", + "nounsDaoExecutor": "0x6c2dD53b8DbDD3af1209DeB9dA87D487EaE8E638", + "nounsDAOProxy": "0xBE875f62C35124e27C3F638164049617b883B746", + "nounsDAOLogicV1": "0xe67E2e26E89E1F08227F55A2b8CB42f6028c6b61", + "nounsDAOData": "0x6D1aa9B6Ee004deBE34390d8bc7602A36a4b9655" } } diff --git a/packages/nouns-subgraph/config/sepolia.json b/packages/nouns-subgraph/config/sepolia.json index d32289d597..069e7c334d 100644 --- a/packages/nouns-subgraph/config/sepolia.json +++ b/packages/nouns-subgraph/config/sepolia.json @@ -1,19 +1,19 @@ { "network": "sepolia", "nounsToken": { - "address": "0x4C4674bb72a096855496a7204962297bd7e12b85", - "startBlock": 3594636 + "address": "0x2824dcE6253476cBfAB91764F5715763d6e451a3", + "startBlock": 4418279 }, "nounsAuctionHouse": { - "address": "0x488609b7113FCf3B761A05956300d605E8f6BcAf", - "startBlock": 3594636 + "address": "0x45ebbdb0E66aC2a8339D98aDB6934C89f166A754", + "startBlock": 4418279 }, "nounsDAO": { - "address": "0x35d2670d7C8931AACdd37C89Ddcb0638c3c44A57", - "startBlock": 3594636 + "address": "0xBE875f62C35124e27C3F638164049617b883B746", + "startBlock": 4418279 }, "nounsDAOData": { - "address": "0x9040f720AA8A693F950B9cF94764b4b06079D002", - "startBlock": 3594636 + "address": "0x6D1aa9B6Ee004deBE34390d8bc7602A36a4b9655", + "startBlock": 4418279 } } diff --git a/packages/nouns-subgraph/package.json b/packages/nouns-subgraph/package.json index 1cc2fe1352..5c3af453b6 100644 --- a/packages/nouns-subgraph/package.json +++ b/packages/nouns-subgraph/package.json @@ -26,7 +26,7 @@ "deploy:hardhat": "yarn clean && yarn prepare:hardhat && yarn codegen && yarn create:localnode nounsdao/nouns-subgraph && yarn deploy:localnode nounsdao/nouns-subgraph", "deploy:rinkeby": "yarn clean && yarn prepare:rinkeby && yarn codegen && yarn deploy nounsdao/nouns-subgraph-rinkeby", "deploy:goerli": "yarn clean && yarn prepare:goerli && yarn codegen && yarn graph build && goldsky subgraph deploy nouns-v3-goerli/0.1.6", - "deploy:sepolia": "yarn clean && yarn prepare:sepolia && yarn codegen && yarn graph build && goldsky subgraph deploy nouns-sepolia/0.1.6", + "deploy:sepolia": "yarn clean && yarn prepare:sepolia && yarn codegen && yarn graph build && goldsky subgraph deploy nouns-sepolia-the-burn/0.1.0", "deploy:mainnet": "yarn clean && yarn prepare:mainnet && yarn codegen && yarn graph build && goldsky subgraph deploy nouns/0.2.0", "mustache": "mustache" }, diff --git a/packages/nouns-webapp/src/config.ts b/packages/nouns-webapp/src/config.ts index 4406ec0d94..2f602d41db 100644 --- a/packages/nouns-webapp/src/config.ts +++ b/packages/nouns-webapp/src/config.ts @@ -78,7 +78,7 @@ const app: Record = { jsonRpcUri: createNetworkHttpUrl('sepolia'), wsRpcUri: createNetworkWsUrl('sepolia'), subgraphApiUri: - 'https://api.goldsky.com/api/public/project_cldf2o9pqagp43svvbk5u3kmo/subgraphs/nouns-sepolia-elad/0.1.1/gn', + 'https://api.goldsky.com/api/public/project_cldf2o9pqagp43svvbk5u3kmo/subgraphs/nouns-sepolia-the-burn/0.1.0/gn', enableHistory: process.env.REACT_APP_ENABLE_HISTORY === 'true', }, [ChainId.Mainnet]: { @@ -143,7 +143,7 @@ const getAddresses = (): ContractAddresses => { let nounsAddresses = {} as NounsContractAddresses; try { nounsAddresses = getContractAddressesForChainOrThrow(CHAIN_ID); - } catch { } + } catch {} return { ...nounsAddresses, ...externalAddresses[CHAIN_ID] }; };