From 332b906a1fcc9b82fcaab0f1fa3d5f78a8dd3513 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 17 Oct 2022 18:19:05 -0500 Subject: [PATCH 001/115] initial Noracle lib implementation many tests and refinements TODO --- .../contracts/libs/Noracle.sol | 112 ++++++++++++++++++ .../test/foundry/Noracle.t.sol | 61 ++++++++++ 2 files changed, 173 insertions(+) create mode 100644 packages/nouns-contracts/contracts/libs/Noracle.sol create mode 100644 packages/nouns-contracts/test/foundry/Noracle.t.sol diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol new file mode 100644 index 0000000000..a49d2f0111 --- /dev/null +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title A library used to maintain Nouns Auction House price history + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.6; + +library Noracle { + struct Observation { + // The block.timestamp when the auction was settled + uint256 blockTimestamp; + // ID for the Noun (ERC721 token ID) + uint256 nounId; + // The current highest bid amount + uint256 amount; + // The address of the auction winner + address winner; + // whether or not the observation is initialized + bool initialized; + } + + struct NoracleState { + mapping(uint16 => Observation) observations; + uint16 index; + uint16 cardinality; + uint16 cardinalityNext; + } + + function initialize(NoracleState storage self) internal { + self.cardinality = 1; + self.cardinalityNext = 1; + warmUpObservation(self.observations[0]); + } + + function write( + NoracleState storage self, + uint256 blockTimestamp, + uint256 nounId, + uint256 amount, + address winner + ) internal { + // if the conditions are right, we can bump the cardinality + if (self.cardinalityNext > self.cardinality && self.index == (self.cardinality - 1)) { + self.cardinality = self.cardinalityNext; + } + + self.index = (self.index + 1) % self.cardinality; + self.observations[self.index] = Observation({ + initialized: true, + blockTimestamp: blockTimestamp, + nounId: nounId, + amount: amount, + winner: winner + }); + } + + function grow(NoracleState storage self, uint16 next) internal returns (uint16) { + uint16 current = self.cardinalityNext; + + // no-op if the passed next value isn't greater than the current next value + if (next <= current) return current; + + // store in each slot to prevent fresh SSTOREs in swaps + // this data will not be used because the initialized boolean is still false + for (uint16 i = current; i < next; i++) { + warmUpObservation(self.observations[i]); + } + + return self.cardinalityNext = next; + } + + function observe(NoracleState storage self, uint16 fromAuctionsAgo) + internal + view + returns (Observation[] memory observations) + { + uint16 cardinality = self.cardinality; + require(fromAuctionsAgo <= cardinality, 'too many auctions ago'); + + observations = new Observation[](fromAuctionsAgo); + uint16 initializedObservationsFound = 0; + uint16 checkedIndexesCount = 0; + while (initializedObservationsFound < fromAuctionsAgo && checkedIndexesCount < cardinality) { + checkedIndexesCount++; + uint16 checkIndex = (self.index + (cardinality - checkedIndexesCount)) % cardinality; + Observation storage obs = self.observations[checkIndex]; + if (obs.initialized) { + observations[initializedObservationsFound] = obs; + initializedObservationsFound++; + } + } + } + + function warmUpObservation(Observation storage obs) private { + obs.blockTimestamp = 1; + obs.nounId = 1; + obs.amount = 1; + obs.winner = address(1); + } +} diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol new file mode 100644 index 0000000000..5bb16cf3a0 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/Noracle.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.6; + +import 'forge-std/Test.sol'; +import { Noracle } from '../../contracts/libs/Noracle.sol'; + +contract NoracleTest is Test { + using Noracle for Noracle.NoracleState; + + Noracle.NoracleState state; + + function test_cardinality1_worksWithOneWrite() public { + state.initialize(); + state.write(block.timestamp, 142, 69, address(0xdead)); + + Noracle.Observation[] memory observations = state.observe(1); + + assertEq(observations.length, 1); + assertEq(observations[0].blockTimestamp, block.timestamp); + assertEq(observations[0].nounId, 142); + assertEq(observations[0].amount, 69); + assertEq(observations[0].winner, address(0xdead)); + } + + function test_cardinality1_secondWriteOverrides() public { + state.initialize(); + state.write(block.timestamp, 142, 69, address(0xdead)); + state.write(block.timestamp + 1, 143, 70, address(0x1234)); + + Noracle.Observation[] memory observations = state.observe(1); + + assertEq(observations.length, 1); + assertEq(observations[0].blockTimestamp, block.timestamp + 1); + assertEq(observations[0].nounId, 143); + assertEq(observations[0].amount, 70); + assertEq(observations[0].winner, address(0x1234)); + + vm.expectRevert('too many auctions ago'); + state.observe(2); + } + + function test_cadinality2_secondWriteDoesNotOverride() public { + state.initialize(); + state.write(block.timestamp, 142, 69, address(0xdead)); + state.grow(2); + state.write(block.timestamp + 1, 143, 70, address(0x1234)); + + Noracle.Observation[] memory observations = state.observe(2); + assertEq(observations.length, 2); + + assertEq(observations[0].blockTimestamp, block.timestamp); + assertEq(observations[0].nounId, 142); + assertEq(observations[0].amount, 69); + assertEq(observations[0].winner, address(0xdead)); + + assertEq(observations[1].blockTimestamp, block.timestamp + 1); + assertEq(observations[1].nounId, 143); + assertEq(observations[1].amount, 70); + assertEq(observations[1].winner, address(0x1234)); + } +} From b3389eaa39007ee2ed06be6a6ffe79ad5344a399 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 18 Oct 2022 13:34:03 -0500 Subject: [PATCH 002/115] noracle bit packing --- .../contracts/libs/Noracle.sol | 19 ++++++-------- .../test/foundry/Noracle.t.sol | 26 +++++++++---------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index a49d2f0111..bec5383094 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -20,11 +20,11 @@ pragma solidity ^0.8.6; library Noracle { struct Observation { // The block.timestamp when the auction was settled - uint256 blockTimestamp; + uint32 blockTimestamp; // ID for the Noun (ERC721 token ID) - uint256 nounId; - // The current highest bid amount - uint256 amount; + uint16 nounId; + // The winning bid amount, with 8 decimal places (reducing accuracy to save bits) + uint40 amount; // The address of the auction winner address winner; // whether or not the observation is initialized @@ -46,9 +46,9 @@ library Noracle { function write( NoracleState storage self, - uint256 blockTimestamp, - uint256 nounId, - uint256 amount, + uint32 blockTimestamp, + uint16 nounId, + uint40 amount, address winner ) internal { // if the conditions are right, we can bump the cardinality @@ -93,20 +93,17 @@ library Noracle { uint16 initializedObservationsFound = 0; uint16 checkedIndexesCount = 0; while (initializedObservationsFound < fromAuctionsAgo && checkedIndexesCount < cardinality) { - checkedIndexesCount++; uint16 checkIndex = (self.index + (cardinality - checkedIndexesCount)) % cardinality; Observation storage obs = self.observations[checkIndex]; if (obs.initialized) { observations[initializedObservationsFound] = obs; initializedObservationsFound++; } + checkedIndexesCount++; } } function warmUpObservation(Observation storage obs) private { obs.blockTimestamp = 1; - obs.nounId = 1; - obs.amount = 1; - obs.winner = address(1); } } diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol index 5bb16cf3a0..836e64f386 100644 --- a/packages/nouns-contracts/test/foundry/Noracle.t.sol +++ b/packages/nouns-contracts/test/foundry/Noracle.t.sol @@ -11,7 +11,7 @@ contract NoracleTest is Test { function test_cardinality1_worksWithOneWrite() public { state.initialize(); - state.write(block.timestamp, 142, 69, address(0xdead)); + state.write(uint32(block.timestamp), 142, 69, address(0xdead)); Noracle.Observation[] memory observations = state.observe(1); @@ -24,8 +24,8 @@ contract NoracleTest is Test { function test_cardinality1_secondWriteOverrides() public { state.initialize(); - state.write(block.timestamp, 142, 69, address(0xdead)); - state.write(block.timestamp + 1, 143, 70, address(0x1234)); + state.write(uint32(block.timestamp), 142, 69, address(0xdead)); + state.write(uint32(block.timestamp + 1), 143, 70, address(0x1234)); Noracle.Observation[] memory observations = state.observe(1); @@ -41,21 +41,21 @@ contract NoracleTest is Test { function test_cadinality2_secondWriteDoesNotOverride() public { state.initialize(); - state.write(block.timestamp, 142, 69, address(0xdead)); + state.write(uint32(block.timestamp), 142, 69, address(0xdead)); state.grow(2); - state.write(block.timestamp + 1, 143, 70, address(0x1234)); + state.write(uint32(block.timestamp + 1), 143, 70, address(0x1234)); Noracle.Observation[] memory observations = state.observe(2); assertEq(observations.length, 2); - assertEq(observations[0].blockTimestamp, block.timestamp); - assertEq(observations[0].nounId, 142); - assertEq(observations[0].amount, 69); - assertEq(observations[0].winner, address(0xdead)); + assertEq(observations[0].blockTimestamp, block.timestamp + 1); + assertEq(observations[0].nounId, 143); + assertEq(observations[0].amount, 70); + assertEq(observations[0].winner, address(0x1234)); - assertEq(observations[1].blockTimestamp, block.timestamp + 1); - assertEq(observations[1].nounId, 143); - assertEq(observations[1].amount, 70); - assertEq(observations[1].winner, address(0x1234)); + assertEq(observations[1].blockTimestamp, block.timestamp); + assertEq(observations[1].nounId, 142); + assertEq(observations[1].amount, 69); + assertEq(observations[1].winner, address(0xdead)); } } From 971f661f99f4685362360dfac37ab75600a29fea Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 18 Oct 2022 13:43:04 -0500 Subject: [PATCH 003/115] noracle: test high price keeps 8 decimals --- packages/nouns-contracts/contracts/libs/Noracle.sol | 4 ++++ packages/nouns-contracts/test/foundry/Noracle.t.sol | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index bec5383094..92c8e475c2 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -103,6 +103,10 @@ library Noracle { } } + function ethPriceToUint40(uint256 ethPrice) internal pure returns (uint40) { + return uint40(ethPrice / 1e10); + } + function warmUpObservation(Observation storage obs) private { obs.blockTimestamp = 1; } diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol index 836e64f386..3fca96dc4b 100644 --- a/packages/nouns-contracts/test/foundry/Noracle.t.sol +++ b/packages/nouns-contracts/test/foundry/Noracle.t.sol @@ -22,6 +22,19 @@ contract NoracleTest is Test { assertEq(observations[0].winner, address(0xdead)); } + function test_cardinality1_preserves8DecimalsUnder2KETH() public { + state.initialize(); + state.write(uint32(block.timestamp), 142, Noracle.ethPriceToUint40(1999.98765432109 ether), address(0xdead)); + + Noracle.Observation[] memory observations = state.observe(1); + + assertEq(observations.length, 1); + assertEq(observations[0].blockTimestamp, block.timestamp); + assertEq(observations[0].nounId, 142); + assertEq(observations[0].amount, 199998765432); + assertEq(observations[0].winner, address(0xdead)); + } + function test_cardinality1_secondWriteOverrides() public { state.initialize(); state.write(uint32(block.timestamp), 142, 69, address(0xdead)); From e17cf14dd94077f385a5e1727d7ff7d1fc3ad3a0 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 18 Oct 2022 13:51:13 -0500 Subject: [PATCH 004/115] noracle: gas optimizations and cleanups --- .../contracts/libs/Noracle.sol | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 92c8e475c2..490825cdd4 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -32,10 +32,10 @@ library Noracle { } struct NoracleState { - mapping(uint16 => Observation) observations; - uint16 index; - uint16 cardinality; - uint16 cardinalityNext; + mapping(uint32 => Observation) observations; + uint32 index; + uint32 cardinality; + uint32 cardinalityNext; } function initialize(NoracleState storage self) internal { @@ -52,12 +52,15 @@ library Noracle { address winner ) internal { // if the conditions are right, we can bump the cardinality - if (self.cardinalityNext > self.cardinality && self.index == (self.cardinality - 1)) { - self.cardinality = self.cardinalityNext; + uint32 cardinalityNext = self.cardinalityNext; + uint32 currentIndex = self.index; + if (cardinalityNext > self.cardinality && currentIndex == (self.cardinality - 1)) { + self.cardinality = cardinalityNext; } - self.index = (self.index + 1) % self.cardinality; - self.observations[self.index] = Observation({ + uint32 newIndex = (currentIndex + 1) % self.cardinality; + self.index = newIndex; + self.observations[newIndex] = Observation({ initialized: true, blockTimestamp: blockTimestamp, nounId: nounId, @@ -66,40 +69,42 @@ library Noracle { }); } - function grow(NoracleState storage self, uint16 next) internal returns (uint16) { - uint16 current = self.cardinalityNext; + function grow(NoracleState storage self, uint32 next) internal returns (uint32) { + uint32 current = self.cardinalityNext; // no-op if the passed next value isn't greater than the current next value if (next <= current) return current; - // store in each slot to prevent fresh SSTOREs in swaps + // store in each slot to prevent fresh SSTOREs // this data will not be used because the initialized boolean is still false - for (uint16 i = current; i < next; i++) { + for (uint32 i = current; i < next; i++) { warmUpObservation(self.observations[i]); } return self.cardinalityNext = next; } - function observe(NoracleState storage self, uint16 fromAuctionsAgo) + function observe(NoracleState storage self, uint32 fromAuctionsAgo) internal view returns (Observation[] memory observations) { - uint16 cardinality = self.cardinality; + uint32 cardinality = self.cardinality; require(fromAuctionsAgo <= cardinality, 'too many auctions ago'); + uint32 index = self.index; observations = new Observation[](fromAuctionsAgo); - uint16 initializedObservationsFound = 0; - uint16 checkedIndexesCount = 0; + uint32 initializedObservationsFound = 0; + uint32 checkedIndexesCount = 0; while (initializedObservationsFound < fromAuctionsAgo && checkedIndexesCount < cardinality) { - uint16 checkIndex = (self.index + (cardinality - checkedIndexesCount)) % cardinality; + uint32 checkIndex = (index + (cardinality - checkedIndexesCount)) % cardinality; + checkedIndexesCount++; + Observation storage obs = self.observations[checkIndex]; if (obs.initialized) { observations[initializedObservationsFound] = obs; initializedObservationsFound++; } - checkedIndexesCount++; } } From c3e20bffac7ee4c2bdf7defa1a1d7b66bbde2910 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 18 Oct 2022 16:54:19 -0500 Subject: [PATCH 005/115] noracle: start testing auction house V2 with oracle --- .../contracts/NounsAuctionHouseV2.sol | 293 ++++++++++++++++++ .../contracts/libs/Noracle.sol | 2 + .../test/foundry/NounsAuctionHouseV2.t.sol | 51 +++ .../test/foundry/helpers/DeployUtils.sol | 62 ++++ 4 files changed, 408 insertions(+) create mode 100644 packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol create mode 100644 packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol new file mode 100644 index 0000000000..a3c8fb8f23 --- /dev/null +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -0,0 +1,293 @@ +// 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.6; + +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 { INounsAuctionHouse } from './interfaces/INounsAuctionHouse.sol'; +import { INounsToken } from './interfaces/INounsToken.sol'; +import { IWETH } from './interfaces/IWETH.sol'; +import { Noracle } from './libs/Noracle.sol'; + +contract NounsAuctionHouseV2 is + INounsAuctionHouse, + PausableUpgradeable, + ReentrancyGuardUpgradeable, + OwnableUpgradeable +{ + using Noracle for Noracle.NoracleState; + + // The Nouns ERC721 token contract + INounsToken public nouns; + + // The address of the WETH contract + address public weth; + + // The minimum amount of time left in an auction after a new bid is created + uint256 public timeBuffer; + + // The minimum price accepted in an auction + uint256 public reservePrice; + + // The minimum percentage difference between the last bid amount and the current bid + uint8 public minBidIncrementPercentage; + + // The duration of a single auction + uint256 public duration; + + // The active auction + INounsAuctionHouse.Auction public auction; + + // The Nouns price feed state + Noracle.NoracleState public oracle; + + /** + * @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( + INounsToken _nouns, + address _weth, + uint256 _timeBuffer, + uint256 _reservePrice, + uint8 _minBidIncrementPercentage, + uint256 _duration + ) external initializer { + __Pausable_init(); + __ReentrancyGuard_init(); + __Ownable_init(); + + _pause(); + + nouns = _nouns; + weth = _weth; + timeBuffer = _timeBuffer; + reservePrice = _reservePrice; + minBidIncrementPercentage = _minBidIncrementPercentage; + duration = _duration; + } + + /** + * @notice Settle the current auction, mint a new Noun, and put it up for auction. + */ + function settleCurrentAndCreateNewAuction() external override nonReentrant 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 nonReentrant { + _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 nonReentrant { + INounsAuctionHouse.Auction memory _auction = auction; + + 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' + ); + + address payable lastBidder = _auction.bidder; + + // Refund the last bidder, if applicable + if (lastBidder != address(0)) { + _safeTransferETHWithFallback(lastBidder, _auction.amount); + } + + auction.amount = msg.value; + auction.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; + if (extended) { + auction.endTime = _auction.endTime = block.timestamp + timeBuffer; + } + + emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); + + if (extended) { + emit AuctionExtended(_auction.nounId, _auction.endTime); + } + } + + /** + * @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 (auction.startTime == 0 || auction.settled) { + _createAuction(); + } + } + + /** + * @notice Set the auction time buffer. + * @dev Only callable by the owner. + */ + function setTimeBuffer(uint256 _timeBuffer) external override onlyOwner { + timeBuffer = _timeBuffer; + + emit AuctionTimeBufferUpdated(_timeBuffer); + } + + /** + * @notice Set the auction reserve price. + * @dev Only callable by the owner. + */ + function setReservePrice(uint256 _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) { + uint256 startTime = block.timestamp; + uint256 endTime = startTime + duration; + + auction = Auction({ + nounId: 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 { + INounsAuctionHouse.Auction memory _auction = auction; + + 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"); + + auction.settled = true; + + oracle.write( + uint32(block.timestamp), + uint16(_auction.nounId), + Noracle.ethPriceToUint40(_auction.amount), + _auction.bidder + ); + + 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); + } + + 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, ) = to.call{ value: value, gas: 30_000 }(new bytes(0)); + return success; + } + + function initializeOracle() external onlyOwner { + oracle.initialize(); + } + + function growPriceHistory(uint32 newCardinality) external { + oracle.grow(newCardinality); + + // TODO emit event + } + + function prices(uint32 fromAuctionsAgo) external view returns (Noracle.Observation[] memory observations) { + return oracle.observe(fromAuctionsAgo); + } +} diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 490825cdd4..e71e482d68 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -39,6 +39,8 @@ library Noracle { } function initialize(NoracleState storage self) internal { + if (self.cardinality > 0) return; + self.cardinality = 1; self.cardinalityNext = 1; warmUpObservation(self.observations[0]); 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..0c824c63bc --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Test.sol'; +import { DeployUtils } from './helpers/DeployUtils.sol'; +import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; +import { Noracle } from '../../contracts/libs/Noracle.sol'; + +contract NounsAuctionHouseV2Test is Test, DeployUtils { + address owner = address(0x1111); + address noundersDAO = address(0x2222); + address minter = address(0x3333); + address user = address(0x4444); + + NounsAuctionHouseV2 auction; + + function setUp() public { + (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + owner, + noundersDAO, + minter + ); + + NounsAuctionHouseV2 auctionV2 = new NounsAuctionHouseV2(); + _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy, address(auctionV2)); + + auction = NounsAuctionHouseV2(address(auctionProxy)); + + vm.prank(owner); + auction.unpause(); + vm.roll(block.number + 1); + } + + function test_prices_worksWithOneAuction() public { + (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + vm.deal(user, 1 ether); + vm.prank(user); + auction.createBid{ value: 1 ether }(nounId); + vm.warp(endTime); + auction.settleCurrentAndCreateNewAuction(); + + Noracle.Observation[] memory prices = auction.prices(1); + + assertEq(prices[0].blockTimestamp, uint32(block.timestamp)); + assertEq(prices[0].nounId, 1); + assertEq(prices[0].amount, 1e8); + assertEq(prices[0].winner, user); + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index 783c0962e4..2e5ed2cbff 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -14,6 +14,11 @@ import { NounsSeeder } from '../../../contracts/NounsSeeder.sol'; import { NounsToken } from '../../../contracts/NounsToken.sol'; import { NounsDAOProxy } from '../../../contracts/governance/NounsDAOProxy.sol'; import { Inflator } from '../../../contracts/Inflator.sol'; +import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import { NounsAuctionHouse } from '../../../contracts/NounsAuctionHouse.sol'; +import { NounsAuctionHouseV2 } from '../../../contracts/NounsAuctionHouseV2.sol'; +import { WETH } from '../../../contracts/test/WETH.sol'; abstract contract DeployUtils is Test, DescriptorHelpers { uint256 constant TIMELOCK_DELAY = 2 days; @@ -21,6 +26,55 @@ 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); + } + + function _upgradeAuctionHouse( + address owner, + NounsAuctionHouseProxyAdmin proxyAdmin, + NounsAuctionHouseProxy proxy, + address newLogic + ) internal { + vm.startPrank(owner); + + proxyAdmin.upgrade(proxy, newLogic); + + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(proxy)); + auctionV2.initializeOracle(); + + vm.stopPrank(); + } function _deployAndPopulateV2() internal returns (NounsDescriptorV2) { NounsDescriptorV2 descriptorV2 = _deployDescriptorV2(); @@ -70,4 +124,12 @@ 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)); + NounsDescriptor descriptor = new NounsDescriptor(); + _populateDescriptor(descriptor); + + nounsToken = new NounsToken(noundersDAO, minter, descriptor, new NounsSeeder(), proxyRegistry); + } } From 467f9c6cd5e901f7fcd2fe1e96cc2df7abdc319e Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 19 Oct 2022 11:15:04 -0500 Subject: [PATCH 006/115] rename --- .../nouns-contracts/contracts/NounsAuctionHouseV2.sol | 4 ++-- packages/nouns-contracts/contracts/libs/Noracle.sol | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index a3c8fb8f23..9241b1a88a 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -287,7 +287,7 @@ contract NounsAuctionHouseV2 is // TODO emit event } - function prices(uint32 fromAuctionsAgo) external view returns (Noracle.Observation[] memory observations) { - return oracle.observe(fromAuctionsAgo); + function prices(uint32 auctionCount) external view returns (Noracle.Observation[] memory observations) { + return oracle.observe(auctionCount); } } diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index e71e482d68..b51b51669d 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -86,19 +86,19 @@ library Noracle { return self.cardinalityNext = next; } - function observe(NoracleState storage self, uint32 fromAuctionsAgo) + function observe(NoracleState storage self, uint32 auctionCount) internal view returns (Observation[] memory observations) { uint32 cardinality = self.cardinality; - require(fromAuctionsAgo <= cardinality, 'too many auctions ago'); + require(auctionCount <= cardinality, 'too many auctions ago'); uint32 index = self.index; - observations = new Observation[](fromAuctionsAgo); + observations = new Observation[](auctionCount); uint32 initializedObservationsFound = 0; uint32 checkedIndexesCount = 0; - while (initializedObservationsFound < fromAuctionsAgo && checkedIndexesCount < cardinality) { + while (initializedObservationsFound < auctionCount && checkedIndexesCount < cardinality) { uint32 checkIndex = (index + (cardinality - checkedIndexesCount)) % cardinality; checkedIndexesCount++; From 6ebb89959b6d87cc312367cb420b9d2aedc2ccef Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 19 Oct 2022 11:38:20 -0500 Subject: [PATCH 007/115] noracle: add integration tests with auction house v2 --- .../test/foundry/NounsAuctionHouseV2.t.sol | 102 ++++++++++++++++-- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 0c824c63bc..77dc768100 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -12,7 +12,6 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { address owner = address(0x1111); address noundersDAO = address(0x2222); address minter = address(0x3333); - address user = address(0x4444); NounsAuctionHouseV2 auction; @@ -34,18 +33,105 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { } function test_prices_worksWithOneAuction() public { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); - vm.deal(user, 1 ether); - vm.prank(user); - auction.createBid{ value: 1 ether }(nounId); - vm.warp(endTime); - auction.settleCurrentAndCreateNewAuction(); + address bidder = address(0x4444); + bidAndWinCurrentAuction(bidder, 1 ether); Noracle.Observation[] memory prices = auction.prices(1); assertEq(prices[0].blockTimestamp, uint32(block.timestamp)); assertEq(prices[0].nounId, 1); assertEq(prices[0].amount, 1e8); - assertEq(prices[0].winner, user); + assertEq(prices[0].winner, bidder); + } + + function test_prices_worksWithThreeAuctions() public { + vm.prank(owner); + auction.growPriceHistory(3); + + address bidder1 = address(0x4444); + address bidder2 = address(0x5555); + address bidder3 = address(0x6666); + uint256 bid1Time = bidAndWinCurrentAuction(bidder1, 1.1 ether); + uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2.2 ether); + uint256 bid3Time = bidAndWinCurrentAuction(bidder3, 3.3 ether); + + Noracle.Observation[] memory prices = auction.prices(3); + assertEq(prices.length, 3); + + assertEq(prices[0].blockTimestamp, uint32(bid3Time)); + assertEq(prices[0].nounId, 3); + assertEq(prices[0].amount, 3.3e8); + assertEq(prices[0].winner, bidder3); + + assertEq(prices[1].blockTimestamp, uint32(bid2Time)); + assertEq(prices[1].nounId, 2); + assertEq(prices[1].amount, 2.2e8); + assertEq(prices[1].winner, bidder2); + + assertEq(prices[2].blockTimestamp, uint32(bid1Time)); + assertEq(prices[2].nounId, 1); + assertEq(prices[2].amount, 1.1e8); + assertEq(prices[2].winner, bidder1); + } + + function test_prices_worksWithHigherCardinality() public { + address bidder1 = address(0x4444); + uint256 bid1Time = bidAndWinCurrentAuction(bidder1, 1 ether); + + vm.prank(owner); + auction.growPriceHistory(1_000); + + address bidder2 = address(0x5555); + uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2 ether); + + (, uint32 cardinality, ) = auction.oracle(); + assertEq(cardinality, 1_000); + + Noracle.Observation[] memory prices = auction.prices(2); + + assertEq(prices[0].blockTimestamp, uint32(bid2Time)); + assertEq(prices[0].nounId, 2); + assertEq(prices[0].amount, 2e8); + assertEq(prices[0].winner, bidder2); + + assertEq(prices[1].blockTimestamp, uint32(bid1Time)); + assertEq(prices[1].nounId, 1); + assertEq(prices[1].amount, 1e8); + assertEq(prices[1].winner, bidder1); + } + + function test_prices_dropsEarlierBidsWithLowerCardinality() public { + vm.prank(owner); + auction.growPriceHistory(2); + + address bidder1 = address(0x4444); + address bidder2 = address(0x5555); + address bidder3 = address(0x6666); + bidAndWinCurrentAuction(bidder1, 1.1 ether); + uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2.2 ether); + uint256 bid3Time = bidAndWinCurrentAuction(bidder3, 3.3 ether); + + Noracle.Observation[] memory prices = auction.prices(2); + assertEq(prices.length, 2); + + assertEq(prices[0].blockTimestamp, uint32(bid3Time)); + assertEq(prices[0].nounId, 3); + assertEq(prices[0].amount, 3.3e8); + assertEq(prices[0].winner, bidder3); + + assertEq(prices[1].blockTimestamp, uint32(bid2Time)); + assertEq(prices[1].nounId, 2); + assertEq(prices[1].amount, 2.2e8); + assertEq(prices[1].winner, bidder2); + } + + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { + (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + vm.deal(bidder, bid); + vm.prank(bidder); + auction.createBid{ value: bid }(nounId); + vm.warp(endTime); + auction.settleCurrentAndCreateNewAuction(); + return block.timestamp; } } From 36b976fdcf30bd88e60dbbd1cc2b8d3b7a3d5374 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 19 Oct 2022 11:59:33 -0500 Subject: [PATCH 008/115] noracle: add tests --- .../contracts/libs/Noracle.sol | 13 ++++++--- .../test/foundry/Noracle.t.sol | 27 +++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index b51b51669d..741ae61c5c 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -18,6 +18,10 @@ pragma solidity ^0.8.6; library Noracle { + error AlreadyInitialized(); + error NotInitialized(); + error AuctionCountOutOfBounds(uint32 auctionCount, uint32 cardinality); + struct Observation { // The block.timestamp when the auction was settled uint32 blockTimestamp; @@ -39,7 +43,7 @@ library Noracle { } function initialize(NoracleState storage self) internal { - if (self.cardinality > 0) return; + if (self.cardinality > 0) revert AlreadyInitialized(); self.cardinality = 1; self.cardinalityNext = 1; @@ -53,8 +57,10 @@ library Noracle { uint40 amount, address winner ) internal { - // if the conditions are right, we can bump the cardinality uint32 cardinalityNext = self.cardinalityNext; + if (cardinalityNext == 0) revert NotInitialized(); + + // if the conditions are right, we can bump the cardinality uint32 currentIndex = self.index; if (cardinalityNext > self.cardinality && currentIndex == (self.cardinality - 1)) { self.cardinality = cardinalityNext; @@ -73,6 +79,7 @@ library Noracle { function grow(NoracleState storage self, uint32 next) internal returns (uint32) { uint32 current = self.cardinalityNext; + if (current == 0) revert NotInitialized(); // no-op if the passed next value isn't greater than the current next value if (next <= current) return current; @@ -92,7 +99,7 @@ library Noracle { returns (Observation[] memory observations) { uint32 cardinality = self.cardinality; - require(auctionCount <= cardinality, 'too many auctions ago'); + if (auctionCount > cardinality) revert AuctionCountOutOfBounds(auctionCount, cardinality); uint32 index = self.index; observations = new Observation[](auctionCount); diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol index 3fca96dc4b..11337caf6d 100644 --- a/packages/nouns-contracts/test/foundry/Noracle.t.sol +++ b/packages/nouns-contracts/test/foundry/Noracle.t.sol @@ -9,7 +9,7 @@ contract NoracleTest is Test { Noracle.NoracleState state; - function test_cardinality1_worksWithOneWrite() public { + function test_writeObserve_cardinality1_worksWithOneWrite() public { state.initialize(); state.write(uint32(block.timestamp), 142, 69, address(0xdead)); @@ -22,7 +22,7 @@ contract NoracleTest is Test { assertEq(observations[0].winner, address(0xdead)); } - function test_cardinality1_preserves8DecimalsUnder2KETH() public { + function test_writeObserve_cardinality1_preserves8DecimalsUnder2KETH() public { state.initialize(); state.write(uint32(block.timestamp), 142, Noracle.ethPriceToUint40(1999.98765432109 ether), address(0xdead)); @@ -35,7 +35,7 @@ contract NoracleTest is Test { assertEq(observations[0].winner, address(0xdead)); } - function test_cardinality1_secondWriteOverrides() public { + function test_writeObserve_cardinality1_secondWriteOverrides() public { state.initialize(); state.write(uint32(block.timestamp), 142, 69, address(0xdead)); state.write(uint32(block.timestamp + 1), 143, 70, address(0x1234)); @@ -48,11 +48,11 @@ contract NoracleTest is Test { assertEq(observations[0].amount, 70); assertEq(observations[0].winner, address(0x1234)); - vm.expectRevert('too many auctions ago'); + vm.expectRevert(abi.encodeWithSelector(Noracle.AuctionCountOutOfBounds.selector, 2, 1)); state.observe(2); } - function test_cadinality2_secondWriteDoesNotOverride() public { + function test_writeObserve_cadinality2_secondWriteDoesNotOverride() public { state.initialize(); state.write(uint32(block.timestamp), 142, 69, address(0xdead)); state.grow(2); @@ -71,4 +71,21 @@ contract NoracleTest is Test { assertEq(observations[1].amount, 69); assertEq(observations[1].winner, address(0xdead)); } + + function test_initialize_revertsOnRepeatAttempt() public { + state.initialize(); + + vm.expectRevert(abi.encodeWithSelector(Noracle.AlreadyInitialized.selector)); + state.initialize(); + } + + function test_write_revertsWheNotInitialized() public { + vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); + state.write(uint32(block.timestamp), 142, 69, address(0xdead)); + } + + function test_grow_revertsWheNotInitialized() public { + vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); + state.grow(42); + } } From 3e1d9b6f88868dba7ac2edae0479b9ea88cc46cf Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 19 Oct 2022 12:25:42 -0500 Subject: [PATCH 009/115] noracle: trim observations array --- .../contracts/libs/Noracle.sol | 8 ++++ .../test/foundry/Noracle.t.sol | 38 ++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 741ae61c5c..8fb351658a 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -115,6 +115,14 @@ library Noracle { initializedObservationsFound++; } } + + if (auctionCount > initializedObservationsFound) { + Observation[] memory trimmedObservations = new Observation[](initializedObservationsFound); + for (uint32 i = 0; i < initializedObservationsFound; i++) { + trimmedObservations[i] = observations[i]; + } + observations = trimmedObservations; + } } function ethPriceToUint40(uint256 ethPrice) internal pure returns (uint40) { diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol index 11337caf6d..824c646dc0 100644 --- a/packages/nouns-contracts/test/foundry/Noracle.t.sol +++ b/packages/nouns-contracts/test/foundry/Noracle.t.sol @@ -9,6 +9,23 @@ contract NoracleTest is Test { Noracle.NoracleState state; + function test_initialize_revertsOnRepeatAttempt() public { + state.initialize(); + + vm.expectRevert(abi.encodeWithSelector(Noracle.AlreadyInitialized.selector)); + state.initialize(); + } + + function test_grow_revertsWheNotInitialized() public { + vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); + state.grow(42); + } + + function test_write_revertsWheNotInitialized() public { + vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); + state.write(uint32(block.timestamp), 142, 69, address(0xdead)); + } + function test_writeObserve_cardinality1_worksWithOneWrite() public { state.initialize(); state.write(uint32(block.timestamp), 142, 69, address(0xdead)); @@ -72,20 +89,23 @@ contract NoracleTest is Test { assertEq(observations[1].winner, address(0xdead)); } - function test_initialize_revertsOnRepeatAttempt() public { + function test_observe_returnsEmptyArrayGivenNoWrites() public { state.initialize(); - vm.expectRevert(abi.encodeWithSelector(Noracle.AlreadyInitialized.selector)); - state.initialize(); + Noracle.Observation[] memory observations = state.observe(1); + + assertEq(observations.length, 0); } - function test_write_revertsWheNotInitialized() public { - vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); + function test_observe_trimsObervationsArrayGivenHighCardinalityWithManyEmptySlots() public { + state.initialize(); state.write(uint32(block.timestamp), 142, 69, address(0xdead)); - } + state.grow(1_000); + state.write(uint32(block.timestamp + 1), 143, 70, address(0x1234)); - function test_grow_revertsWheNotInitialized() public { - vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); - state.grow(42); + Noracle.Observation[] memory observations = state.observe(500); + + assertEq(state.cardinality, 1_000); + assertEq(observations.length, 2); } } From 7c2d51607481f19706d239cf3666198950c7d3a7 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 19 Oct 2022 12:36:48 -0500 Subject: [PATCH 010/115] noracle: emit event when price history grows --- .../contracts/NounsAuctionHouseV2.sol | 5 +++-- .../interfaces/INounsAuctionHouse.sol | 2 ++ .../test/foundry/NounsAuctionHouseV2.t.sol | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 9241b1a88a..ee27a064ee 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -282,9 +282,10 @@ contract NounsAuctionHouseV2 is } function growPriceHistory(uint32 newCardinality) external { - oracle.grow(newCardinality); + uint32 current = oracle.cardinalityNext; + uint32 next = oracle.grow(newCardinality); - // TODO emit event + emit PriceHistoryGrown(current, next); } function prices(uint32 auctionCount) external view returns (Noracle.Observation[] memory observations) { diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 33d37c9254..6b81c62ae8 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -47,6 +47,8 @@ interface INounsAuctionHouse { event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage); + event PriceHistoryGrown(uint32 current, uint32 next); + function settleAuction() external; function settleCurrentAndCreateNewAuction() external; diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 77dc768100..5ed84252f8 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -9,6 +9,8 @@ import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; import { Noracle } from '../../contracts/libs/Noracle.sol'; contract NounsAuctionHouseV2Test is Test, DeployUtils { + event PriceHistoryGrown(uint32 current, uint32 next); + address owner = address(0x1111); address noundersDAO = address(0x2222); address minter = address(0x3333); @@ -125,6 +127,23 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { assertEq(prices[1].winner, bidder2); } + function test_growPriceHistory_emitsEvent() public { + vm.expectEmit(true, true, true, true); + emit PriceHistoryGrown(1, 3); + vm.prank(owner); + auction.growPriceHistory(3); + + vm.expectEmit(true, true, true, true); + emit PriceHistoryGrown(3, 5); + vm.prank(owner); + auction.growPriceHistory(5); + + vm.expectEmit(true, true, true, true); + emit PriceHistoryGrown(5, 5); + vm.prank(owner); + auction.growPriceHistory(5); + } + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); vm.deal(bidder, bid); From 5468d13a2ceacac4cca2fd7266a5ecfb2fd0c52c Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 24 Oct 2022 11:31:25 -0500 Subject: [PATCH 011/115] noracle: add bits to observation amount --- .../contracts/NounsAuctionHouseV2.sol | 2 +- .../contracts/libs/Noracle.sol | 21 +++++++++++-------- .../test/foundry/Noracle.t.sol | 18 +++++++++++++--- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index ee27a064ee..47f8d7b35b 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -241,7 +241,7 @@ contract NounsAuctionHouseV2 is oracle.write( uint32(block.timestamp), uint16(_auction.nounId), - Noracle.ethPriceToUint40(_auction.amount), + Noracle.ethPriceToUint48(_auction.amount), _auction.bidder ); diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 8fb351658a..673ae2f115 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -18,6 +18,8 @@ pragma solidity ^0.8.6; library Noracle { + uint32 public constant WARMUP_TIMESTAMP = 1; + error AlreadyInitialized(); error NotInitialized(); error AuctionCountOutOfBounds(uint32 auctionCount, uint32 cardinality); @@ -28,11 +30,9 @@ library Noracle { // ID for the Noun (ERC721 token ID) uint16 nounId; // The winning bid amount, with 8 decimal places (reducing accuracy to save bits) - uint40 amount; + uint48 amount; // The address of the auction winner address winner; - // whether or not the observation is initialized - bool initialized; } struct NoracleState { @@ -54,7 +54,7 @@ library Noracle { NoracleState storage self, uint32 blockTimestamp, uint16 nounId, - uint40 amount, + uint48 amount, address winner ) internal { uint32 cardinalityNext = self.cardinalityNext; @@ -69,7 +69,6 @@ library Noracle { uint32 newIndex = (currentIndex + 1) % self.cardinality; self.index = newIndex; self.observations[newIndex] = Observation({ - initialized: true, blockTimestamp: blockTimestamp, nounId: nounId, amount: amount, @@ -110,7 +109,7 @@ library Noracle { checkedIndexesCount++; Observation storage obs = self.observations[checkIndex]; - if (obs.initialized) { + if (initialized(obs)) { observations[initializedObservationsFound] = obs; initializedObservationsFound++; } @@ -125,11 +124,15 @@ library Noracle { } } - function ethPriceToUint40(uint256 ethPrice) internal pure returns (uint40) { - return uint40(ethPrice / 1e10); + function ethPriceToUint48(uint256 ethPrice) internal pure returns (uint48) { + return uint48(ethPrice / 1e10); } function warmUpObservation(Observation storage obs) private { - obs.blockTimestamp = 1; + obs.blockTimestamp = WARMUP_TIMESTAMP; + } + + function initialized(Observation storage obs) internal view returns (bool) { + return obs.blockTimestamp > WARMUP_TIMESTAMP; } } diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol index 824c646dc0..a90ebc6f11 100644 --- a/packages/nouns-contracts/test/foundry/Noracle.t.sol +++ b/packages/nouns-contracts/test/foundry/Noracle.t.sol @@ -9,6 +9,10 @@ contract NoracleTest is Test { Noracle.NoracleState state; + function setUp() public { + vm.warp(Noracle.WARMUP_TIMESTAMP + 1); + } + function test_initialize_revertsOnRepeatAttempt() public { state.initialize(); @@ -39,16 +43,24 @@ contract NoracleTest is Test { assertEq(observations[0].winner, address(0xdead)); } - function test_writeObserve_cardinality1_preserves8DecimalsUnder2KETH() public { + function test_writeObserve_cardinality1_preserves8DecimalsUnderUint48MaxValue() public { state.initialize(); - state.write(uint32(block.timestamp), 142, Noracle.ethPriceToUint40(1999.98765432109 ether), address(0xdead)); + + // amount is uint48; maxValue - 1 = 281474976710655 + // at 8 decimal points it's 2814749.76710655 + state.write( + uint32(block.timestamp), + 142, + Noracle.ethPriceToUint48(2814749.76710655999999 ether), + address(0xdead) + ); Noracle.Observation[] memory observations = state.observe(1); assertEq(observations.length, 1); assertEq(observations[0].blockTimestamp, block.timestamp); assertEq(observations[0].nounId, 142); - assertEq(observations[0].amount, 199998765432); + assertEq(observations[0].amount, 281474976710655); assertEq(observations[0].winner, address(0xdead)); } From b8557264a97216dfe1bb35a9c8705a0439fe35eb Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 24 Oct 2022 16:20:58 -0500 Subject: [PATCH 012/115] noracle: clean up `observe` --- .../contracts/libs/Noracle.sol | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 673ae2f115..16a4bf62b9 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -102,22 +102,24 @@ library Noracle { uint32 index = self.index; observations = new Observation[](auctionCount); - uint32 initializedObservationsFound = 0; - uint32 checkedIndexesCount = 0; - while (initializedObservationsFound < auctionCount && checkedIndexesCount < cardinality) { - uint32 checkIndex = (index + (cardinality - checkedIndexesCount)) % cardinality; - checkedIndexesCount++; + uint32 observationsCount = 0; + while (observationsCount < auctionCount) { + uint32 checkIndex = (index + (cardinality - observationsCount)) % cardinality; Observation storage obs = self.observations[checkIndex]; - if (initialized(obs)) { - observations[initializedObservationsFound] = obs; - initializedObservationsFound++; - } + + // when cardinality has been increased, and not used up yet, there are uninitialized items + // this loop reads from index backwards, so when it hits an uninitialized item + // it means all items from there backwards until index are uninitialized, so we should break. + if (!initialized(obs)) break; + + observations[observationsCount] = obs; + ++observationsCount; } - if (auctionCount > initializedObservationsFound) { - Observation[] memory trimmedObservations = new Observation[](initializedObservationsFound); - for (uint32 i = 0; i < initializedObservationsFound; i++) { + if (auctionCount > observationsCount) { + Observation[] memory trimmedObservations = new Observation[](observationsCount); + for (uint32 i = 0; i < observationsCount; i++) { trimmedObservations[i] = observations[i]; } observations = trimmedObservations; From 07230d585bc411d4582fc37b1437bf048d8d7dd2 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 24 Oct 2022 16:35:22 -0500 Subject: [PATCH 013/115] noracle: deploy test token with descriptor v2 trying to assess auction house gas v1 vs v2 best to use what's used on mainnet now --- packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index 2e5ed2cbff..62fa97805f 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -127,8 +127,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { function deployToken(address noundersDAO, address minter) internal returns (NounsToken nounsToken) { IProxyRegistry proxyRegistry = IProxyRegistry(address(3)); - NounsDescriptor descriptor = new NounsDescriptor(); - _populateDescriptor(descriptor); + NounsDescriptorV2 descriptor = _deployAndPopulateV2(); nounsToken = new NounsToken(noundersDAO, minter, descriptor, new NounsSeeder(), proxyRegistry); } From e8c3a39eb837408cb36f1eb661febf5e42cfe1f9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 24 Oct 2022 16:41:02 -0500 Subject: [PATCH 014/115] cleanup --- .../contracts/NounsAuctionHouseV2.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 47f8d7b35b..727caacad5 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -238,13 +238,6 @@ contract NounsAuctionHouseV2 is auction.settled = true; - oracle.write( - uint32(block.timestamp), - uint16(_auction.nounId), - Noracle.ethPriceToUint48(_auction.amount), - _auction.bidder - ); - if (_auction.bidder == address(0)) { nouns.burn(_auction.nounId); } else { @@ -255,6 +248,13 @@ contract NounsAuctionHouseV2 is _safeTransferETHWithFallback(owner(), _auction.amount); } + oracle.write( + uint32(block.timestamp), + uint16(_auction.nounId), + Noracle.ethPriceToUint48(_auction.amount), + _auction.bidder + ); + emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount); } From 0d4daaf92832a75000a49174b14d3732ab9bd4a0 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 27 Oct 2022 12:03:29 -0500 Subject: [PATCH 015/115] noracle: storage read gas optimization --- packages/nouns-contracts/contracts/libs/Noracle.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 16a4bf62b9..b683440c13 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -57,16 +57,17 @@ library Noracle { uint48 amount, address winner ) internal { + uint32 currentIndex = self.index; + uint32 cardinality = self.cardinality; uint32 cardinalityNext = self.cardinalityNext; if (cardinalityNext == 0) revert NotInitialized(); // if the conditions are right, we can bump the cardinality - uint32 currentIndex = self.index; - if (cardinalityNext > self.cardinality && currentIndex == (self.cardinality - 1)) { - self.cardinality = cardinalityNext; + if (cardinalityNext > cardinality && currentIndex == (cardinality - 1)) { + self.cardinality = cardinality = cardinalityNext; } - uint32 newIndex = (currentIndex + 1) % self.cardinality; + uint32 newIndex = (currentIndex + 1) % cardinality; self.index = newIndex; self.observations[newIndex] = Observation({ blockTimestamp: blockTimestamp, From 1a55f4a0d6d127b42232756ba396f523c4591cb9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 27 Oct 2022 12:06:07 -0500 Subject: [PATCH 016/115] noracle: nit fix --- packages/nouns-contracts/contracts/libs/Noracle.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index b683440c13..8cfb58f2b3 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -90,7 +90,8 @@ library Noracle { warmUpObservation(self.observations[i]); } - return self.cardinalityNext = next; + self.cardinalityNext = next; + return next; } function observe(NoracleState storage self, uint32 auctionCount) From d2087081add399545132885f9ed69134db318b7b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 27 Oct 2022 12:06:45 -0500 Subject: [PATCH 017/115] noracle: gas nit --- packages/nouns-contracts/contracts/libs/Noracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 8cfb58f2b3..a74764b3d7 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -86,7 +86,7 @@ library Noracle { // store in each slot to prevent fresh SSTOREs // this data will not be used because the initialized boolean is still false - for (uint32 i = current; i < next; i++) { + for (uint32 i = current; i < next; ++i) { warmUpObservation(self.observations[i]); } @@ -121,7 +121,7 @@ library Noracle { if (auctionCount > observationsCount) { Observation[] memory trimmedObservations = new Observation[](observationsCount); - for (uint32 i = 0; i < observationsCount; i++) { + for (uint32 i = 0; i < observationsCount; ++i) { trimmedObservations[i] = observations[i]; } observations = trimmedObservations; From 43c4b9b69ade701a8904e86489dc12e23710a71f Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 27 Oct 2022 12:11:11 -0500 Subject: [PATCH 018/115] noracle: optimize grow event gas --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 3 +-- packages/nouns-contracts/contracts/libs/Noracle.sol | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 727caacad5..629b40aafe 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -282,8 +282,7 @@ contract NounsAuctionHouseV2 is } function growPriceHistory(uint32 newCardinality) external { - uint32 current = oracle.cardinalityNext; - uint32 next = oracle.grow(newCardinality); + (uint32 current, uint32 next) = oracle.grow(newCardinality); emit PriceHistoryGrown(current, next); } diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index a74764b3d7..14a1504d89 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -77,12 +77,12 @@ library Noracle { }); } - function grow(NoracleState storage self, uint32 next) internal returns (uint32) { + function grow(NoracleState storage self, uint32 next) internal returns (uint32, uint32) { uint32 current = self.cardinalityNext; if (current == 0) revert NotInitialized(); // no-op if the passed next value isn't greater than the current next value - if (next <= current) return current; + if (next <= current) return (current, current); // store in each slot to prevent fresh SSTOREs // this data will not be used because the initialized boolean is still false @@ -91,7 +91,7 @@ library Noracle { } self.cardinalityNext = next; - return next; + return (current, next); } function observe(NoracleState storage self, uint32 auctionCount) From eaa90b9f888e8f3d95d65e58d04fe37bc559fab5 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 27 Oct 2022 12:24:04 -0500 Subject: [PATCH 019/115] noracle: write doesnt revert when not initialized taking a more tolerant appraoch since auction house can work fine without the oracle --- packages/nouns-contracts/contracts/libs/Noracle.sol | 2 +- packages/nouns-contracts/test/foundry/Noracle.t.sol | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 14a1504d89..d18eefeae4 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -60,7 +60,7 @@ library Noracle { uint32 currentIndex = self.index; uint32 cardinality = self.cardinality; uint32 cardinalityNext = self.cardinalityNext; - if (cardinalityNext == 0) revert NotInitialized(); + if (cardinalityNext == 0) return; // if the conditions are right, we can bump the cardinality if (cardinalityNext > cardinality && currentIndex == (cardinality - 1)) { diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol index a90ebc6f11..af9ff3e1c4 100644 --- a/packages/nouns-contracts/test/foundry/Noracle.t.sol +++ b/packages/nouns-contracts/test/foundry/Noracle.t.sol @@ -25,8 +25,7 @@ contract NoracleTest is Test { state.grow(42); } - function test_write_revertsWheNotInitialized() public { - vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); + function test_write_doesntRevertsWheNotInitialized() public { state.write(uint32(block.timestamp), 142, 69, address(0xdead)); } From d3ba861fbca0afe708e2d482d77cce5c2fcc5f33 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 27 Oct 2022 13:08:41 -0500 Subject: [PATCH 020/115] add task to deploy auction house v2 --- .../tasks/deploy-auctionhouse-v2-logic.ts | 41 +++++++++++++++++++ packages/nouns-contracts/tasks/index.ts | 1 + 2 files changed, 42 insertions(+) create mode 100644 packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts diff --git a/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts b/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts new file mode 100644 index 0000000000..aa9cf513c8 --- /dev/null +++ b/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts @@ -0,0 +1,41 @@ +import { task } from 'hardhat/config'; +import promptjs from 'prompt'; +import { + getDeploymentConfirmationWithPrompt, + getGasPriceWithPrompt, + printEstimatedCost, +} from './utils'; + +promptjs.colors = false; +promptjs.message = '> '; +promptjs.delimiter = ''; + +task('deploy-auctionhouse-v2-logic', 'Deploys NounsAuctionHouseV2').setAction( + async (_args, { ethers, run }) => { + const factory = await ethers.getContractFactory('NounsAuctionHouseV2'); + + const gasPrice = await getGasPriceWithPrompt(ethers); + await printEstimatedCost(factory, gasPrice); + + const deployConfirmed = await getDeploymentConfirmationWithPrompt(); + if (!deployConfirmed) { + console.log('Exiting'); + return; + } + + console.log('Deploying...'); + const contract = await factory.deploy({ gasPrice }); + await contract.deployed(); + console.log(`Transaction hash: ${contract.deployTransaction.hash} \n`); + console.log(`NounsAuctionHouseV2 deployed to ${contract.address}`); + + await new Promise(f => setTimeout(f, 60000)); + + console.log('Verifying on Etherscan...'); + await run('verify:verify', { + address: contract.address, + constructorArguments: [], + }); + console.log('Verified'); + }, +); diff --git a/packages/nouns-contracts/tasks/index.ts b/packages/nouns-contracts/tasks/index.ts index 24fd1b3003..3983ee774b 100644 --- a/packages/nouns-contracts/tasks/index.ts +++ b/packages/nouns-contracts/tasks/index.ts @@ -23,3 +23,4 @@ export * from './verify-etherscan-daov2'; export * from './update-configs-daov2'; export * from './deploy-short-times-daov1'; export * from './deploy-and-configure-short-times-daov1'; +export * from './deploy-auctionhouse-v2-logic'; From db9d1592d2c1a69f361f2ee4f9d8db81443cddd9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 28 Oct 2022 11:57:20 -0500 Subject: [PATCH 021/115] noracle: add natspec --- .../contracts/NounsAuctionHouseV2.sol | 25 +++++++ .../contracts/libs/Noracle.sol | 75 ++++++++++++++++++- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 629b40aafe..4836d6b4b0 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -277,16 +277,41 @@ contract NounsAuctionHouseV2 is return success; } + /** + * @notice Initialize the price oracle, setting its cardinality to one. + * Cardinality is the the number past prices to store. For the oracle to store more than one price + * call `growPriceHistory` with the new number of auctions the oracle should store. + * @dev This function can only be called by `owner`. + */ function initializeOracle() external onlyOwner { oracle.initialize(); } + /** + * @notice Increase the price oracle cardinality, i.e. how many past auction prices it stores. + * The caller of this function chooses to spend gas to warm up the new storage slots, so that + * writing price history upon auction settlement will cost minimal gas. + * In other words, the higher the value of `newCardinality`, the more gas calling this function costs. + * @param newCardinality the new total number of price history slots the oracle can store. + */ function growPriceHistory(uint32 newCardinality) external { (uint32 current, uint32 next) = oracle.grow(newCardinality); emit PriceHistoryGrown(current, next); } + /** + * @notice Get past auction prices, up to `oracle.cardinality` observations. + * There are times when cardinality is increased and not yet fully used, when a user might request more + * observations than what's stored; in such cases users will get the maximum number of observations the + * oracle has to offer. + * For example, say cardinality was just increased from 3 to 10, a user can then ask for 10 observations. + * Since the oracle only has 3 prices stored, the user will get 3 observations. + * @dev Reverts with a `AuctionCountOutOfBounds` error if `auctionCount` is greater than `oracle.cardinality`. + * @param auctionCount The number of price observations to get. + * @return observations An array of type `Noracle.Observation`, where each Observation includes a timestamp, + * the Noun ID of that auction, the winning bid amount, and the winner's addreess. + */ function prices(uint32 auctionCount) external view returns (Noracle.Observation[] memory observations) { return oracle.observe(auctionCount); } diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index d18eefeae4..ec416d8add 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -17,31 +17,58 @@ pragma solidity ^0.8.6; +/** + * @dev A Nouns price history oracle. Inspired by Uniswap's Oracle.sol: + * https://github.com/Uniswap/v3-core/blob/fc2107bd5709cdee6742d5164c1eb998566bcb75/contracts/libraries/Oracle.sol + * + * It uses warmed up storage slots only, to minimize the cost on users when settling auctions and storing prices. + * The number of slots is stored in the `cardinality` state var, and can be increased by users who choose to pay the gas. + * + * `Observation` variable sizes have been packed to fit all variables in a single storage slot, to further optimize + * gas on both `grow` and `write` functions. + */ library Noracle { + /// @notice The value assigned to an Observation's `blockTimestamp` to warm up the storage slot. uint32 public constant WARMUP_TIMESTAMP = 1; error AlreadyInitialized(); error NotInitialized(); error AuctionCountOutOfBounds(uint32 auctionCount, uint32 cardinality); + /** + * @dev Struct members size has been adjusted so the sum of bits is 256, to fit into a single storage slot. + * The maximum `amount` supported is 2814749.76710655 ETH. + */ struct Observation { - // The block.timestamp when the auction was settled + // The block.timestamp when the auction was settled. uint32 blockTimestamp; - // ID for the Noun (ERC721 token ID) + // ID for the Noun (ERC721 token ID). uint16 nounId; - // The winning bid amount, with 8 decimal places (reducing accuracy to save bits) + // The winning bid amount, with 8 decimal places (reducing accuracy to save bits). uint48 amount; - // The address of the auction winner + // The address of the auction winner. address winner; } + /** + * @dev Contains the full oracle state, the primary `self` used in this library, as a UX optimization for + * contracts using this library, so they don't have to keep track of any state variables (as opposed to Uniswap's + * oracle where the Pool contract holds these state variables). + */ struct NoracleState { + // Price history, stored as a mapping, and not an array, due to Solidity limitations. mapping(uint32 => Observation) observations; + // The latest price index. uint32 index; + // The maximum number of observation the oracle can store. uint32 cardinality; + // The next value to assign to cardinality once the oracle needs more slots, also the current number of warm storage slots. uint32 cardinalityNext; } + /** + * @dev Initialize the oracle with a cardinality of one. + */ function initialize(NoracleState storage self) internal { if (self.cardinality > 0) revert AlreadyInitialized(); @@ -50,6 +77,14 @@ library Noracle { warmUpObservation(self.observations[0]); } + /** + * @dev Write a new auction price to oracle storage. + * @param self The oracle state. + * @param blockTimestamp The auction's settlement block timestamp, reduced to uint32. + * @param nounId The auction's Noun ID. + * @param amount The auction's winning bid, reduced to a uint48 with 8 decimal places. Max supported value is 2814749.76710655 ETH. + * @param winner The auction winner's address. + */ function write( NoracleState storage self, uint32 blockTimestamp, @@ -77,6 +112,14 @@ library Noracle { }); } + /** + * @dev Grow the oracle's next cardinality, warming up the relevant storage slots. + * Does nothing if `next` isn't greater than the current `cardinalityNext` value. + * @param self The oracle state. + * @param next The new `cardinalityNext` value. + * @return uint32 The previous `cardinalityNext` value. + * @return uint32 The new `cardinalityNext` value. + */ function grow(NoracleState storage self, uint32 next) internal returns (uint32, uint32) { uint32 current = self.cardinalityNext; if (current == 0) revert NotInitialized(); @@ -94,6 +137,19 @@ library Noracle { return (current, next); } + /** + * @dev Get past auction prices, up to `oracle.cardinality` observations. + * There are times when cardinality is increased and not yet fully used, when a user might request more + * observations than what's stored; in such cases users will get the maximum number of observations the + * oracle has to offer. + * For example, say cardinality was just increased from 3 to 10, a user can then ask for 10 observations. + * Since the oracle only has 3 prices stored, the user will get 3 observations. + * Reverts with a `AuctionCountOutOfBounds` error if `auctionCount` is greater than `oracle.cardinality`. + * @param self The oracle state. + * @param auctionCount The number of price observations to get. + * @return observations An array of type `Noracle.Observation`, where each Observation includes a timestamp, + * the Noun ID of that auction, the winning bid amount, and the winner's addreess. + */ function observe(NoracleState storage self, uint32 auctionCount) internal view @@ -128,14 +184,25 @@ library Noracle { } } + /** + * @dev Convert an ETH price of 256 bits with 18 decimals, to 48 bits with 8 decimals. + * Max supported value is 2814749.76710655 ETH. + */ function ethPriceToUint48(uint256 ethPrice) internal pure returns (uint48) { return uint48(ethPrice / 1e10); } + /** + * @dev Write a stub value to warm up the observation storage slot. + */ function warmUpObservation(Observation storage obs) private { obs.blockTimestamp = WARMUP_TIMESTAMP; } + /** + * @dev Check if an observation has been initialized, by checking if it has a + * non-stub `blockTimestamp` value. + */ function initialized(Observation storage obs) internal view returns (bool) { return obs.blockTimestamp > WARMUP_TIMESTAMP; } From 0ee50ec8e0bc6a5970827ada2704d5f3b86d0e5d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 10:10:41 -0500 Subject: [PATCH 022/115] remove unnecessary nonReentrant modifiers settleAuction is protected from re-entry by requiring `settled` to be false and setting it to true first thing after the require statements --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 4836d6b4b0..8347a2173d 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -95,7 +95,7 @@ contract NounsAuctionHouseV2 is /** * @notice Settle the current auction, mint a new Noun, and put it up for auction. */ - function settleCurrentAndCreateNewAuction() external override nonReentrant whenNotPaused { + function settleCurrentAndCreateNewAuction() external override whenNotPaused { _settleAuction(); _createAuction(); } @@ -104,7 +104,7 @@ contract NounsAuctionHouseV2 is * @notice Settle the current auction. * @dev This function can only be called when the contract is paused. */ - function settleAuction() external override whenPaused nonReentrant { + function settleAuction() external override whenPaused { _settleAuction(); } From 25ef1471b2e64c61f2cafc9732f46d655f26dd14 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 10:54:15 -0500 Subject: [PATCH 023/115] move lastBidder refund down the bid function should make it clearer that this function can't be abused via reentry msg.value is required to be higher than the previous amount + min increment by setting `auction.amount` right after that requirement, it's clear the next reentry will have to provide a higher amount and so on --- .../contracts/NounsAuctionHouseV2.sol | 14 +++++++------- .../test/foundry/NounsAuctionHouseV2.t.sol | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 8347a2173d..4816eb2020 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -123,13 +123,6 @@ contract NounsAuctionHouseV2 is 'Must send more than last bid by minBidIncrementPercentage amount' ); - address payable lastBidder = _auction.bidder; - - // Refund the last bidder, if applicable - if (lastBidder != address(0)) { - _safeTransferETHWithFallback(lastBidder, _auction.amount); - } - auction.amount = msg.value; auction.bidder = payable(msg.sender); @@ -144,6 +137,13 @@ contract NounsAuctionHouseV2 is if (extended) { 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); + } } /** diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 5ed84252f8..b2ff5cc138 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -144,6 +144,25 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { auction.growPriceHistory(5); } + function test_createBid_refundsPreviousBidder() public { + (uint256 nounId, , , , , ) = auction.auction(); + 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 bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); vm.deal(bidder, bid); From c09992d662008c1ed7a7aab8c76328f93b88bbef Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 10:55:00 -0500 Subject: [PATCH 024/115] remove unnecessary nonReentrant modifier from createBid --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 4816eb2020..09b322a613 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -112,7 +112,7 @@ contract NounsAuctionHouseV2 is * @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 nonReentrant { + function createBid(uint256 nounId) external payable override { INounsAuctionHouse.Auction memory _auction = auction; require(_auction.nounId == nounId, 'Noun not up for auction'); From 7485280e6422a7474381dbe5b78e48316b49a95d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 12:01:10 -0500 Subject: [PATCH 025/115] auction house: fix gas griefing per this issue found in the latest C4 audit: https://github.com/code-423n4/2022-08-nounsdao-findings/issues/382 --- .../contracts/NounsAuctionHouseV2.sol | 5 +++- .../test/foundry/NounsAuctionHouseV2.t.sol | 26 +++++++++++++++++++ .../foundry/helpers/BidderWithGasGriefing.sol | 16 ++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 packages/nouns-contracts/test/foundry/helpers/BidderWithGasGriefing.sol diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 09b322a613..8d3a1b3065 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -273,7 +273,10 @@ contract NounsAuctionHouseV2 is * @dev This function only forwards 30,000 gas to the callee. */ function _safeTransferETH(address to, uint256 value) internal returns (bool) { - (bool success, ) = to.call{ value: value, gas: 30_000 }(new bytes(0)); + bool success; + assembly { + success := call(30000, to, value, 0, 0, 0, 0) + } return success; } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index b2ff5cc138..947eea1d49 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -7,6 +7,7 @@ import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHous import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; import { Noracle } from '../../contracts/libs/Noracle.sol'; +import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; contract NounsAuctionHouseV2Test is Test, DeployUtils { event PriceHistoryGrown(uint32 current, uint32 next); @@ -163,6 +164,31 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { assertEq(bidder2.balance, 0); } + function test_createBid_preventsGasGriefingUponRefunding() public { + BidderWithGasGriefing badBidder = new BidderWithGasGriefing(); + (uint256 nounId, , , , , ) = auction.auction(); + + 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 bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); vm.deal(bidder, bid); 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) + } + } +} From 840616af037783157da7b9aaf167471cd5975689 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 14:28:59 -0500 Subject: [PATCH 026/115] auctionhousev2: optimize auction struct bits created AuctionV2 that fits into 2 storage slots instead of 5 previously added a migration contract between V1 and V2 that rewrites auction data for V2 --- .../NounsAuctionHousePreV2Migration.sol | 50 +++++++++++++++++++ .../contracts/NounsAuctionHouseV2.sol | 18 +++---- .../interfaces/INounsAuctionHouse.sol | 15 ++++++ .../test/foundry/NounsAuctionHouseV2.t.sol | 44 +++++++++++++++- .../test/foundry/helpers/DeployUtils.sol | 15 ++++-- 5 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol diff --git a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol new file mode 100644 index 0000000000..6997ab29d8 --- /dev/null +++ b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title An interim contract for storage migration between V1 and V2 + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.6; + +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 'forge-std/console.sol'; + +contract NounsAuctionHousePreV2Migration is PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable { + // See NounsAuctionHouse for docs on these state vars + address public nouns; + address public weth; + uint256 public timeBuffer; + uint256 public reservePrice; + uint8 public minBidIncrementPercentage; + uint256 public duration; + INounsAuctionHouse.Auction public auction; + + function migrate() public onlyOwner { + INounsAuctionHouse.Auction memory _auction = auction; + INounsAuctionHouse.AuctionV2 storage auctionV2; + assembly { + auctionV2.slot := auction.slot + } + + auctionV2.nounId = uint128(_auction.nounId); + auctionV2.amount = uint128(_auction.amount); + auctionV2.startTime = uint40(_auction.startTime); + auctionV2.endTime = uint40(_auction.endTime); + auctionV2.bidder = _auction.bidder; + auctionV2.settled = _auction.settled; + } +} diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 8d3a1b3065..37fef06e12 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -60,7 +60,7 @@ contract NounsAuctionHouseV2 is uint256 public duration; // The active auction - INounsAuctionHouse.Auction public auction; + INounsAuctionHouse.AuctionV2 public auction; // The Nouns price feed state Noracle.NoracleState public oracle; @@ -113,7 +113,7 @@ contract NounsAuctionHouseV2 is * @dev This contract only accepts payment in ETH. */ function createBid(uint256 nounId) external payable override { - INounsAuctionHouse.Auction memory _auction = auction; + INounsAuctionHouse.AuctionV2 memory _auction = auction; require(_auction.nounId == nounId, 'Noun not up for auction'); require(block.timestamp < _auction.endTime, 'Auction expired'); @@ -123,13 +123,13 @@ contract NounsAuctionHouseV2 is 'Must send more than last bid by minBidIncrementPercentage amount' ); - auction.amount = msg.value; + auction.amount = uint128(msg.value); auction.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; if (extended) { - auction.endTime = _auction.endTime = block.timestamp + timeBuffer; + auction.endTime = _auction.endTime = uint40(block.timestamp + timeBuffer); } emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); @@ -207,11 +207,11 @@ contract NounsAuctionHouseV2 is */ function _createAuction() internal { try nouns.mint() returns (uint256 nounId) { - uint256 startTime = block.timestamp; - uint256 endTime = startTime + duration; + uint40 startTime = uint40(block.timestamp); + uint40 endTime = startTime + uint40(duration); - auction = Auction({ - nounId: nounId, + auction = AuctionV2({ + nounId: uint128(nounId), amount: 0, startTime: startTime, endTime: endTime, @@ -230,7 +230,7 @@ contract NounsAuctionHouseV2 is * @dev If there are no bids, the Noun is burned. */ function _settleAuction() internal { - INounsAuctionHouse.Auction memory _auction = auction; + INounsAuctionHouse.AuctionV2 memory _auction = auction; require(_auction.startTime != 0, "Auction hasn't begun"); require(!_auction.settled, 'Auction has already been settled'); diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 6b81c62ae8..d3043d320a 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -33,6 +33,21 @@ interface INounsAuctionHouse { bool settled; } + 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; + } + event AuctionCreated(uint256 indexed nounId, uint256 startTime, uint256 endTime); event AuctionBid(uint256 indexed nounId, address sender, uint256 value, bool extended); diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 947eea1d49..10012ec01f 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -5,7 +5,10 @@ import 'forge-std/Test.sol'; import { DeployUtils } from './helpers/DeployUtils.sol'; import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import { NounsAuctionHouse } from '../../contracts/NounsAuctionHouse.sol'; +import { INounsAuctionHouse } from '../../contracts/interfaces/INounsAuctionHouse.sol'; import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; +import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHousePreV2Migration.sol'; import { Noracle } from '../../contracts/libs/Noracle.sol'; import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; @@ -25,8 +28,7 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { minter ); - NounsAuctionHouseV2 auctionV2 = new NounsAuctionHouseV2(); - _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy, address(auctionV2)); + _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); auction = NounsAuctionHouseV2(address(auctionProxy)); @@ -189,6 +191,44 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { assertLt(gasDiffWithGriefing - gasDiffNoGriefing, 10_000); } + 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); + + _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); + ( + uint128 nounIdV2, + uint128 amountV2, + uint40 startTimeV2, + uint40 endTimeV2, + address payable bidderV2, + bool settledV2 + ) = auctionV2.auction(); + + assertEq(nounIdV2, nounId); + assertEq(amountV2, amount); + assertEq(startTimeV2, startTime); + assertEq(endTimeV2, endTime); + assertEq(bidderV2, bidder); + assertEq(settledV2, false); + } + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); vm.deal(bidder, bid); diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index 56cfd7237a..fd8af65427 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -18,6 +18,7 @@ import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionH import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; import { NounsAuctionHouse } from '../../../contracts/NounsAuctionHouse.sol'; import { NounsAuctionHouseV2 } from '../../../contracts/NounsAuctionHouseV2.sol'; +import { NounsAuctionHousePreV2Migration } from '../../../contracts/NounsAuctionHousePreV2Migration.sol'; import { WETH } from '../../../contracts/test/WETH.sol'; abstract contract DeployUtils is Test, DescriptorHelpers { @@ -63,13 +64,21 @@ abstract contract DeployUtils is Test, DescriptorHelpers { function _upgradeAuctionHouse( address owner, NounsAuctionHouseProxyAdmin proxyAdmin, - NounsAuctionHouseProxy proxy, - address newLogic + NounsAuctionHouseProxy proxy ) internal { + NounsAuctionHouseV2 newLogic = new NounsAuctionHouseV2(); + NounsAuctionHousePreV2Migration migratorLogic = new NounsAuctionHousePreV2Migration(); + vm.startPrank(owner); - proxyAdmin.upgrade(proxy, newLogic); + // 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)); NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(proxy)); auctionV2.initializeOracle(); From 81aa3dae6cc8a7398c27c92b5059c557437b8cd7 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 14:30:49 -0500 Subject: [PATCH 027/115] cleanup --- .../contracts/NounsAuctionHousePreV2Migration.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol index 6997ab29d8..eb31edf822 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol @@ -21,7 +21,6 @@ import { PausableUpgradeable } from '@openzeppelin/contracts-upgradeable/securit 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 'forge-std/console.sol'; contract NounsAuctionHousePreV2Migration is PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable { // See NounsAuctionHouse for docs on these state vars From 2396eab2a560c013015fccbc9eac53c0a2ce78e6 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 15:03:15 -0500 Subject: [PATCH 028/115] convert require strings to custom errors a bit more gas savings --- .../contracts/NounsAuctionHouseV2.sol | 18 +- .../interfaces/INounsAuctionHouse.sol | 14 + .../test/foundry/NounsAuctionHouseV2.t.sol | 256 +++++++++++------- 3 files changed, 186 insertions(+), 102 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 37fef06e12..4cf0a0d507 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -115,13 +115,11 @@ contract NounsAuctionHouseV2 is function createBid(uint256 nounId) external payable override { INounsAuctionHouse.AuctionV2 memory _auction = auction; - 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' - ); + if (_auction.nounId != nounId) revert NounNotUpForAuction(); + if (block.timestamp >= _auction.endTime) revert AuctionExpired(); + if (msg.value < reservePrice) revert MustSendAtLeastReservePrice(); + if (msg.value < _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100)) + revert BidDifferenceMustBeGreaterThanMinBidIncrement(); auction.amount = uint128(msg.value); auction.bidder = payable(msg.sender); @@ -232,9 +230,9 @@ contract NounsAuctionHouseV2 is function _settleAuction() internal { INounsAuctionHouse.AuctionV2 memory _auction = auction; - 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"); + if (_auction.startTime == 0) revert AuctionHasntBegun(); + if (_auction.settled) revert AuctionAlreadySettled(); + if (block.timestamp < _auction.endTime) revert AuctionNotDone(); auction.settled = true; diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index d3043d320a..216269f082 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -64,6 +64,20 @@ interface INounsAuctionHouse { event PriceHistoryGrown(uint32 current, uint32 next); + error NounNotUpForAuction(); + + error AuctionExpired(); + + error AuctionHasntBegun(); + + error AuctionAlreadySettled(); + + error AuctionNotDone(); + + error MustSendAtLeastReservePrice(); + + error BidDifferenceMustBeGreaterThanMinBidIncrement(); + function settleAuction() external; function settleCurrentAndCreateNewAuction() external; diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 10012ec01f..c613a71478 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -37,6 +37,170 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { vm.roll(block.number + 1); } + function test_createBid_revertsGivenWrongNounId() public { + (uint128 nounId, , , , , ) = auction.auction(); + + vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.NounNotUpForAuction.selector)); + auction.createBid(nounId - 1); + + vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.NounNotUpForAuction.selector)); + auction.createBid(nounId + 1); + } + + function test_createBid_revertsPastEndTime() public { + (uint128 nounId, , , uint40 endTime, , ) = auction.auction(); + vm.warp(endTime + 1); + + vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.AuctionExpired.selector)); + auction.createBid(nounId); + } + + function test_createBid_revertsGivenBidBelowReservePrice() public { + vm.prank(owner); + auction.setReservePrice(1 ether); + + (uint128 nounId, , , , , ) = auction.auction(); + + vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.MustSendAtLeastReservePrice.selector)); + auction.createBid{ value: 0.9 ether }(nounId); + } + + function test_createBid_revertsGivenBidLowerThanMinIncrement() public { + vm.prank(owner); + auction.setMinBidIncrementPercentage(50); + (uint128 nounId, , , , , ) = auction.auction(); + auction.createBid{ value: 1 ether }(nounId); + + vm.expectRevert( + abi.encodeWithSelector(INounsAuctionHouse.BidDifferenceMustBeGreaterThanMinBidIncrement.selector) + ); + auction.createBid{ value: 1.49 ether }(nounId); + } + + function test_createBid_refundsPreviousBidder() public { + (uint256 nounId, , , , , ) = auction.auction(); + 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(); + + 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(abi.encodeWithSelector(INounsAuctionHouse.AuctionNotDone.selector)); + auction.settleCurrentAndCreateNewAuction(); + } + + function test_settleAuction_revertsWhenSettled() public { + (, , , uint40 endTime, , ) = auction.auction(); + vm.warp(endTime + 1); + + vm.prank(owner); + auction.pause(); + auction.settleAuction(); + + vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.AuctionAlreadySettled.selector)); + auction.settleAuction(); + } + + function test_settleAuction_revertsWhenAuctionHasntBegunYet() public { + (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + owner, + noundersDAO, + minter + ); + _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + auction = NounsAuctionHouseV2(address(auctionProxy)); + + vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.AuctionHasntBegun.selector)); + auction.settleAuction(); + } + + 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); + + _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); + ( + uint128 nounIdV2, + uint128 amountV2, + uint40 startTimeV2, + uint40 endTimeV2, + address payable bidderV2, + bool settledV2 + ) = auctionV2.auction(); + + assertEq(nounIdV2, nounId); + assertEq(amountV2, amount); + assertEq(startTimeV2, startTime); + assertEq(endTimeV2, endTime); + assertEq(bidderV2, bidder); + assertEq(settledV2, false); + } + + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { + (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + vm.deal(bidder, bid); + vm.prank(bidder); + auction.createBid{ value: bid }(nounId); + vm.warp(endTime); + auction.settleCurrentAndCreateNewAuction(); + return block.timestamp; + } +} + +contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2Test { function test_prices_worksWithOneAuction() public { address bidder = address(0x4444); bidAndWinCurrentAuction(bidder, 1 ether); @@ -146,96 +310,4 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { vm.prank(owner); auction.growPriceHistory(5); } - - function test_createBid_refundsPreviousBidder() public { - (uint256 nounId, , , , , ) = auction.auction(); - 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(); - - 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_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); - - _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); - - NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); - ( - uint128 nounIdV2, - uint128 amountV2, - uint40 startTimeV2, - uint40 endTimeV2, - address payable bidderV2, - bool settledV2 - ) = auctionV2.auction(); - - assertEq(nounIdV2, nounId); - assertEq(amountV2, amount); - assertEq(startTimeV2, startTime); - assertEq(endTimeV2, endTime); - assertEq(bidderV2, bidder); - assertEq(settledV2, false); - } - - function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); - vm.deal(bidder, bid); - vm.prank(bidder); - auction.createBid{ value: bid }(nounId); - vm.warp(endTime); - auction.settleCurrentAndCreateNewAuction(); - return block.timestamp; - } } From c608be639af14a8a4c8d0bd66f8b07d86603e60c Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 31 Oct 2022 18:24:13 -0500 Subject: [PATCH 029/115] cleanup --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 4cf0a0d507..7c77551234 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -126,13 +126,11 @@ contract NounsAuctionHouseV2 is // Extend the auction if the bid was received within `timeBuffer` of the auction end time bool extended = _auction.endTime - block.timestamp < timeBuffer; - if (extended) { - auction.endTime = _auction.endTime = uint40(block.timestamp + timeBuffer); - } emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); if (extended) { + auction.endTime = _auction.endTime = uint40(block.timestamp + timeBuffer); emit AuctionExtended(_auction.nounId, _auction.endTime); } From f7a514689f97bcfaa98cd211ccb81d9aec763fb6 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 1 Nov 2022 10:28:47 -0500 Subject: [PATCH 030/115] zero out auction V1 state vars per code review: to prevent accidental bugs if adding new storage variables in the future --- .../contracts/NounsAuctionHousePreV2Migration.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol index eb31edf822..bd1a4aff2d 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol @@ -34,6 +34,9 @@ contract NounsAuctionHousePreV2Migration is PausableUpgradeable, ReentrancyGuard function migrate() public onlyOwner { INounsAuctionHouse.Auction memory _auction = auction; + + auction = INounsAuctionHouse.Auction(0, 0, 0, 0, payable(address(0)), false); + INounsAuctionHouse.AuctionV2 storage auctionV2; assembly { auctionV2.slot := auction.slot From 890292652fb6516f4fa84cb3588390f146d9a4ee Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 2 Nov 2022 13:42:55 -0500 Subject: [PATCH 031/115] noracle: trim observations more efficiently thanks to soli's feedback --- packages/nouns-contracts/contracts/libs/Noracle.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index ec416d8add..a528ecd9b0 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -176,11 +176,9 @@ library Noracle { } if (auctionCount > observationsCount) { - Observation[] memory trimmedObservations = new Observation[](observationsCount); - for (uint32 i = 0; i < observationsCount; ++i) { - trimmedObservations[i] = observations[i]; + assembly { + mstore(observations, observationsCount) } - observations = trimmedObservations; } } From d1da1bb481a9543ad7d4098f49c3e38873fe340c Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 2 Nov 2022 13:43:19 -0500 Subject: [PATCH 032/115] noracle: minor gas optimization --- packages/nouns-contracts/contracts/libs/Noracle.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index a528ecd9b0..b2392ac7c7 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -155,12 +155,11 @@ library Noracle { view returns (Observation[] memory observations) { + uint32 index = self.index; uint32 cardinality = self.cardinality; if (auctionCount > cardinality) revert AuctionCountOutOfBounds(auctionCount, cardinality); - uint32 index = self.index; observations = new Observation[](auctionCount); - uint32 observationsCount = 0; while (observationsCount < auctionCount) { uint32 checkIndex = (index + (cardinality - observationsCount)) % cardinality; From 644aced220410cb94da8c96778b25e50b04a6d87 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 2 Nov 2022 13:48:08 -0500 Subject: [PATCH 033/115] add inline comment --- packages/nouns-contracts/contracts/libs/Noracle.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index b2392ac7c7..f2500bb5a1 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -175,6 +175,7 @@ library Noracle { } if (auctionCount > observationsCount) { + // this assembly trims the observations array, getting rid of unused cells assembly { mstore(observations, observationsCount) } From 06045cc3117c25479c87b7ebc94b8407495064e6 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 7 Nov 2022 14:19:46 -0500 Subject: [PATCH 034/115] update auction house v2 deploy to include the state migration contract --- .../tasks/deploy-auctionhouse-v2-logic.ts | 45 ++++++++++--------- packages/nouns-contracts/tasks/utils/index.ts | 4 +- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts b/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts index aa9cf513c8..c45f7747ae 100644 --- a/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts +++ b/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts @@ -12,30 +12,35 @@ promptjs.delimiter = ''; task('deploy-auctionhouse-v2-logic', 'Deploys NounsAuctionHouseV2').setAction( async (_args, { ethers, run }) => { - const factory = await ethers.getContractFactory('NounsAuctionHouseV2'); - const gasPrice = await getGasPriceWithPrompt(ethers); - await printEstimatedCost(factory, gasPrice); + const contractNames = ['NounsAuctionHousePreV2Migration', 'NounsAuctionHouseV2']; - const deployConfirmed = await getDeploymentConfirmationWithPrompt(); - if (!deployConfirmed) { - console.log('Exiting'); - return; - } + for (const contractName of contractNames) { + const factory = await ethers.getContractFactory(contractName); + + console.log(`Estimating deployment cost for ${contractName}...`); + await printEstimatedCost(factory, gasPrice); - console.log('Deploying...'); - const contract = await factory.deploy({ gasPrice }); - await contract.deployed(); - console.log(`Transaction hash: ${contract.deployTransaction.hash} \n`); - console.log(`NounsAuctionHouseV2 deployed to ${contract.address}`); + const deployConfirmed = await getDeploymentConfirmationWithPrompt(); + if (!deployConfirmed) { + console.log('Exiting'); + return; + } - await new Promise(f => setTimeout(f, 60000)); + console.log('Deploying...'); + const contract = await factory.deploy({ gasPrice }); + await contract.deployed(); + console.log(`Transaction hash: ${contract.deployTransaction.hash} \n`); + console.log(`${contractName} deployed to ${contract.address}`); - console.log('Verifying on Etherscan...'); - await run('verify:verify', { - address: contract.address, - constructorArguments: [], - }); - console.log('Verified'); + await new Promise(f => setTimeout(f, 60000)); + + console.log('Verifying on Etherscan...'); + await run('verify:verify', { + address: contract.address, + constructorArguments: [], + }); + console.log('Verified'); + } }, ); 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[]): { From 5f10c4780e0698d00f1c53ac22e66dea5ee31862 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 7 Nov 2022 16:13:08 -0500 Subject: [PATCH 035/115] add test for v1 getter compatibility --- .../test/foundry/NounsAuctionHouseV2.t.sol | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index c613a71478..5eec38e476 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -189,6 +189,40 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { assertEq(settledV2, false); } + 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)); + + ( + uint128 nounIdV2, + uint128 amountV2, + uint40 startTimeV2, + uint40 endTimeV2, + address payable bidderV2, + bool settledV2 + ) = auction.auction(); + + ( + uint256 nounIdV1, + uint256 amountV1, + uint256 startTimeV1, + uint256 endTimeV1, + address payable bidderV1, + bool settledV1 + ) = auctionV1.auction(); + + assertEq(nounIdV2, nounIdV1); + assertEq(amountV2, amountV1); + assertEq(startTimeV2, startTimeV1); + assertEq(endTimeV2, endTimeV1); + assertEq(bidderV2, bidderV1); + assertEq(settledV2, settledV1); + } + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); vm.deal(bidder, bid); From 9e53560777a05366fe6bee0ad118ea5f0f1028e8 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 25 Nov 2022 13:42:21 -0500 Subject: [PATCH 036/115] update higher cardinality test --- .../nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 5eec38e476..9f69e2dd2d 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -290,7 +290,9 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2Test { (, uint32 cardinality, ) = auction.oracle(); assertEq(cardinality, 1_000); - Noracle.Observation[] memory prices = auction.prices(2); + Noracle.Observation[] memory prices = auction.prices(1_000); + + assertEq(prices.length, 2); assertEq(prices[0].blockTimestamp, uint32(bid2Time)); assertEq(prices[0].nounId, 2); From 1c94df1358a3baabed2d7aeee8347777f9016ee9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 25 Nov 2022 13:49:29 -0500 Subject: [PATCH 037/115] noracle: natspec update --- packages/nouns-contracts/contracts/libs/Noracle.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index f2500bb5a1..af047fca82 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -37,6 +37,7 @@ library Noracle { /** * @dev Struct members size has been adjusted so the sum of bits is 256, to fit into a single storage slot. + * If updated to use more than 256 bits, consider updating `warmUpObservation` to warm up the additional storage space. * The maximum `amount` supported is 2814749.76710655 ETH. */ struct Observation { From 647a3feb17427a7a8266dc1ad25178699c57932a Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 28 Nov 2022 17:28:12 -0500 Subject: [PATCH 038/115] add tests and inline comments --- .../contracts/libs/Noracle.sol | 8 ++++- .../test/foundry/NounsAuctionHouseV2.t.sol | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index af047fca82..5853429b14 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -64,6 +64,10 @@ library Noracle { // The maximum number of observation the oracle can store. uint32 cardinality; // The next value to assign to cardinality once the oracle needs more slots, also the current number of warm storage slots. + // Important to the proper function of `observe`; if `cardinality` was incremented directly in `grow`, `observe` would not + // work as expected when looping back from index zero to the end of stored values is necessary + // e.g. 10 written values, index at zero, asking to `observe` 2+ prices, would loop back to the new cardinality - 1, hit an + // uninitialized observation and exit early. uint32 cardinalityNext; } @@ -98,11 +102,13 @@ library Noracle { uint32 cardinalityNext = self.cardinalityNext; if (cardinalityNext == 0) return; - // if the conditions are right, we can bump the cardinality + // if the conditions are right, we can bump the cardinality if (cardinalityNext > cardinality && currentIndex == (cardinality - 1)) { self.cardinality = cardinality = cardinalityNext; } + // if `grow` is executed before the first write, cardinality is greater than 1 + // and the first write will be into index 1. uint32 newIndex = (currentIndex + 1) % cardinality; self.index = newIndex; self.observations[newIndex] = Observation({ diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 9f69e2dd2d..deff731747 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -330,6 +330,39 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2Test { assertEq(prices[1].winner, bidder2); } + function test_prices_worksWhenLoopingBackFromIndexZeroAfterGrow() public { + vm.prank(owner); + + // because grow happens before any writes, the first written index is 1 + // so we need 4 writes to bring index to zero, such that getting two prices + // leads to a loop back to index = cardinality - 1 + auction.growPriceHistory(2); + + address bidder1 = address(0x4444); + address bidder2 = address(0x5555); + address bidder3 = address(0x6666); + address bidder4 = address(0x6666); + bidAndWinCurrentAuction(bidder1, 1.1 ether); + uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2.2 ether); + uint256 bid3Time = bidAndWinCurrentAuction(bidder3, 3.3 ether); + uint256 bid4Time = bidAndWinCurrentAuction(bidder4, 4.4 ether); + + auction.growPriceHistory(4); + + Noracle.Observation[] memory prices = auction.prices(2); + assertEq(prices.length, 2); + + assertEq(prices[0].blockTimestamp, uint32(bid4Time)); + assertEq(prices[0].nounId, 4); + assertEq(prices[0].amount, 4.4e8); + assertEq(prices[0].winner, bidder4); + + assertEq(prices[1].blockTimestamp, uint32(bid3Time)); + assertEq(prices[1].nounId, 3); + assertEq(prices[1].amount, 3.3e8); + assertEq(prices[1].winner, bidder3); + } + function test_growPriceHistory_emitsEvent() public { vm.expectEmit(true, true, true, true); emit PriceHistoryGrown(1, 3); From b84d5a8258512d40180c16ab8b3abea17ab12c87 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 29 Nov 2022 10:13:47 -0500 Subject: [PATCH 039/115] cleanup tests --- .../test/foundry/NounsAuctionHouseV2.t.sol | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index deff731747..6f27324f60 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -12,7 +12,7 @@ import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHou import { Noracle } from '../../contracts/libs/Noracle.sol'; import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; -contract NounsAuctionHouseV2Test is Test, DeployUtils { +contract NounsAuctionHouseV2TestBase is Test, DeployUtils { event PriceHistoryGrown(uint32 current, uint32 next); address owner = address(0x1111); @@ -37,6 +37,19 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { vm.roll(block.number + 1); } + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { + (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + vm.deal(bidder, bid); + vm.prank(bidder); + auction.createBid{ value: bid }(nounId); + vm.warp(endTime); + auction.settleCurrentAndCreateNewAuction(); + return block.timestamp; + } +} + +contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { + function test_createBid_revertsGivenWrongNounId() public { (uint128 nounId, , , , , ) = auction.auction(); @@ -222,19 +235,9 @@ contract NounsAuctionHouseV2Test is Test, DeployUtils { assertEq(bidderV2, bidderV1); assertEq(settledV2, settledV1); } - - function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); - vm.deal(bidder, bid); - vm.prank(bidder); - auction.createBid{ value: bid }(nounId); - vm.warp(endTime); - auction.settleCurrentAndCreateNewAuction(); - return block.timestamp; - } } -contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2Test { +contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { function test_prices_worksWithOneAuction() public { address bidder = address(0x4444); bidAndWinCurrentAuction(bidder, 1 ether); @@ -343,7 +346,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2Test { address bidder3 = address(0x6666); address bidder4 = address(0x6666); bidAndWinCurrentAuction(bidder1, 1.1 ether); - uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2.2 ether); + bidAndWinCurrentAuction(bidder2, 2.2 ether); uint256 bid3Time = bidAndWinCurrentAuction(bidder3, 3.3 ether); uint256 bid4Time = bidAndWinCurrentAuction(bidder4, 4.4 ether); From 1ee422ad421387604b08bd132d6ed9adaa0d04e3 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 29 Nov 2022 10:15:21 -0500 Subject: [PATCH 040/115] CR cleanups --- .../contracts/NounsAuctionHousePreV2Migration.sol | 1 + packages/nouns-contracts/contracts/libs/Noracle.sol | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol index bd1a4aff2d..eb5bb82cea 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol @@ -35,6 +35,7 @@ contract NounsAuctionHousePreV2Migration is PausableUpgradeable, ReentrancyGuard function migrate() public onlyOwner { INounsAuctionHouse.Auction memory _auction = auction; + // nullifying previous storage slots to avoid the risk of leftovers auction = INounsAuctionHouse.Auction(0, 0, 0, 0, payable(address(0)), false); INounsAuctionHouse.AuctionV2 storage auctionV2; diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol index 5853429b14..e1f76d637b 100644 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ b/packages/nouns-contracts/contracts/libs/Noracle.sol @@ -102,7 +102,7 @@ library Noracle { uint32 cardinalityNext = self.cardinalityNext; if (cardinalityNext == 0) return; - // if the conditions are right, we can bump the cardinality + // if the conditions are right, we can bump the cardinality if (cardinalityNext > cardinality && currentIndex == (cardinality - 1)) { self.cardinality = cardinality = cardinalityNext; } From 3911ccd633be1950ccb87e181dd130972d5b96ac Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 6 Jan 2023 12:02:20 -0500 Subject: [PATCH 041/115] infinite oracle initial version --- .../contracts/NounsAuctionHouseV2.sol | 118 +++++--- .../interfaces/INounsAuctionHouse.sol | 21 ++ .../contracts/libs/Noracle.sol | 214 -------------- .../test/foundry/Noracle.t.sol | 122 -------- .../test/foundry/NounsAuctionHouseV2.t.sol | 267 ++++++++++-------- .../test/foundry/helpers/DeployUtils.sol | 3 - 6 files changed, 257 insertions(+), 488 deletions(-) delete mode 100644 packages/nouns-contracts/contracts/libs/Noracle.sol delete mode 100644 packages/nouns-contracts/test/foundry/Noracle.t.sol diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 7c77551234..4a86a55f21 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -31,7 +31,6 @@ import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import { INounsAuctionHouse } from './interfaces/INounsAuctionHouse.sol'; import { INounsToken } from './interfaces/INounsToken.sol'; import { IWETH } from './interfaces/IWETH.sol'; -import { Noracle } from './libs/Noracle.sol'; contract NounsAuctionHouseV2 is INounsAuctionHouse, @@ -39,8 +38,6 @@ contract NounsAuctionHouseV2 is ReentrancyGuardUpgradeable, OwnableUpgradeable { - using Noracle for Noracle.NoracleState; - // The Nouns ERC721 token contract INounsToken public nouns; @@ -63,7 +60,7 @@ contract NounsAuctionHouseV2 is INounsAuctionHouse.AuctionV2 public auction; // The Nouns price feed state - Noracle.NoracleState public oracle; + mapping(uint256 => ObservationState) observations; /** * @notice Initialize the auction house and base contracts, @@ -244,12 +241,11 @@ contract NounsAuctionHouseV2 is _safeTransferETHWithFallback(owner(), _auction.amount); } - oracle.write( - uint32(block.timestamp), - uint16(_auction.nounId), - Noracle.ethPriceToUint48(_auction.amount), - _auction.bidder - ); + observations[_auction.nounId] = ObservationState({ + blockTimestamp: uint32(block.timestamp), + amount: ethPriceToUint64(_auction.amount), + winner: _auction.bidder + }); emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount); } @@ -276,27 +272,16 @@ contract NounsAuctionHouseV2 is return success; } - /** - * @notice Initialize the price oracle, setting its cardinality to one. - * Cardinality is the the number past prices to store. For the oracle to store more than one price - * call `growPriceHistory` with the new number of auctions the oracle should store. - * @dev This function can only be called by `owner`. - */ - function initializeOracle() external onlyOwner { - oracle.initialize(); - } - - /** - * @notice Increase the price oracle cardinality, i.e. how many past auction prices it stores. - * The caller of this function chooses to spend gas to warm up the new storage slots, so that - * writing price history upon auction settlement will cost minimal gas. - * In other words, the higher the value of `newCardinality`, the more gas calling this function costs. - * @param newCardinality the new total number of price history slots the oracle can store. - */ - function growPriceHistory(uint32 newCardinality) external { - (uint32 current, uint32 next) = oracle.grow(newCardinality); + function setPrices(Observation[] memory observations_) external onlyOwner { + for (uint256 i = 0; i < observations_.length; ++i) { + observations[observations_[i].nounId] = ObservationState({ + blockTimestamp: observations_[i].blockTimestamp, + amount: observations_[i].amount, + winner: observations_[i].winner + }); + } - emit PriceHistoryGrown(current, next); + // TODO emit event } /** @@ -308,10 +293,77 @@ contract NounsAuctionHouseV2 is * Since the oracle only has 3 prices stored, the user will get 3 observations. * @dev Reverts with a `AuctionCountOutOfBounds` error if `auctionCount` is greater than `oracle.cardinality`. * @param auctionCount The number of price observations to get. - * @return observations An array of type `Noracle.Observation`, where each Observation includes a timestamp, + * @return observations_ An array of type `Noracle.Observation`, where each Observation includes a timestamp, * the Noun ID of that auction, the winning bid amount, and the winner's addreess. */ - function prices(uint32 auctionCount) external view returns (Noracle.Observation[] memory observations) { - return oracle.observe(auctionCount); + function prices(uint256 auctionCount) external view returns (Observation[] memory observations_) { + uint256 latestNounId = auction.nounId; + if (!auction.settled && latestNounId > 0) { + latestNounId -= 1; + } + + observations_ = new Observation[](auctionCount); + uint256 observationsCount = 0; + while (observationsCount < auctionCount && latestNounId > 0) { + // Skip Nouner reward Nouns, they have no price + if (latestNounId <= 1820 && latestNounId % 10 == 0) { + --latestNounId; + continue; + } + + observations_[observationsCount] = Observation({ + blockTimestamp: observations[latestNounId].blockTimestamp, + amount: observations[latestNounId].amount, + winner: observations[latestNounId].winner, + nounId: latestNounId + }); + ++observationsCount; + --latestNounId; + } + + if (auctionCount > observationsCount) { + // this assembly trims the observations array, getting rid of unused cells + assembly { + mstore(observations_, observationsCount) + } + } + } + + function prices(uint256 latestId, uint256 oldestId) external view returns (Observation[] memory observations_) { + observations_ = new Observation[](latestId - oldestId); + uint256 observationsCount = 0; + uint256 currentId = latestId; + while (currentId > oldestId) { + // Skip Nouner reward Nouns, they have no price + if (currentId <= 1820 && currentId % 10 == 0) { + --currentId; + continue; + } + + observations_[observationsCount] = Observation({ + blockTimestamp: observations[currentId].blockTimestamp, + amount: observations[currentId].amount, + winner: observations[currentId].winner, + nounId: currentId + }); + ++observationsCount; + --currentId; + } + + if (observations_.length > observationsCount) { + // this assembly trims the observations array, getting rid of unused cells + assembly { + mstore(observations_, observationsCount) + } + } + } + + /** + * @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); } } diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 216269f082..8cb35f2ccf 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -48,6 +48,27 @@ interface INounsAuctionHouse { bool settled; } + struct ObservationState { + // The block.timestamp when the auction was settled. + uint32 blockTimestamp; + // The winning bid amount, with 10 decimal places (reducing accuracy to save bits). + // TODO update accuracy + uint64 amount; + // The address of the auction winner. + address winner; + } + + struct Observation { + // 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; + // 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); diff --git a/packages/nouns-contracts/contracts/libs/Noracle.sol b/packages/nouns-contracts/contracts/libs/Noracle.sol deleted file mode 100644 index e1f76d637b..0000000000 --- a/packages/nouns-contracts/contracts/libs/Noracle.sol +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -/// @title A library used to maintain Nouns Auction House price history - -/********************************* - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - * ░░░░░░█████████░░█████████░░░ * - * ░░░░░░██░░░████░░██░░░████░░░ * - * ░░██████░░░████████░░░████░░░ * - * ░░██░░██░░░████░░██░░░████░░░ * - * ░░██░░██░░░████░░██░░░████░░░ * - * ░░░░░░█████████░░█████████░░░ * - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - *********************************/ - -pragma solidity ^0.8.6; - -/** - * @dev A Nouns price history oracle. Inspired by Uniswap's Oracle.sol: - * https://github.com/Uniswap/v3-core/blob/fc2107bd5709cdee6742d5164c1eb998566bcb75/contracts/libraries/Oracle.sol - * - * It uses warmed up storage slots only, to minimize the cost on users when settling auctions and storing prices. - * The number of slots is stored in the `cardinality` state var, and can be increased by users who choose to pay the gas. - * - * `Observation` variable sizes have been packed to fit all variables in a single storage slot, to further optimize - * gas on both `grow` and `write` functions. - */ -library Noracle { - /// @notice The value assigned to an Observation's `blockTimestamp` to warm up the storage slot. - uint32 public constant WARMUP_TIMESTAMP = 1; - - error AlreadyInitialized(); - error NotInitialized(); - error AuctionCountOutOfBounds(uint32 auctionCount, uint32 cardinality); - - /** - * @dev Struct members size has been adjusted so the sum of bits is 256, to fit into a single storage slot. - * If updated to use more than 256 bits, consider updating `warmUpObservation` to warm up the additional storage space. - * The maximum `amount` supported is 2814749.76710655 ETH. - */ - struct Observation { - // The block.timestamp when the auction was settled. - uint32 blockTimestamp; - // ID for the Noun (ERC721 token ID). - uint16 nounId; - // The winning bid amount, with 8 decimal places (reducing accuracy to save bits). - uint48 amount; - // The address of the auction winner. - address winner; - } - - /** - * @dev Contains the full oracle state, the primary `self` used in this library, as a UX optimization for - * contracts using this library, so they don't have to keep track of any state variables (as opposed to Uniswap's - * oracle where the Pool contract holds these state variables). - */ - struct NoracleState { - // Price history, stored as a mapping, and not an array, due to Solidity limitations. - mapping(uint32 => Observation) observations; - // The latest price index. - uint32 index; - // The maximum number of observation the oracle can store. - uint32 cardinality; - // The next value to assign to cardinality once the oracle needs more slots, also the current number of warm storage slots. - // Important to the proper function of `observe`; if `cardinality` was incremented directly in `grow`, `observe` would not - // work as expected when looping back from index zero to the end of stored values is necessary - // e.g. 10 written values, index at zero, asking to `observe` 2+ prices, would loop back to the new cardinality - 1, hit an - // uninitialized observation and exit early. - uint32 cardinalityNext; - } - - /** - * @dev Initialize the oracle with a cardinality of one. - */ - function initialize(NoracleState storage self) internal { - if (self.cardinality > 0) revert AlreadyInitialized(); - - self.cardinality = 1; - self.cardinalityNext = 1; - warmUpObservation(self.observations[0]); - } - - /** - * @dev Write a new auction price to oracle storage. - * @param self The oracle state. - * @param blockTimestamp The auction's settlement block timestamp, reduced to uint32. - * @param nounId The auction's Noun ID. - * @param amount The auction's winning bid, reduced to a uint48 with 8 decimal places. Max supported value is 2814749.76710655 ETH. - * @param winner The auction winner's address. - */ - function write( - NoracleState storage self, - uint32 blockTimestamp, - uint16 nounId, - uint48 amount, - address winner - ) internal { - uint32 currentIndex = self.index; - uint32 cardinality = self.cardinality; - uint32 cardinalityNext = self.cardinalityNext; - if (cardinalityNext == 0) return; - - // if the conditions are right, we can bump the cardinality - if (cardinalityNext > cardinality && currentIndex == (cardinality - 1)) { - self.cardinality = cardinality = cardinalityNext; - } - - // if `grow` is executed before the first write, cardinality is greater than 1 - // and the first write will be into index 1. - uint32 newIndex = (currentIndex + 1) % cardinality; - self.index = newIndex; - self.observations[newIndex] = Observation({ - blockTimestamp: blockTimestamp, - nounId: nounId, - amount: amount, - winner: winner - }); - } - - /** - * @dev Grow the oracle's next cardinality, warming up the relevant storage slots. - * Does nothing if `next` isn't greater than the current `cardinalityNext` value. - * @param self The oracle state. - * @param next The new `cardinalityNext` value. - * @return uint32 The previous `cardinalityNext` value. - * @return uint32 The new `cardinalityNext` value. - */ - function grow(NoracleState storage self, uint32 next) internal returns (uint32, uint32) { - uint32 current = self.cardinalityNext; - if (current == 0) revert NotInitialized(); - - // no-op if the passed next value isn't greater than the current next value - if (next <= current) return (current, current); - - // store in each slot to prevent fresh SSTOREs - // this data will not be used because the initialized boolean is still false - for (uint32 i = current; i < next; ++i) { - warmUpObservation(self.observations[i]); - } - - self.cardinalityNext = next; - return (current, next); - } - - /** - * @dev Get past auction prices, up to `oracle.cardinality` observations. - * There are times when cardinality is increased and not yet fully used, when a user might request more - * observations than what's stored; in such cases users will get the maximum number of observations the - * oracle has to offer. - * For example, say cardinality was just increased from 3 to 10, a user can then ask for 10 observations. - * Since the oracle only has 3 prices stored, the user will get 3 observations. - * Reverts with a `AuctionCountOutOfBounds` error if `auctionCount` is greater than `oracle.cardinality`. - * @param self The oracle state. - * @param auctionCount The number of price observations to get. - * @return observations An array of type `Noracle.Observation`, where each Observation includes a timestamp, - * the Noun ID of that auction, the winning bid amount, and the winner's addreess. - */ - function observe(NoracleState storage self, uint32 auctionCount) - internal - view - returns (Observation[] memory observations) - { - uint32 index = self.index; - uint32 cardinality = self.cardinality; - if (auctionCount > cardinality) revert AuctionCountOutOfBounds(auctionCount, cardinality); - - observations = new Observation[](auctionCount); - uint32 observationsCount = 0; - while (observationsCount < auctionCount) { - uint32 checkIndex = (index + (cardinality - observationsCount)) % cardinality; - Observation storage obs = self.observations[checkIndex]; - - // when cardinality has been increased, and not used up yet, there are uninitialized items - // this loop reads from index backwards, so when it hits an uninitialized item - // it means all items from there backwards until index are uninitialized, so we should break. - if (!initialized(obs)) break; - - observations[observationsCount] = obs; - ++observationsCount; - } - - if (auctionCount > observationsCount) { - // this assembly trims the observations array, getting rid of unused cells - assembly { - mstore(observations, observationsCount) - } - } - } - - /** - * @dev Convert an ETH price of 256 bits with 18 decimals, to 48 bits with 8 decimals. - * Max supported value is 2814749.76710655 ETH. - */ - function ethPriceToUint48(uint256 ethPrice) internal pure returns (uint48) { - return uint48(ethPrice / 1e10); - } - - /** - * @dev Write a stub value to warm up the observation storage slot. - */ - function warmUpObservation(Observation storage obs) private { - obs.blockTimestamp = WARMUP_TIMESTAMP; - } - - /** - * @dev Check if an observation has been initialized, by checking if it has a - * non-stub `blockTimestamp` value. - */ - function initialized(Observation storage obs) internal view returns (bool) { - return obs.blockTimestamp > WARMUP_TIMESTAMP; - } -} diff --git a/packages/nouns-contracts/test/foundry/Noracle.t.sol b/packages/nouns-contracts/test/foundry/Noracle.t.sol deleted file mode 100644 index af9ff3e1c4..0000000000 --- a/packages/nouns-contracts/test/foundry/Noracle.t.sol +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.6; - -import 'forge-std/Test.sol'; -import { Noracle } from '../../contracts/libs/Noracle.sol'; - -contract NoracleTest is Test { - using Noracle for Noracle.NoracleState; - - Noracle.NoracleState state; - - function setUp() public { - vm.warp(Noracle.WARMUP_TIMESTAMP + 1); - } - - function test_initialize_revertsOnRepeatAttempt() public { - state.initialize(); - - vm.expectRevert(abi.encodeWithSelector(Noracle.AlreadyInitialized.selector)); - state.initialize(); - } - - function test_grow_revertsWheNotInitialized() public { - vm.expectRevert(abi.encodeWithSelector(Noracle.NotInitialized.selector)); - state.grow(42); - } - - function test_write_doesntRevertsWheNotInitialized() public { - state.write(uint32(block.timestamp), 142, 69, address(0xdead)); - } - - function test_writeObserve_cardinality1_worksWithOneWrite() public { - state.initialize(); - state.write(uint32(block.timestamp), 142, 69, address(0xdead)); - - Noracle.Observation[] memory observations = state.observe(1); - - assertEq(observations.length, 1); - assertEq(observations[0].blockTimestamp, block.timestamp); - assertEq(observations[0].nounId, 142); - assertEq(observations[0].amount, 69); - assertEq(observations[0].winner, address(0xdead)); - } - - function test_writeObserve_cardinality1_preserves8DecimalsUnderUint48MaxValue() public { - state.initialize(); - - // amount is uint48; maxValue - 1 = 281474976710655 - // at 8 decimal points it's 2814749.76710655 - state.write( - uint32(block.timestamp), - 142, - Noracle.ethPriceToUint48(2814749.76710655999999 ether), - address(0xdead) - ); - - Noracle.Observation[] memory observations = state.observe(1); - - assertEq(observations.length, 1); - assertEq(observations[0].blockTimestamp, block.timestamp); - assertEq(observations[0].nounId, 142); - assertEq(observations[0].amount, 281474976710655); - assertEq(observations[0].winner, address(0xdead)); - } - - function test_writeObserve_cardinality1_secondWriteOverrides() public { - state.initialize(); - state.write(uint32(block.timestamp), 142, 69, address(0xdead)); - state.write(uint32(block.timestamp + 1), 143, 70, address(0x1234)); - - Noracle.Observation[] memory observations = state.observe(1); - - assertEq(observations.length, 1); - assertEq(observations[0].blockTimestamp, block.timestamp + 1); - assertEq(observations[0].nounId, 143); - assertEq(observations[0].amount, 70); - assertEq(observations[0].winner, address(0x1234)); - - vm.expectRevert(abi.encodeWithSelector(Noracle.AuctionCountOutOfBounds.selector, 2, 1)); - state.observe(2); - } - - function test_writeObserve_cadinality2_secondWriteDoesNotOverride() public { - state.initialize(); - state.write(uint32(block.timestamp), 142, 69, address(0xdead)); - state.grow(2); - state.write(uint32(block.timestamp + 1), 143, 70, address(0x1234)); - - Noracle.Observation[] memory observations = state.observe(2); - assertEq(observations.length, 2); - - assertEq(observations[0].blockTimestamp, block.timestamp + 1); - assertEq(observations[0].nounId, 143); - assertEq(observations[0].amount, 70); - assertEq(observations[0].winner, address(0x1234)); - - assertEq(observations[1].blockTimestamp, block.timestamp); - assertEq(observations[1].nounId, 142); - assertEq(observations[1].amount, 69); - assertEq(observations[1].winner, address(0xdead)); - } - - function test_observe_returnsEmptyArrayGivenNoWrites() public { - state.initialize(); - - Noracle.Observation[] memory observations = state.observe(1); - - assertEq(observations.length, 0); - } - - function test_observe_trimsObervationsArrayGivenHighCardinalityWithManyEmptySlots() public { - state.initialize(); - state.write(uint32(block.timestamp), 142, 69, address(0xdead)); - state.grow(1_000); - state.write(uint32(block.timestamp + 1), 143, 70, address(0x1234)); - - Noracle.Observation[] memory observations = state.observe(500); - - assertEq(state.cardinality, 1_000); - assertEq(observations.length, 2); - } -} diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 6f27324f60..6e20c53463 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -9,7 +9,6 @@ import { NounsAuctionHouse } from '../../contracts/NounsAuctionHouse.sol'; import { INounsAuctionHouse } from '../../contracts/interfaces/INounsAuctionHouse.sol'; import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHousePreV2Migration.sol'; -import { Noracle } from '../../contracts/libs/Noracle.sol'; import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; contract NounsAuctionHouseV2TestBase is Test, DeployUtils { @@ -46,10 +45,18 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { auction.settleCurrentAndCreateNewAuction(); return block.timestamp; } + + function bidDontCreateNewAuction(address bidder, uint256 bid) internal returns (uint256) { + (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + 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(); @@ -238,148 +245,176 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { - function test_prices_worksWithOneAuction() public { + function test_prices_oneAuction_higherAuctionCountReturnsTheOneAuction() public { address bidder = address(0x4444); bidAndWinCurrentAuction(bidder, 1 ether); - Noracle.Observation[] memory prices = auction.prices(1); + INounsAuctionHouse.Observation[] memory prices = auction.prices(2); + assertEq(prices.length, 1); assertEq(prices[0].blockTimestamp, uint32(block.timestamp)); assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1e8); + assertEq(prices[0].amount, 1e10); assertEq(prices[0].winner, bidder); } - function test_prices_worksWithThreeAuctions() public { - vm.prank(owner); - auction.growPriceHistory(3); + 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); - address bidder1 = address(0x4444); - address bidder2 = address(0x5555); - address bidder3 = address(0x6666); - uint256 bid1Time = bidAndWinCurrentAuction(bidder1, 1.1 ether); - uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2.2 ether); - uint256 bid3Time = bidAndWinCurrentAuction(bidder3, 3.3 ether); - - Noracle.Observation[] memory prices = auction.prices(3); - assertEq(prices.length, 3); - - assertEq(prices[0].blockTimestamp, uint32(bid3Time)); - assertEq(prices[0].nounId, 3); - assertEq(prices[0].amount, 3.3e8); - assertEq(prices[0].winner, bidder3); - - assertEq(prices[1].blockTimestamp, uint32(bid2Time)); - assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2.2e8); - assertEq(prices[1].winner, bidder2); - - assertEq(prices[2].blockTimestamp, uint32(bid1Time)); - assertEq(prices[2].nounId, 1); - assertEq(prices[2].amount, 1.1e8); - assertEq(prices[2].winner, bidder1); - } - - function test_prices_worksWithHigherCardinality() public { - address bidder1 = address(0x4444); - uint256 bid1Time = bidAndWinCurrentAuction(bidder1, 1 ether); - - vm.prank(owner); - auction.growPriceHistory(1_000); - - address bidder2 = address(0x5555); - uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2 ether); - - (, uint32 cardinality, ) = auction.oracle(); - assertEq(cardinality, 1_000); - - Noracle.Observation[] memory prices = auction.prices(1_000); - - assertEq(prices.length, 2); - - assertEq(prices[0].blockTimestamp, uint32(bid2Time)); - assertEq(prices[0].nounId, 2); - assertEq(prices[0].amount, 2e8); - assertEq(prices[0].winner, bidder2); + INounsAuctionHouse.Observation[] memory prices = auction.prices(1); - assertEq(prices[1].blockTimestamp, uint32(bid1Time)); - assertEq(prices[1].nounId, 1); - assertEq(prices[1].amount, 1e8); - assertEq(prices[1].winner, bidder1); + assertEq(prices.length, 1); + assertEq(prices[0].nounId, 1); + assertEq(prices[0].amount, 18446744073709551615); + assertEq(prices[0].winner, makeAddr('bidder')); } - function test_prices_dropsEarlierBidsWithLowerCardinality() public { - vm.prank(owner); - auction.growPriceHistory(2); + function test_prices_overflowsGracefullyOverUint64MaxValue() public { + bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551617 ether); - address bidder1 = address(0x4444); - address bidder2 = address(0x5555); - address bidder3 = address(0x6666); - bidAndWinCurrentAuction(bidder1, 1.1 ether); - uint256 bid2Time = bidAndWinCurrentAuction(bidder2, 2.2 ether); - uint256 bid3Time = bidAndWinCurrentAuction(bidder3, 3.3 ether); - - Noracle.Observation[] memory prices = auction.prices(2); - assertEq(prices.length, 2); + INounsAuctionHouse.Observation[] memory prices = auction.prices(1); - assertEq(prices[0].blockTimestamp, uint32(bid3Time)); - assertEq(prices[0].nounId, 3); - assertEq(prices[0].amount, 3.3e8); - assertEq(prices[0].winner, bidder3); - - assertEq(prices[1].blockTimestamp, uint32(bid2Time)); - assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2.2e8); - assertEq(prices[1].winner, bidder2); + assertEq(prices.length, 1); + assertEq(prices[0].nounId, 1); + assertEq(prices[0].amount, 1); + assertEq(prices[0].winner, makeAddr('bidder')); } - function test_prices_worksWhenLoopingBackFromIndexZeroAfterGrow() public { - vm.prank(owner); + function test_prices_20Auctions_skipsNounerNounsAsExpected() public { + for (uint256 i = 1; i <= 20; ++i) { + address bidder = makeAddr(vm.toString(i)); + bidAndWinCurrentAuction(bidder, i * 1e18); + } + + INounsAuctionHouse.Observation[] memory prices = auction.prices(20); + assertEq(prices[0].nounId, 22); + assertEq(prices[1].nounId, 21); + assertEq(prices[2].nounId, 19); + assertEq(prices[10].nounId, 11); + assertEq(prices[11].nounId, 9); + assertEq(prices[19].nounId, 1); + + assertEq(prices[0].amount, 20e10); + assertEq(prices[1].amount, 19e10); + assertEq(prices[2].amount, 18e10); + assertEq(prices[10].amount, 10e10); + assertEq(prices[11].amount, 9e10); + assertEq(prices[19].amount, 1e10); + } - // because grow happens before any writes, the first written index is 1 - // so we need 4 writes to bring index to zero, such that getting two prices - // leads to a loop back to index = cardinality - 1 - auction.growPriceHistory(2); + function test_prices_2AuctionsNoNewAuction_includesSettledNoun() public { + uint256 bid1Timestamp = bidAndWinCurrentAuction(makeAddr('bidder'), 1 ether); + uint256 bid2Timestamp = bidDontCreateNewAuction(makeAddr('bidder 2'), 2 ether); - address bidder1 = address(0x4444); - address bidder2 = address(0x5555); - address bidder3 = address(0x6666); - address bidder4 = address(0x6666); - bidAndWinCurrentAuction(bidder1, 1.1 ether); - bidAndWinCurrentAuction(bidder2, 2.2 ether); - uint256 bid3Time = bidAndWinCurrentAuction(bidder3, 3.3 ether); - uint256 bid4Time = bidAndWinCurrentAuction(bidder4, 4.4 ether); + vm.prank(auction.owner()); + auction.pause(); + auction.settleAuction(); - auction.growPriceHistory(4); + INounsAuctionHouse.Observation[] memory prices = auction.prices(2); - Noracle.Observation[] memory prices = auction.prices(2); assertEq(prices.length, 2); + assertEq(prices[0].blockTimestamp, uint32(bid2Timestamp)); + assertEq(prices[0].nounId, 2); + assertEq(prices[0].amount, 2e10); + assertEq(prices[0].winner, makeAddr('bidder 2')); + assertEq(prices[1].blockTimestamp, uint32(bid1Timestamp)); + assertEq(prices[1].nounId, 1); + assertEq(prices[1].amount, 1e10); + assertEq(prices[1].winner, makeAddr('bidder')); + } - assertEq(prices[0].blockTimestamp, uint32(bid4Time)); - assertEq(prices[0].nounId, 4); - assertEq(prices[0].amount, 4.4e8); - assertEq(prices[0].winner, bidder4); + 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); + } - assertEq(prices[1].blockTimestamp, uint32(bid3Time)); + INounsAuctionHouse.Observation[] memory prices = auction.prices(4, 0); + assertEq(prices.length, 4); + assertEq(prices[0].blockTimestamp, 0); + assertEq(prices[0].nounId, 4); + assertEq(prices[0].amount, 0); + assertEq(prices[0].winner, address(0)); + assertEq(prices[1].blockTimestamp, uint32(lastBidTime)); assertEq(prices[1].nounId, 3); - assertEq(prices[1].amount, 3.3e8); - assertEq(prices[1].winner, bidder3); + assertEq(prices[1].amount, 3e10); + assertEq(prices[1].winner, makeAddr('3')); + assertEq(prices[2].nounId, 2); + assertEq(prices[2].amount, 2e10); + assertEq(prices[2].winner, makeAddr('2')); + assertEq(prices[3].nounId, 1); + assertEq(prices[3].amount, 1e10); + assertEq(prices[3].winner, makeAddr('1')); } - function test_growPriceHistory_emitsEvent() public { - vm.expectEmit(true, true, true, true); - emit PriceHistoryGrown(1, 3); - vm.prank(owner); - auction.growPriceHistory(3); + function test_prices_withRange_givenSmallerRangeThanAuctionsReturnsAuctions() public { + for (uint256 i = 1; i <= 20; ++i) { + address bidder = makeAddr(vm.toString(i)); + bidAndWinCurrentAuction(bidder, i * 1e18); + } + + INounsAuctionHouse.Observation[] memory prices = auction.prices(11, 6); + assertEq(prices.length, 4); + assertEq(prices[0].nounId, 11); + assertEq(prices[0].amount, 10e10); + assertEq(prices[0].winner, makeAddr('10')); + assertEq(prices[1].nounId, 9); + assertEq(prices[1].amount, 9e10); + assertEq(prices[1].winner, makeAddr('9')); + assertEq(prices[2].nounId, 8); + assertEq(prices[2].amount, 8e10); + assertEq(prices[2].winner, makeAddr('8')); + assertEq(prices[3].nounId, 7); + assertEq(prices[3].amount, 7e10); + assertEq(prices[3].winner, makeAddr('7')); + } - vm.expectEmit(true, true, true, true); - emit PriceHistoryGrown(3, 5); - vm.prank(owner); - auction.growPriceHistory(5); + function test_setPrices_revertsForNonOwner() public { + INounsAuctionHouse.Observation[] memory observations = new INounsAuctionHouse.Observation[](1); + observations[0] = INounsAuctionHouse.Observation({ + blockTimestamp: uint32(block.timestamp), + amount: 42e10, + winner: makeAddr('winner'), + nounId: 3 + }); + + vm.expectRevert('Ownable: caller is not the owner'); + auction.setPrices(observations); + } - vm.expectEmit(true, true, true, true); - emit PriceHistoryGrown(5, 5); - vm.prank(owner); - auction.growPriceHistory(5); + function test_setPrices_worksForOwner() public { + INounsAuctionHouse.Observation[] memory observations = new INounsAuctionHouse.Observation[](20); + uint256 nounId = 0; + for (uint256 i = 0; i < 20; ++i) { + // skip Nouners + if (nounId <= 1820 && nounId % 10 == 0) { + nounId++; + } + + observations[i] = INounsAuctionHouse.Observation({ + blockTimestamp: uint32(nounId), + amount: uint64(nounId * 1e10), + winner: makeAddr(vm.toString(nounId)), + nounId: nounId + }); + + nounId++; + } + + vm.prank(auction.owner()); + auction.setPrices(observations); + + INounsAuctionHouse.Observation[] memory actualObservations = auction.prices(22, 0); + assertEq(actualObservations.length, 20); + for (uint256 i = 0; i < 20; ++i) { + uint256 actualIndex = 19 - i; + assertEq(observations[i].blockTimestamp, actualObservations[actualIndex].blockTimestamp); + assertEq(observations[i].amount, actualObservations[actualIndex].amount); + assertEq(observations[i].winner, actualObservations[actualIndex].winner); + assertEq(observations[i].nounId, actualObservations[actualIndex].nounId); + } } } diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index fd8af65427..9c84a79a62 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -77,10 +77,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { proxyAdmin.upgrade(proxy, address(migratorLogic)); NounsAuctionHousePreV2Migration migrator = NounsAuctionHousePreV2Migration(address(proxy)); migrator.migrate(); - proxyAdmin.upgrade(proxy, address(newLogic)); - NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(proxy)); - auctionV2.initializeOracle(); vm.stopPrank(); } From 2c00c8240861916d9e439a43050db14cec99cee6 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 13 Jan 2023 12:18:31 -0500 Subject: [PATCH 042/115] rename --- .../contracts/NounsAuctionHouseV2.sol | 52 +++++++++---------- .../interfaces/INounsAuctionHouse.sol | 4 +- .../test/foundry/NounsAuctionHouseV2.t.sol | 24 ++++----- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 4a86a55f21..844d34083a 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -60,7 +60,7 @@ contract NounsAuctionHouseV2 is INounsAuctionHouse.AuctionV2 public auction; // The Nouns price feed state - mapping(uint256 => ObservationState) observations; + mapping(uint256 => SettlementState) settlementHistory; /** * @notice Initialize the auction house and base contracts, @@ -241,7 +241,7 @@ contract NounsAuctionHouseV2 is _safeTransferETHWithFallback(owner(), _auction.amount); } - observations[_auction.nounId] = ObservationState({ + settlementHistory[_auction.nounId] = SettlementState({ blockTimestamp: uint32(block.timestamp), amount: ethPriceToUint64(_auction.amount), winner: _auction.bidder @@ -272,9 +272,9 @@ contract NounsAuctionHouseV2 is return success; } - function setPrices(Observation[] memory observations_) external onlyOwner { + function setPrices(Settlement[] memory observations_) external onlyOwner { for (uint256 i = 0; i < observations_.length; ++i) { - observations[observations_[i].nounId] = ObservationState({ + settlementHistory[observations_[i].nounId] = SettlementState({ blockTimestamp: observations_[i].blockTimestamp, amount: observations_[i].amount, winner: observations_[i].winner @@ -293,45 +293,45 @@ contract NounsAuctionHouseV2 is * Since the oracle only has 3 prices stored, the user will get 3 observations. * @dev Reverts with a `AuctionCountOutOfBounds` error if `auctionCount` is greater than `oracle.cardinality`. * @param auctionCount The number of price observations to get. - * @return observations_ An array of type `Noracle.Observation`, where each Observation includes a timestamp, + * @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 addreess. */ - function prices(uint256 auctionCount) external view returns (Observation[] memory observations_) { + function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements) { uint256 latestNounId = auction.nounId; if (!auction.settled && latestNounId > 0) { latestNounId -= 1; } - observations_ = new Observation[](auctionCount); - uint256 observationsCount = 0; - while (observationsCount < auctionCount && latestNounId > 0) { + settlements = new Settlement[](auctionCount); + uint256 actualCount = 0; + while (actualCount < auctionCount && latestNounId > 0) { // Skip Nouner reward Nouns, they have no price if (latestNounId <= 1820 && latestNounId % 10 == 0) { --latestNounId; continue; } - observations_[observationsCount] = Observation({ - blockTimestamp: observations[latestNounId].blockTimestamp, - amount: observations[latestNounId].amount, - winner: observations[latestNounId].winner, + settlements[actualCount] = Settlement({ + blockTimestamp: settlementHistory[latestNounId].blockTimestamp, + amount: settlementHistory[latestNounId].amount, + winner: settlementHistory[latestNounId].winner, nounId: latestNounId }); - ++observationsCount; + ++actualCount; --latestNounId; } - if (auctionCount > observationsCount) { + if (auctionCount > actualCount) { // this assembly trims the observations array, getting rid of unused cells assembly { - mstore(observations_, observationsCount) + mstore(settlements, actualCount) } } } - function prices(uint256 latestId, uint256 oldestId) external view returns (Observation[] memory observations_) { - observations_ = new Observation[](latestId - oldestId); - uint256 observationsCount = 0; + function prices(uint256 latestId, uint256 oldestId) external view returns (Settlement[] memory settlements) { + settlements = new Settlement[](latestId - oldestId); + uint256 actualCount = 0; uint256 currentId = latestId; while (currentId > oldestId) { // Skip Nouner reward Nouns, they have no price @@ -340,20 +340,20 @@ contract NounsAuctionHouseV2 is continue; } - observations_[observationsCount] = Observation({ - blockTimestamp: observations[currentId].blockTimestamp, - amount: observations[currentId].amount, - winner: observations[currentId].winner, + settlements[actualCount] = Settlement({ + blockTimestamp: settlementHistory[currentId].blockTimestamp, + amount: settlementHistory[currentId].amount, + winner: settlementHistory[currentId].winner, nounId: currentId }); - ++observationsCount; + ++actualCount; --currentId; } - if (observations_.length > observationsCount) { + if (settlements.length > actualCount) { // this assembly trims the observations array, getting rid of unused cells assembly { - mstore(observations_, observationsCount) + mstore(settlements, actualCount) } } } diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 8cb35f2ccf..01c87056cb 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -48,7 +48,7 @@ interface INounsAuctionHouse { bool settled; } - struct ObservationState { + 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). @@ -58,7 +58,7 @@ interface INounsAuctionHouse { address winner; } - struct Observation { + struct Settlement { // The block.timestamp when the auction was settled. uint32 blockTimestamp; // The winning bid amount, with 10 decimal places (reducing accuracy to save bits). diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 6e20c53463..33ca6a6072 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -249,7 +249,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { address bidder = address(0x4444); bidAndWinCurrentAuction(bidder, 1 ether); - INounsAuctionHouse.Observation[] memory prices = auction.prices(2); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(2); assertEq(prices.length, 1); assertEq(prices[0].blockTimestamp, uint32(block.timestamp)); @@ -263,7 +263,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { // at 10 decimal points it's 1844674407.3709551615 bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551615999999 ether); - INounsAuctionHouse.Observation[] memory prices = auction.prices(1); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(1); assertEq(prices.length, 1); assertEq(prices[0].nounId, 1); @@ -274,7 +274,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { function test_prices_overflowsGracefullyOverUint64MaxValue() public { bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551617 ether); - INounsAuctionHouse.Observation[] memory prices = auction.prices(1); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(1); assertEq(prices.length, 1); assertEq(prices[0].nounId, 1); @@ -288,7 +288,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Observation[] memory prices = auction.prices(20); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(20); assertEq(prices[0].nounId, 22); assertEq(prices[1].nounId, 21); assertEq(prices[2].nounId, 19); @@ -312,7 +312,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { auction.pause(); auction.settleAuction(); - INounsAuctionHouse.Observation[] memory prices = auction.prices(2); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(2); assertEq(prices.length, 2); assertEq(prices[0].blockTimestamp, uint32(bid2Timestamp)); @@ -332,7 +332,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { lastBidTime = bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Observation[] memory prices = auction.prices(4, 0); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(4, 0); assertEq(prices.length, 4); assertEq(prices[0].blockTimestamp, 0); assertEq(prices[0].nounId, 4); @@ -356,7 +356,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Observation[] memory prices = auction.prices(11, 6); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(11, 6); assertEq(prices.length, 4); assertEq(prices[0].nounId, 11); assertEq(prices[0].amount, 10e10); @@ -373,8 +373,8 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } function test_setPrices_revertsForNonOwner() public { - INounsAuctionHouse.Observation[] memory observations = new INounsAuctionHouse.Observation[](1); - observations[0] = INounsAuctionHouse.Observation({ + INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.Settlement[](1); + observations[0] = INounsAuctionHouse.Settlement({ blockTimestamp: uint32(block.timestamp), amount: 42e10, winner: makeAddr('winner'), @@ -386,7 +386,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } function test_setPrices_worksForOwner() public { - INounsAuctionHouse.Observation[] memory observations = new INounsAuctionHouse.Observation[](20); + INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.Settlement[](20); uint256 nounId = 0; for (uint256 i = 0; i < 20; ++i) { // skip Nouners @@ -394,7 +394,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { nounId++; } - observations[i] = INounsAuctionHouse.Observation({ + observations[i] = INounsAuctionHouse.Settlement({ blockTimestamp: uint32(nounId), amount: uint64(nounId * 1e10), winner: makeAddr(vm.toString(nounId)), @@ -407,7 +407,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { vm.prank(auction.owner()); auction.setPrices(observations); - INounsAuctionHouse.Observation[] memory actualObservations = auction.prices(22, 0); + INounsAuctionHouse.Settlement[] memory actualObservations = auction.prices(22, 0); assertEq(actualObservations.length, 20); for (uint256 i = 0; i < 20; ++i) { uint256 actualIndex = 19 - i; From 76814869de55404d124176227edc69831dc23e43 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 13 Jan 2023 12:22:02 -0500 Subject: [PATCH 043/115] add a function to warm up settlement history state --- .../nouns-contracts/contracts/NounsAuctionHouseV2.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 844d34083a..6a08ae70f8 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -284,6 +284,14 @@ contract NounsAuctionHouseV2 is // TODO emit event } + function warmUpSettlementState(uint256[] memory nounIds) external { + for (uint256 i = 0; i < nounIds.length; ++i) { + if (settlementHistory[nounIds[i]].blockTimestamp == 0) { + settlementHistory[nounIds[i]] = SettlementState({ blockTimestamp: 1, amount: 0, winner: address(0) }); + } + } + } + /** * @notice Get past auction prices, up to `oracle.cardinality` observations. * There are times when cardinality is increased and not yet fully used, when a user might request more From 7abbf30fc8bdbc69f6a55e622b64b713100b387b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 26 Sep 2023 14:29:54 -0500 Subject: [PATCH 044/115] ah: better skipping of nouns with no price history --- .../contracts/NounsAuctionHouseV2.sol | 6 +++-- .../test/foundry/NounsAuctionHouseV2.t.sol | 27 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 6a08ae70f8..2177cd5fc3 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -314,7 +314,8 @@ contract NounsAuctionHouseV2 is uint256 actualCount = 0; while (actualCount < auctionCount && latestNounId > 0) { // Skip Nouner reward Nouns, they have no price - if (latestNounId <= 1820 && latestNounId % 10 == 0) { + // Also skips IDs with no price data + if (settlementHistory[latestNounId].blockTimestamp == 0) { --latestNounId; continue; } @@ -343,7 +344,8 @@ contract NounsAuctionHouseV2 is uint256 currentId = latestId; while (currentId > oldestId) { // Skip Nouner reward Nouns, they have no price - if (currentId <= 1820 && currentId % 10 == 0) { + // Also skips IDs with no price data + if (settlementHistory[currentId].blockTimestamp == 0) { --currentId; continue; } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 33ca6a6072..59e312c7e9 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -333,21 +333,18 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } INounsAuctionHouse.Settlement[] memory prices = auction.prices(4, 0); - assertEq(prices.length, 4); - assertEq(prices[0].blockTimestamp, 0); - assertEq(prices[0].nounId, 4); - assertEq(prices[0].amount, 0); - assertEq(prices[0].winner, address(0)); - assertEq(prices[1].blockTimestamp, uint32(lastBidTime)); - assertEq(prices[1].nounId, 3); - assertEq(prices[1].amount, 3e10); - assertEq(prices[1].winner, makeAddr('3')); - assertEq(prices[2].nounId, 2); - assertEq(prices[2].amount, 2e10); - assertEq(prices[2].winner, makeAddr('2')); - assertEq(prices[3].nounId, 1); - assertEq(prices[3].amount, 1e10); - assertEq(prices[3].winner, makeAddr('1')); + // lastest ID 4 has no settlement data, so it's not included in the result + assertEq(prices.length, 3); + assertEq(prices[0].blockTimestamp, uint32(lastBidTime)); + assertEq(prices[0].nounId, 3); + assertEq(prices[0].amount, 3e10); + assertEq(prices[0].winner, makeAddr('3')); + assertEq(prices[1].nounId, 2); + assertEq(prices[1].amount, 2e10); + assertEq(prices[1].winner, makeAddr('2')); + assertEq(prices[2].nounId, 1); + assertEq(prices[2].amount, 1e10); + assertEq(prices[2].winner, makeAddr('1')); } function test_prices_withRange_givenSmallerRangeThanAuctionsReturnsAuctions() public { From 1a6f21afe4e117f5611e1cc9937df503d50ac4ae Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 26 Sep 2023 14:33:05 -0500 Subject: [PATCH 045/115] fix natspec --- .../nouns-contracts/contracts/NounsAuctionHouseV2.sol | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 2177cd5fc3..a65c0c55b0 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -293,13 +293,8 @@ contract NounsAuctionHouseV2 is } /** - * @notice Get past auction prices, up to `oracle.cardinality` observations. - * There are times when cardinality is increased and not yet fully used, when a user might request more - * observations than what's stored; in such cases users will get the maximum number of observations the - * oracle has to offer. - * For example, say cardinality was just increased from 3 to 10, a user can then ask for 10 observations. - * Since the oracle only has 3 prices stored, the user will get 3 observations. - * @dev Reverts with a `AuctionCountOutOfBounds` error if `auctionCount` is greater than `oracle.cardinality`. + * @notice Get past auction prices. + * @dev Returns prices in reverse order, meaning settlements[0] will be the most recent auction price. * @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 addreess. From 77ec541c5785e4c6e9c42b11f548e31fbaabf33f Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 26 Sep 2023 15:15:12 -0500 Subject: [PATCH 046/115] ah: flip the order of ranged prices view function seems more intuitive --- .../contracts/NounsAuctionHouseV2.sol | 18 ++++--- .../test/foundry/NounsAuctionHouseV2.t.sol | 53 +++++++++---------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index a65c0c55b0..441ba97d1e 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -333,15 +333,21 @@ contract NounsAuctionHouseV2 is } } - function prices(uint256 latestId, uint256 oldestId) external view returns (Settlement[] memory settlements) { - settlements = new Settlement[](latestId - oldestId); + /** + * @notice Get a range of past auction prices. + * @dev Returns prices in chronological order, as opposed to `prices(count)` which returns prices in reverse order. + * @param startId the first Noun ID to get prices for. + * @param endId end Noun ID (up to, but not including). + */ + function prices(uint256 startId, uint256 endId) external view returns (Settlement[] memory settlements) { + settlements = new Settlement[](endId - startId); uint256 actualCount = 0; - uint256 currentId = latestId; - while (currentId > oldestId) { + uint256 currentId = startId; + while (currentId < endId) { // Skip Nouner reward Nouns, they have no price // Also skips IDs with no price data if (settlementHistory[currentId].blockTimestamp == 0) { - --currentId; + ++currentId; continue; } @@ -352,7 +358,7 @@ contract NounsAuctionHouseV2 is nounId: currentId }); ++actualCount; - --currentId; + ++currentId; } if (settlements.length > actualCount) { diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 59e312c7e9..739a96b9b4 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -332,19 +332,19 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { lastBidTime = bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Settlement[] memory prices = auction.prices(4, 0); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(0, 5); // lastest ID 4 has no settlement data, so it's not included in the result assertEq(prices.length, 3); - assertEq(prices[0].blockTimestamp, uint32(lastBidTime)); - assertEq(prices[0].nounId, 3); - assertEq(prices[0].amount, 3e10); - assertEq(prices[0].winner, makeAddr('3')); + assertEq(prices[0].nounId, 1); + assertEq(prices[0].amount, 1e10); + assertEq(prices[0].winner, makeAddr('1')); assertEq(prices[1].nounId, 2); assertEq(prices[1].amount, 2e10); assertEq(prices[1].winner, makeAddr('2')); - assertEq(prices[2].nounId, 1); - assertEq(prices[2].amount, 1e10); - assertEq(prices[2].winner, makeAddr('1')); + assertEq(prices[2].blockTimestamp, uint32(lastBidTime)); + assertEq(prices[2].nounId, 3); + assertEq(prices[2].amount, 3e10); + assertEq(prices[2].winner, makeAddr('3')); } function test_prices_withRange_givenSmallerRangeThanAuctionsReturnsAuctions() public { @@ -353,20 +353,20 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Settlement[] memory prices = auction.prices(11, 6); + INounsAuctionHouse.Settlement[] memory prices = auction.prices(7, 12); assertEq(prices.length, 4); - assertEq(prices[0].nounId, 11); - assertEq(prices[0].amount, 10e10); - assertEq(prices[0].winner, makeAddr('10')); - assertEq(prices[1].nounId, 9); - assertEq(prices[1].amount, 9e10); - assertEq(prices[1].winner, makeAddr('9')); - assertEq(prices[2].nounId, 8); - assertEq(prices[2].amount, 8e10); - assertEq(prices[2].winner, makeAddr('8')); - assertEq(prices[3].nounId, 7); - assertEq(prices[3].amount, 7e10); - assertEq(prices[3].winner, makeAddr('7')); + assertEq(prices[0].nounId, 7); + assertEq(prices[0].amount, 7e10); + assertEq(prices[0].winner, makeAddr('7')); + assertEq(prices[1].nounId, 8); + assertEq(prices[1].amount, 8e10); + assertEq(prices[1].winner, makeAddr('8')); + assertEq(prices[2].nounId, 9); + assertEq(prices[2].amount, 9e10); + assertEq(prices[2].winner, makeAddr('9')); + assertEq(prices[3].nounId, 11); + assertEq(prices[3].amount, 10e10); + assertEq(prices[3].winner, makeAddr('10')); } function test_setPrices_revertsForNonOwner() public { @@ -404,14 +404,13 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { vm.prank(auction.owner()); auction.setPrices(observations); - INounsAuctionHouse.Settlement[] memory actualObservations = auction.prices(22, 0); + INounsAuctionHouse.Settlement[] memory actualObservations = auction.prices(0, 23); assertEq(actualObservations.length, 20); for (uint256 i = 0; i < 20; ++i) { - uint256 actualIndex = 19 - i; - assertEq(observations[i].blockTimestamp, actualObservations[actualIndex].blockTimestamp); - assertEq(observations[i].amount, actualObservations[actualIndex].amount); - assertEq(observations[i].winner, actualObservations[actualIndex].winner); - assertEq(observations[i].nounId, actualObservations[actualIndex].nounId); + assertEq(observations[i].blockTimestamp, actualObservations[i].blockTimestamp); + assertEq(observations[i].amount, actualObservations[i].amount); + assertEq(observations[i].winner, actualObservations[i].winner); + assertEq(observations[i].nounId, actualObservations[i].nounId); } } } From 364b1a846929c0c5267c3b9a252a11971a474dfd Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 14:48:04 -0500 Subject: [PATCH 047/115] ah: add tests for skipping missing auction data --- .../test/foundry/NounsAuctionHouseV2.t.sol | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 739a96b9b4..138841eadd 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -325,6 +325,32 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices[1].winner, makeAddr('bidder')); } + 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); + + INounsAuctionHouse.Settlement[] memory prices = auction.prices(3); + assertEq(prices.length, 3); + assertEq(prices[0].nounId, 6); + assertEq(prices[0].amount, 3e10); + assertEq(prices[0].winner, bidder); + assertEq(prices[1].nounId, 2); + assertEq(prices[1].amount, 2e10); + assertEq(prices[1].winner, bidder); + assertEq(prices[2].nounId, 1); + assertEq(prices[2].amount, 1e10); + assertEq(prices[2].winner, bidder); + } + function test_prices_withRange_givenBiggerRangeThanAuctionsReturnsAuctionsAndZeroObservations() public { uint256 lastBidTime; for (uint256 i = 1; i <= 3; ++i) { @@ -369,6 +395,32 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices[3].winner, makeAddr('10')); } + 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); + + INounsAuctionHouse.Settlement[] memory prices = auction.prices(1, 7); + assertEq(prices.length, 3); + assertEq(prices[0].nounId, 1); + assertEq(prices[0].amount, 1e10); + assertEq(prices[0].winner, bidder); + assertEq(prices[1].nounId, 2); + assertEq(prices[1].amount, 2e10); + assertEq(prices[1].winner, bidder); + assertEq(prices[2].nounId, 6); + assertEq(prices[2].amount, 3e10); + assertEq(prices[2].winner, bidder); + } + function test_setPrices_revertsForNonOwner() public { INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.Settlement[](1); observations[0] = INounsAuctionHouse.Settlement({ From 5f131295284c791249859c1f191457f640378e5d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 15:18:41 -0500 Subject: [PATCH 048/115] ah: update user-facing oracle prices to 18 decimals for better UX --- .../contracts/NounsAuctionHouseV2.sol | 29 ++++++++--- .../interfaces/INounsAuctionHouse.sol | 4 +- .../test/foundry/NounsAuctionHouseV2.t.sol | 52 +++++++++---------- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 441ba97d1e..1fc7a720af 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -272,12 +272,18 @@ contract NounsAuctionHouseV2 is return success; } - function setPrices(Settlement[] memory observations_) external onlyOwner { - for (uint256 i = 0; i < observations_.length; ++i) { - settlementHistory[observations_[i].nounId] = SettlementState({ - blockTimestamp: observations_[i].blockTimestamp, - amount: observations_[i].amount, - winner: observations_[i].winner + /** + * @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 { + 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 }); } @@ -317,7 +323,7 @@ contract NounsAuctionHouseV2 is settlements[actualCount] = Settlement({ blockTimestamp: settlementHistory[latestNounId].blockTimestamp, - amount: settlementHistory[latestNounId].amount, + amount: uint64PriceToUint256(settlementHistory[latestNounId].amount), winner: settlementHistory[latestNounId].winner, nounId: latestNounId }); @@ -353,7 +359,7 @@ contract NounsAuctionHouseV2 is settlements[actualCount] = Settlement({ blockTimestamp: settlementHistory[currentId].blockTimestamp, - amount: settlementHistory[currentId].amount, + amount: uint64PriceToUint256(settlementHistory[currentId].amount), winner: settlementHistory[currentId].winner, nounId: currentId }); @@ -377,4 +383,11 @@ contract NounsAuctionHouseV2 is 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/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 01c87056cb..6185d6899a 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -61,8 +61,8 @@ interface INounsAuctionHouse { struct Settlement { // 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 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). diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 138841eadd..65560d36af 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -254,7 +254,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices.length, 1); assertEq(prices[0].blockTimestamp, uint32(block.timestamp)); assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1e10); + assertEq(prices[0].amount, 1 ether); assertEq(prices[0].winner, bidder); } @@ -267,7 +267,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices.length, 1); assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 18446744073709551615); + assertEq(prices[0].amount, 1844674407.3709551615 ether); assertEq(prices[0].winner, makeAddr('bidder')); } @@ -278,7 +278,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices.length, 1); assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1); + assertEq(prices[0].amount, 1 * 1e8); assertEq(prices[0].winner, makeAddr('bidder')); } @@ -296,12 +296,12 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices[11].nounId, 9); assertEq(prices[19].nounId, 1); - assertEq(prices[0].amount, 20e10); - assertEq(prices[1].amount, 19e10); - assertEq(prices[2].amount, 18e10); - assertEq(prices[10].amount, 10e10); - assertEq(prices[11].amount, 9e10); - assertEq(prices[19].amount, 1e10); + assertEq(prices[0].amount, 20 ether); + assertEq(prices[1].amount, 19 ether); + assertEq(prices[2].amount, 18 ether); + assertEq(prices[10].amount, 10 ether); + assertEq(prices[11].amount, 9 ether); + assertEq(prices[19].amount, 1 ether); } function test_prices_2AuctionsNoNewAuction_includesSettledNoun() public { @@ -317,11 +317,11 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices.length, 2); assertEq(prices[0].blockTimestamp, uint32(bid2Timestamp)); assertEq(prices[0].nounId, 2); - assertEq(prices[0].amount, 2e10); + assertEq(prices[0].amount, 2 ether); assertEq(prices[0].winner, makeAddr('bidder 2')); assertEq(prices[1].blockTimestamp, uint32(bid1Timestamp)); assertEq(prices[1].nounId, 1); - assertEq(prices[1].amount, 1e10); + assertEq(prices[1].amount, 1 ether); assertEq(prices[1].winner, makeAddr('bidder')); } @@ -341,13 +341,13 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { INounsAuctionHouse.Settlement[] memory prices = auction.prices(3); assertEq(prices.length, 3); assertEq(prices[0].nounId, 6); - assertEq(prices[0].amount, 3e10); + assertEq(prices[0].amount, 3 ether); assertEq(prices[0].winner, bidder); assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2e10); + assertEq(prices[1].amount, 2 ether); assertEq(prices[1].winner, bidder); assertEq(prices[2].nounId, 1); - assertEq(prices[2].amount, 1e10); + assertEq(prices[2].amount, 1 ether); assertEq(prices[2].winner, bidder); } @@ -362,14 +362,14 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { // lastest ID 4 has no settlement data, so it's not included in the result assertEq(prices.length, 3); assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1e10); + assertEq(prices[0].amount, 1 ether); assertEq(prices[0].winner, makeAddr('1')); assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2e10); + assertEq(prices[1].amount, 2 ether); assertEq(prices[1].winner, makeAddr('2')); assertEq(prices[2].blockTimestamp, uint32(lastBidTime)); assertEq(prices[2].nounId, 3); - assertEq(prices[2].amount, 3e10); + assertEq(prices[2].amount, 3 ether); assertEq(prices[2].winner, makeAddr('3')); } @@ -382,16 +382,16 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { INounsAuctionHouse.Settlement[] memory prices = auction.prices(7, 12); assertEq(prices.length, 4); assertEq(prices[0].nounId, 7); - assertEq(prices[0].amount, 7e10); + assertEq(prices[0].amount, 7 ether); assertEq(prices[0].winner, makeAddr('7')); assertEq(prices[1].nounId, 8); - assertEq(prices[1].amount, 8e10); + assertEq(prices[1].amount, 8 ether); assertEq(prices[1].winner, makeAddr('8')); assertEq(prices[2].nounId, 9); - assertEq(prices[2].amount, 9e10); + assertEq(prices[2].amount, 9 ether); assertEq(prices[2].winner, makeAddr('9')); assertEq(prices[3].nounId, 11); - assertEq(prices[3].amount, 10e10); + assertEq(prices[3].amount, 10 ether); assertEq(prices[3].winner, makeAddr('10')); } @@ -411,13 +411,13 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { INounsAuctionHouse.Settlement[] memory prices = auction.prices(1, 7); assertEq(prices.length, 3); assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1e10); + assertEq(prices[0].amount, 1 ether); assertEq(prices[0].winner, bidder); assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2e10); + assertEq(prices[1].amount, 2 ether); assertEq(prices[1].winner, bidder); assertEq(prices[2].nounId, 6); - assertEq(prices[2].amount, 3e10); + assertEq(prices[2].amount, 3 ether); assertEq(prices[2].winner, bidder); } @@ -425,7 +425,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.Settlement[](1); observations[0] = INounsAuctionHouse.Settlement({ blockTimestamp: uint32(block.timestamp), - amount: 42e10, + amount: 42 ether, winner: makeAddr('winner'), nounId: 3 }); @@ -445,7 +445,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { observations[i] = INounsAuctionHouse.Settlement({ blockTimestamp: uint32(nounId), - amount: uint64(nounId * 1e10), + amount: nounId * 1 ether, winner: makeAddr(vm.toString(nounId)), nounId: nounId }); From 0289762b6901d8d08e46377b4804aa7d4e8188bd Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 15:51:55 -0500 Subject: [PATCH 049/115] ah: add hardcoded max time buffer to mitigate the risk of messing up auctions if set with a very large value accidentally --- .../contracts/NounsAuctionHouseV2.sol | 5 +++++ .../contracts/interfaces/INounsAuctionHouse.sol | 2 ++ .../test/foundry/NounsAuctionHouseV2.t.sol | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 1fc7a720af..5bb6bd0534 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -38,6 +38,9 @@ contract NounsAuctionHouseV2 is ReentrancyGuardUpgradeable, OwnableUpgradeable { + /// @notice A hard-coded cap on time buffer to prevent accidental auction disabling if set with a very high value. + uint256 public constant MAX_TIME_BUFFER = 1 days; + // The Nouns ERC721 token contract INounsToken public nouns; @@ -167,6 +170,8 @@ contract NounsAuctionHouseV2 is * @dev Only callable by the owner. */ function setTimeBuffer(uint256 _timeBuffer) external override onlyOwner { + if (_timeBuffer > MAX_TIME_BUFFER) revert TimeBufferTooLarge(); + timeBuffer = _timeBuffer; emit AuctionTimeBufferUpdated(_timeBuffer); diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 6185d6899a..27ffb37947 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -98,6 +98,8 @@ interface INounsAuctionHouse { error MustSendAtLeastReservePrice(); error BidDifferenceMustBeGreaterThanMinBidIncrement(); + + error TimeBufferTooLarge(); function settleAuction() external; diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 65560d36af..a356ebedc6 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -466,3 +466,16 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } } } + +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(abi.encodeWithSelector(INounsAuctionHouse.TimeBufferTooLarge.selector)); + auction.setTimeBuffer(1 days + 1); + } +} \ No newline at end of file From 4fe9197d46d7a87edfa42fddbea7ed284b2ecda2 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 16:19:08 -0500 Subject: [PATCH 050/115] ah: add a price history admin role to make it easier for the DAO to use helper smart contracts in setting historic prices --- .../contracts/NounsAuctionHouseV2.sol | 23 ++++++++++++- .../interfaces/INounsAuctionHouse.sol | 4 +++ .../test/foundry/NounsAuctionHouseV2.t.sol | 33 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 5bb6bd0534..9d96ea9596 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -65,6 +65,9 @@ contract NounsAuctionHouseV2 is // The Nouns price feed state mapping(uint256 => SettlementState) settlementHistory; + // An additional address owner may set that can set historic prices, e.g. a helper smart contract + address public settlementHistoryAdmin; + /** * @notice Initialize the auction house and base contracts, * populate configuration values, and pause the contract. @@ -197,6 +200,17 @@ contract NounsAuctionHouseV2 is emit AuctionMinBidIncrementPercentageUpdated(_minBidIncrementPercentage); } + /** + * @notice Set the settlement history admin address. + * @dev Only callable by the owner. + * @param newSettlementHistoryAdmin the new settlement history admin address. + */ + function setSettlementHistoryAdmin(address newSettlementHistoryAdmin) external onlyOwner { + emit SettlementHistoryAdminSet(settlementHistoryAdmin, newSettlementHistoryAdmin); + + settlementHistoryAdmin = newSettlementHistoryAdmin; + } + /** * @notice Create an auction. * @dev Store the auction details in the `auction` state variable and emit an AuctionCreated event. @@ -283,7 +297,14 @@ contract NounsAuctionHouseV2 is * bit packing, to save gas. * @param settlements The list of historic prices to set. */ - function setPrices(Settlement[] memory settlements) external onlyOwner { + function setPrices(Settlement[] memory settlements) external { + address settlementHistoryAdmin_ = settlementHistoryAdmin; + if (! + (msg.sender == owner() || + (msg.sender == settlementHistoryAdmin_ && settlementHistoryAdmin_ != address(0)))) { + revert OnlyOwnerOrSettlementHistoryAdmin(); + } + for (uint256 i = 0; i < settlements.length; ++i) { settlementHistory[settlements[i].nounId] = SettlementState({ blockTimestamp: settlements[i].blockTimestamp, diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 27ffb37947..b4a8dce3a0 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -85,6 +85,8 @@ interface INounsAuctionHouse { event PriceHistoryGrown(uint32 current, uint32 next); + event SettlementHistoryAdminSet(address oldSettlementHistoryAdmin, address newSettlementHistoryAdmin); + error NounNotUpForAuction(); error AuctionExpired(); @@ -101,6 +103,8 @@ interface INounsAuctionHouse { error TimeBufferTooLarge(); + error OnlyOwnerOrSettlementHistoryAdmin(); + function settleAuction() external; function settleCurrentAndCreateNewAuction() external; diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index a356ebedc6..1c6556a9d8 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -430,7 +430,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { nounId: 3 }); - vm.expectRevert('Ownable: caller is not the owner'); + vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.OnlyOwnerOrSettlementHistoryAdmin.selector)); auction.setPrices(observations); } @@ -465,6 +465,23 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(observations[i].nounId, actualObservations[i].nounId); } } + + function test_setPrices_worksForSettlementAdmin() public { + address admin = makeAddr('settlement admin'); + vm.prank(auction.owner()); + auction.setSettlementHistoryAdmin(admin); + + INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.Settlement[](1); + observations[0] = INounsAuctionHouse.Settlement({ + blockTimestamp: uint32(block.timestamp), + amount: 42 ether, + winner: makeAddr('winner'), + nounId: 3 + }); + + vm.prank(admin); + auction.setPrices(observations); + } } contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { @@ -478,4 +495,18 @@ contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.TimeBufferTooLarge.selector)); auction.setTimeBuffer(1 days + 1); } + + function test_setSettlementHistoryAdmin_revertsForNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + auction.setSettlementHistoryAdmin(makeAddr("some admin")); + } + + function test_setSettlementHistoryAdmin_worksForOwner() public { + address admin = makeAddr('settlement admin'); + + vm.prank(auction.owner()); + auction.setSettlementHistoryAdmin(admin); + + assertEq(auction.settlementHistoryAdmin(), admin); + } } \ No newline at end of file From 03ac12499a18c267c1a2423a0fe1af3b80127f4d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 16:23:19 -0500 Subject: [PATCH 051/115] delete unused event --- .../nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol | 2 -- packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index b4a8dce3a0..5c4c79b26f 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -83,8 +83,6 @@ interface INounsAuctionHouse { event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage); - event PriceHistoryGrown(uint32 current, uint32 next); - event SettlementHistoryAdminSet(address oldSettlementHistoryAdmin, address newSettlementHistoryAdmin); error NounNotUpForAuction(); diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 1c6556a9d8..d5a28d214b 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -12,8 +12,6 @@ import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHou import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; contract NounsAuctionHouseV2TestBase is Test, DeployUtils { - event PriceHistoryGrown(uint32 current, uint32 next); - address owner = address(0x1111); address noundersDAO = address(0x2222); address minter = address(0x3333); From 487442efecd7f0b9670d35f96c11895f9dfec694 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 16:31:52 -0500 Subject: [PATCH 052/115] ah: emit event when historic prices are set --- .../contracts/NounsAuctionHouseV2.sol | 9 +++++++- .../interfaces/INounsAuctionHouse.sol | 2 ++ .../test/foundry/NounsAuctionHouseV2.t.sol | 23 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 9d96ea9596..a3ebcdc6de 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -295,6 +295,7 @@ contract NounsAuctionHouseV2 is * @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. + * @dev Can only be executed by owner or settlementHistoryAdmin. * @param settlements The list of historic prices to set. */ function setPrices(Settlement[] memory settlements) external { @@ -305,15 +306,21 @@ contract NounsAuctionHouseV2 is revert OnlyOwnerOrSettlementHistoryAdmin(); } + 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; } - // TODO emit event + emit HistoricPricesSet(nounIds, prices_); } function warmUpSettlementState(uint256[] memory nounIds) external { diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 5c4c79b26f..41050aabf5 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -85,6 +85,8 @@ interface INounsAuctionHouse { event SettlementHistoryAdminSet(address oldSettlementHistoryAdmin, address newSettlementHistoryAdmin); + event HistoricPricesSet(uint256[] nounIds, uint256[] prices); + error NounNotUpForAuction(); error AuctionExpired(); diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index d5a28d214b..b755749e08 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -12,6 +12,8 @@ import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHou 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); @@ -434,6 +436,9 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { function test_setPrices_worksForOwner() public { INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.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 @@ -441,16 +446,24 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { nounId++; } + uint256 price = nounId * 1 ether; + observations[i] = INounsAuctionHouse.Settlement({ blockTimestamp: uint32(nounId), - amount: nounId * 1 ether, + 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(observations); @@ -477,6 +490,14 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { nounId: 3 }); + uint256[] memory nounIds = new uint256[](1); + uint256[] memory prices = new uint256[](1); + nounIds[0] = 3; + prices[0] = 42 ether; + + vm.expectEmit(true, true, true, true); + emit HistoricPricesSet(nounIds, prices); + vm.prank(admin); auction.setPrices(observations); } From b58087e6c5acd55a983d3de662c2c0a9fc607fed Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 16:35:37 -0500 Subject: [PATCH 053/115] add natspec --- .../contracts/NounsAuctionHouseV2.sol | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index a3ebcdc6de..73d353d04a 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -41,31 +41,31 @@ contract NounsAuctionHouseV2 is /// @notice A hard-coded cap on time buffer to prevent accidental auction disabling if set with a very high value. uint256 public constant MAX_TIME_BUFFER = 1 days; - // The Nouns ERC721 token contract + /// @notice The Nouns ERC721 token contract INounsToken public nouns; - // The address of the WETH contract + /// @notice The address of the WETH contract address public weth; - // The minimum amount of time left in an auction after a new bid is created + /// @notice The minimum amount of time left in an auction after a new bid is created uint256 public timeBuffer; - // The minimum price accepted in an auction + /// @notice The minimum price accepted in an auction uint256 public reservePrice; - // The minimum percentage difference between the last bid amount and the current bid + /// @notice The minimum percentage difference between the last bid amount and the current bid uint8 public minBidIncrementPercentage; - // The duration of a single auction + /// @notice The duration of a single auction uint256 public duration; - // The active auction + /// @notice The active auction INounsAuctionHouse.AuctionV2 public auction; - // The Nouns price feed state + /// @notice The Nouns price feed state mapping(uint256 => SettlementState) settlementHistory; - // An additional address owner may set that can set historic prices, e.g. a helper smart contract + /// @notice An additional address owner may set that can set historic prices, e.g. a helper smart contract address public settlementHistoryAdmin; /** @@ -323,6 +323,13 @@ contract NounsAuctionHouseV2 is 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[] memory nounIds) external { for (uint256 i = 0; i < nounIds.length; ++i) { if (settlementHistory[nounIds[i]].blockTimestamp == 0) { From b2075f4da73ac9110ee9ad42a89836f57092c2b5 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 27 Sep 2023 17:23:00 -0500 Subject: [PATCH 054/115] quick POC of excess eth burn in treasury --- .../governance/NounsDAOExecutorV3.sol | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol new file mode 100644 index 0000000000..b54e5302fb --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: BSD-3-Clause + +/// @title The Nouns DAO executor and treasury, supporting DAO fork + +/** + * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * + */ + +// LICENSE +// NounsDAOExecutor2.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 +// NounsDAOExecutor2.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. +// +// +// NounsDAOExecutor2.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 + +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 {INounsAuctionHouse} from "../interfaces/INounsAuctionHouse.sol"; + +interface RocketETH { + function getExchangeRate() external view returns (uint256); +} + +interface INounsDAOV3 { + function adjustedTotalSupply() external view returns (uint256); +} + +interface INounsAuctionHouseV2 is INounsAuctionHouse { + function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); +} + +contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { + using SafeERC20 for IERC20; + using Address for address payable; + + error NotEnoughAuctionHistory(); + error RocketETHConversionRateTooLow(); + error OnlyNounsDAOExecutor(); + + 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 AuctionSet(address oldAuction, address newAuction); + event NumberOfPastAuctionsForMeanPriceSet(uint16 oldNumberOfPastAuctionsForMeanPrice, uint16 newNumberOfPastAuctionsForMeanPrice); + + 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; + INounsAuctionHouseV2 public auction; + IERC20 public stETH; + IERC20 public wETH; + IERC20 public rETH; + uint16 public numberOfPastAuctionsForMeanPrice; + + mapping(bytes32 => bool) public queuedTransactions; + + 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 setAuction(INounsAuctionHouseV2 newAuction) public { + if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); + + emit AuctionSet(address(auction), address(newAuction)); + + auction = newAuction; + } + + function setSTETH(IERC20 newSTETH) public { + if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); + + stETH = newSTETH; + } + + function setWETH(IERC20 newWETH) public { + if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); + + wETH = newWETH; + } + + function setRETH(IERC20 newRETH) public { + if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); + + rETH = newRETH; + } + + function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) public { + if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); + + emit NumberOfPastAuctionsForMeanPriceSet(numberOfPastAuctionsForMeanPrice, newNumberOfPastAuctionsForMeanPrice); + + numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; + } + + function setBurnParams(INounsAuctionHouseV2 newAuction, IERC20 newSTETH, IERC20 newWETH, IERC20 newRETH, uint16 newNumberOfPastAuctionsForMeanPrice) public { + if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); + + setAuction(newAuction); + setSTETH(newSTETH); + setWETH(newWETH); + setRETH(newRETH); + setNumberOfPastAuctionsForMeanPrice(newNumberOfPastAuctionsForMeanPrice); + } + + 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 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); + } + + function burnExcessETH() public { + payable(address(0)).sendValue(min(excessETH(), address(this).balance)); + } + + function excessETH() public view returns (uint256) { + uint256 expectedTreasuryValue = meanAuctionPrice() * INounsDAOV3(admin).adjustedTotalSupply(); + return treasuryValueInETH() - expectedTreasuryValue; + } + + function meanAuctionPrice() public view returns (uint256) { + uint16 numberOfPastAuctionsForMeanPrice_ = numberOfPastAuctionsForMeanPrice; + INounsAuctionHouseV2.Settlement[] memory settlements = auction.prices(numberOfPastAuctionsForMeanPrice_); + + if (settlements.length < numberOfPastAuctionsForMeanPrice_) revert NotEnoughAuctionHistory(); + + uint256 sum = 0; + for (uint16 i = 0; i < numberOfPastAuctionsForMeanPrice_; i++) { + sum += settlements[i].amount; + } + + return sum / numberOfPastAuctionsForMeanPrice_; + } + + function treasuryValueInETH() public view returns (uint256) { + address me = address(this); + return me.balance + stETH.balanceOf(me) + wETH.balanceOf(me) + rETHBalanceInETH(); + } + + function rETHBalanceInETH() public view returns (uint256) { + uint256 rETHConversionRate = RocketETH(address(rETH)).getExchangeRate(); + if (rETHConversionRate < 1 ether) revert RocketETHConversionRateTooLow(); + + return rETH.balanceOf(address(this)) * rETHConversionRate; + } + + /** + * @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." + ); + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } +} From 9332b223220b150296871ed45747daddd38d4f6d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 11:11:47 -0500 Subject: [PATCH 055/115] use a better rETH oracle function --- .../contracts/governance/NounsDAOExecutorV3.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol index b54e5302fb..5609a8d535 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -49,7 +49,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {INounsAuctionHouse} from "../interfaces/INounsAuctionHouse.sol"; interface RocketETH { - function getExchangeRate() external view returns (uint256); + function getEthValue(uint256 _rethAmount) external view returns (uint256); } interface INounsDAOV3 { @@ -297,10 +297,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { } function rETHBalanceInETH() public view returns (uint256) { - uint256 rETHConversionRate = RocketETH(address(rETH)).getExchangeRate(); - if (rETHConversionRate < 1 ether) revert RocketETHConversionRateTooLow(); - - return rETH.balanceOf(address(this)) * rETHConversionRate; + return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(address(this))); } /** From f6c02e4e0c88fa567c114aee9efd52b207974ff7 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 11:24:00 -0500 Subject: [PATCH 056/115] format fix --- .../contracts/NounsAuctionHouseV2.sol | 6 +++--- .../test/foundry/NounsAuctionHouseV2.t.sol | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 73d353d04a..675a74f78b 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -300,9 +300,9 @@ contract NounsAuctionHouseV2 is */ function setPrices(Settlement[] memory settlements) external { address settlementHistoryAdmin_ = settlementHistoryAdmin; - if (! - (msg.sender == owner() || - (msg.sender == settlementHistoryAdmin_ && settlementHistoryAdmin_ != address(0)))) { + if ( + !(msg.sender == owner() || (msg.sender == settlementHistoryAdmin_ && settlementHistoryAdmin_ != address(0))) + ) { revert OnlyOwnerOrSettlementHistoryAdmin(); } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index b755749e08..abc913d2ae 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -326,7 +326,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } function test_prices_givenMissingAuctionData_skipsMissingNounIDs() public { - address bidder = makeAddr("some bidder"); + address bidder = makeAddr('some bidder'); bidAndWinCurrentAuction(bidder, 1 ether); vm.startPrank(address(auction)); @@ -335,7 +335,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } vm.stopPrank(); - bidAndWinCurrentAuction(bidder, 2 ether); + bidAndWinCurrentAuction(bidder, 2 ether); bidAndWinCurrentAuction(bidder, 3 ether); INounsAuctionHouse.Settlement[] memory prices = auction.prices(3); @@ -396,7 +396,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } function test_prices_withRange_givenMissingAuctionData_skipsMissingNounIDs() public { - address bidder = makeAddr("some bidder"); + address bidder = makeAddr('some bidder'); bidAndWinCurrentAuction(bidder, 1 ether); vm.startPrank(address(auction)); @@ -405,7 +405,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { } vm.stopPrank(); - bidAndWinCurrentAuction(bidder, 2 ether); + bidAndWinCurrentAuction(bidder, 2 ether); bidAndWinCurrentAuction(bidder, 3 ether); INounsAuctionHouse.Settlement[] memory prices = auction.prices(1, 7); @@ -505,7 +505,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { function test_setTimeBuffer_revertsForNonOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); + vm.expectRevert('Ownable: caller is not the owner'); auction.setTimeBuffer(1 days); } @@ -516,16 +516,16 @@ contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { } function test_setSettlementHistoryAdmin_revertsForNonOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - auction.setSettlementHistoryAdmin(makeAddr("some admin")); + vm.expectRevert('Ownable: caller is not the owner'); + auction.setSettlementHistoryAdmin(makeAddr('some admin')); } function test_setSettlementHistoryAdmin_worksForOwner() public { address admin = makeAddr('settlement admin'); - + vm.prank(auction.owner()); auction.setSettlementHistoryAdmin(admin); assertEq(auction.settlementHistoryAdmin(), admin); } -} \ No newline at end of file +} From 385ec99f881276a2e478d59109b35096bbfd242f Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 12:56:27 -0500 Subject: [PATCH 057/115] ah: add setTimeBuffer successful test --- .../test/foundry/NounsAuctionHouseV2.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index abc913d2ae..c26a799bf2 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -515,6 +515,15 @@ contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { 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); + } + function test_setSettlementHistoryAdmin_revertsForNonOwner() public { vm.expectRevert('Ownable: caller is not the owner'); auction.setSettlementHistoryAdmin(makeAddr('some admin')); From ae7cf9e7804e4247192acbdb8d5fb9c68833b819 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 13:03:48 -0500 Subject: [PATCH 058/115] ah: change array params to calldata --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 675a74f78b..218170a825 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -298,7 +298,7 @@ contract NounsAuctionHouseV2 is * @dev Can only be executed by owner or settlementHistoryAdmin. * @param settlements The list of historic prices to set. */ - function setPrices(Settlement[] memory settlements) external { + function setPrices(Settlement[] calldata settlements) external { address settlementHistoryAdmin_ = settlementHistoryAdmin; if ( !(msg.sender == owner() || (msg.sender == settlementHistoryAdmin_ && settlementHistoryAdmin_ != address(0))) @@ -330,7 +330,7 @@ contract NounsAuctionHouseV2 is * @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[] memory nounIds) external { + function warmUpSettlementState(uint256[] calldata nounIds) external { for (uint256 i = 0; i < nounIds.length; ++i) { if (settlementHistory[nounIds[i]].blockTimestamp == 0) { settlementHistory[nounIds[i]] = SettlementState({ blockTimestamp: 1, amount: 0, winner: address(0) }); From eed1cfd59556976d7033c5fd377a1dd564506e06 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 15:15:11 -0500 Subject: [PATCH 059/115] ah: remove the feature of setting historic prices the gas cost of this naive approach is too high and alternative designs require more build time while it doesnt seem important enough at the moment we'd rather focus on shipping the minimum needed for "the burn" --- .../contracts/NounsAuctionHouseV2.sol | 46 --------- .../interfaces/INounsAuctionHouse.sol | 8 +- .../test/foundry/NounsAuctionHouseV2.t.sol | 95 ------------------- 3 files changed, 1 insertion(+), 148 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 218170a825..bbd5b11a13 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -65,9 +65,6 @@ contract NounsAuctionHouseV2 is /// @notice The Nouns price feed state mapping(uint256 => SettlementState) settlementHistory; - /// @notice An additional address owner may set that can set historic prices, e.g. a helper smart contract - address public settlementHistoryAdmin; - /** * @notice Initialize the auction house and base contracts, * populate configuration values, and pause the contract. @@ -200,17 +197,6 @@ contract NounsAuctionHouseV2 is emit AuctionMinBidIncrementPercentageUpdated(_minBidIncrementPercentage); } - /** - * @notice Set the settlement history admin address. - * @dev Only callable by the owner. - * @param newSettlementHistoryAdmin the new settlement history admin address. - */ - function setSettlementHistoryAdmin(address newSettlementHistoryAdmin) external onlyOwner { - emit SettlementHistoryAdminSet(settlementHistoryAdmin, newSettlementHistoryAdmin); - - settlementHistoryAdmin = newSettlementHistoryAdmin; - } - /** * @notice Create an auction. * @dev Store the auction details in the `auction` state variable and emit an AuctionCreated event. @@ -291,38 +277,6 @@ contract NounsAuctionHouseV2 is 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. - * @dev Can only be executed by owner or settlementHistoryAdmin. - * @param settlements The list of historic prices to set. - */ - function setPrices(Settlement[] calldata settlements) external { - address settlementHistoryAdmin_ = settlementHistoryAdmin; - if ( - !(msg.sender == owner() || (msg.sender == settlementHistoryAdmin_ && settlementHistoryAdmin_ != address(0))) - ) { - revert OnlyOwnerOrSettlementHistoryAdmin(); - } - - 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 diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 41050aabf5..85e0ebe219 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -83,10 +83,6 @@ interface INounsAuctionHouse { event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage); - event SettlementHistoryAdminSet(address oldSettlementHistoryAdmin, address newSettlementHistoryAdmin); - - event HistoricPricesSet(uint256[] nounIds, uint256[] prices); - error NounNotUpForAuction(); error AuctionExpired(); @@ -100,10 +96,8 @@ interface INounsAuctionHouse { error MustSendAtLeastReservePrice(); error BidDifferenceMustBeGreaterThanMinBidIncrement(); - - error TimeBufferTooLarge(); - error OnlyOwnerOrSettlementHistoryAdmin(); + error TimeBufferTooLarge(); function settleAuction() external; diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index c26a799bf2..367e32a5dd 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -420,87 +420,6 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices[2].amount, 3 ether); assertEq(prices[2].winner, bidder); } - - function test_setPrices_revertsForNonOwner() public { - INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.Settlement[](1); - observations[0] = INounsAuctionHouse.Settlement({ - blockTimestamp: uint32(block.timestamp), - amount: 42 ether, - winner: makeAddr('winner'), - nounId: 3 - }); - - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.OnlyOwnerOrSettlementHistoryAdmin.selector)); - auction.setPrices(observations); - } - - function test_setPrices_worksForOwner() public { - INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.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; - - observations[i] = INounsAuctionHouse.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(observations); - - INounsAuctionHouse.Settlement[] memory actualObservations = auction.prices(0, 23); - assertEq(actualObservations.length, 20); - for (uint256 i = 0; i < 20; ++i) { - assertEq(observations[i].blockTimestamp, actualObservations[i].blockTimestamp); - assertEq(observations[i].amount, actualObservations[i].amount); - assertEq(observations[i].winner, actualObservations[i].winner); - assertEq(observations[i].nounId, actualObservations[i].nounId); - } - } - - function test_setPrices_worksForSettlementAdmin() public { - address admin = makeAddr('settlement admin'); - vm.prank(auction.owner()); - auction.setSettlementHistoryAdmin(admin); - - INounsAuctionHouse.Settlement[] memory observations = new INounsAuctionHouse.Settlement[](1); - observations[0] = INounsAuctionHouse.Settlement({ - blockTimestamp: uint32(block.timestamp), - amount: 42 ether, - winner: makeAddr('winner'), - nounId: 3 - }); - - uint256[] memory nounIds = new uint256[](1); - uint256[] memory prices = new uint256[](1); - nounIds[0] = 3; - prices[0] = 42 ether; - - vm.expectEmit(true, true, true, true); - emit HistoricPricesSet(nounIds, prices); - - vm.prank(admin); - auction.setPrices(observations); - } } contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { @@ -523,18 +442,4 @@ contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { assertEq(auction.timeBuffer(), 1 days); } - - function test_setSettlementHistoryAdmin_revertsForNonOwner() public { - vm.expectRevert('Ownable: caller is not the owner'); - auction.setSettlementHistoryAdmin(makeAddr('some admin')); - } - - function test_setSettlementHistoryAdmin_worksForOwner() public { - address admin = makeAddr('settlement admin'); - - vm.prank(auction.owner()); - auction.setSettlementHistoryAdmin(admin); - - assertEq(auction.settlementHistoryAdmin(), admin); - } } From f0e978bdccaff5a67db43d98eba8b6a3a8ab27f0 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 15:21:00 -0500 Subject: [PATCH 060/115] fix format --- .../governance/NounsDAOExecutorV3.sol | 130 ++++++++++++------ 1 file changed, 87 insertions(+), 43 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol index 5609a8d535..9b97f69be8 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -41,12 +41,12 @@ 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 {INounsAuctionHouse} from "../interfaces/INounsAuctionHouse.sol"; +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 { INounsAuctionHouse } from '../interfaces/INounsAuctionHouse.sol'; interface RocketETH { function getEthValue(uint256 _rethAmount) external view returns (uint256); @@ -72,20 +72,38 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { 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 + 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 + 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 + 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 AuctionSet(address oldAuction, address newAuction); - event NumberOfPastAuctionsForMeanPriceSet(uint16 oldNumberOfPastAuctionsForMeanPrice, uint16 newNumberOfPastAuctionsForMeanPrice); + event NumberOfPastAuctionsForMeanPriceSet( + uint16 oldNumberOfPastAuctionsForMeanPrice, + uint16 newNumberOfPastAuctionsForMeanPrice + ); - string public constant NAME = "NounsDAOExecutorV3"; + 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; @@ -106,17 +124,17 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { 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."); + 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."); + 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_); @@ -156,7 +174,13 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; } - function setBurnParams(INounsAuctionHouseV2 newAuction, IERC20 newSTETH, IERC20 newWETH, IERC20 newRETH, uint16 newNumberOfPastAuctionsForMeanPrice) public { + function setBurnParams( + INounsAuctionHouseV2 newAuction, + IERC20 newSTETH, + IERC20 newWETH, + IERC20 newRETH, + uint16 newNumberOfPastAuctionsForMeanPrice + ) public { if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); setAuction(newAuction); @@ -167,7 +191,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { } function acceptAdmin() public { - require(msg.sender == pendingAdmin, "NounsDAOExecutor::acceptAdmin: Call must come from pendingAdmin."); + require(msg.sender == pendingAdmin, 'NounsDAOExecutor::acceptAdmin: Call must come from pendingAdmin.'); admin = msg.sender; pendingAdmin = address(0); @@ -175,20 +199,26 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { } function setPendingAdmin(address pendingAdmin_) public { - require(msg.sender == address(this), "NounsDAOExecutor::setPendingAdmin: Call must come from NounsDAOExecutor."); + require( + msg.sender == address(this), + 'NounsDAOExecutor::setPendingAdmin: Call must come from NounsDAOExecutor.' + ); pendingAdmin = pendingAdmin_; emit NewPendingAdmin(pendingAdmin_); } - 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."); + 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." + 'NounsDAOExecutor::queueTransaction: Estimated execution block must satisfy delay.' ); bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); @@ -198,10 +228,14 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { 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."); + 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; @@ -209,19 +243,24 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { 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."); + 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." + getBlockTimestamp() >= eta, + "NounsDAOExecutor::executeTransaction: Transaction hasn't surpassed time lock." ); require( - getBlockTimestamp() <= eta + GRACE_PERIOD, "NounsDAOExecutor::executeTransaction: Transaction is stale." + getBlockTimestamp() <= eta + GRACE_PERIOD, + 'NounsDAOExecutor::executeTransaction: Transaction is stale.' ); queuedTransactions[txHash] = false; @@ -235,8 +274,8 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { } // 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."); + (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); @@ -253,15 +292,19 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { fallback() external payable {} function sendETH(address payable recipient, uint256 ethToSend) external { - require(msg.sender == admin, "NounsDAOExecutor::sendETH: Call must come from admin."); + 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."); + 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); @@ -297,7 +340,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { } function rETHBalanceInETH() public view returns (uint256) { - return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(address(this))); + return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(address(this))); } /** @@ -312,7 +355,8 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { */ function _authorizeUpgrade(address) internal view override { require( - msg.sender == address(this), "NounsDAOExecutor::_authorizeUpgrade: Call must come from NounsDAOExecutor." + msg.sender == address(this), + 'NounsDAOExecutor::_authorizeUpgrade: Call must come from NounsDAOExecutor.' ); } From 46fcf46beb7a3875fa1872fa59940449ed524d50 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 15:58:49 -0500 Subject: [PATCH 061/115] second iteration on the burn * factor logic out to a side contract * burn using selfdestruct rather than transfer to address(0) --- .../contracts/governance/ExcessETH.sol | 173 ++++++++++++++++++ .../governance/NounsDAOExecutorV3.sol | 144 ++++----------- .../contracts/interfaces/IExcessETH.sol | 22 +++ .../nouns-contracts/contracts/libs/Burn.sol | 30 +++ 4 files changed, 257 insertions(+), 112 deletions(-) create mode 100644 packages/nouns-contracts/contracts/governance/ExcessETH.sol create mode 100644 packages/nouns-contracts/contracts/interfaces/IExcessETH.sol create mode 100644 packages/nouns-contracts/contracts/libs/Burn.sol diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol new file mode 100644 index 0000000000..809724d9df --- /dev/null +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title A helpder contract for calculating Nouns excess ETH + +/** + * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * + */ + +pragma solidity ^0.8.19; + +import { IExcessETH } from '../interfaces/IExcessETH.sol'; +import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; +import { UUPSUpgradeable } from '@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { INounsAuctionHouse } from '../interfaces/INounsAuctionHouse.sol'; + +interface RocketETH { + function getEthValue(uint256 _rethAmount) external view returns (uint256); +} + +interface INounsDAOV3 { + function adjustedTotalSupply() external view returns (uint256); +} + +interface INounsAuctionHouseV2 is INounsAuctionHouse { + function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); +} + +contract ExcessETH is IExcessETH, OwnableUpgradeable, UUPSUpgradeable { + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * ERRORS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + error NotEnoughAuctionHistory(); + + error RocketETHConversionRateTooLow(); + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * EVENTS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + event AuctionSet(address oldAuction, address newAuction); + event NumberOfPastAuctionsForMeanPriceSet( + uint16 oldNumberOfPastAuctionsForMeanPrice, + uint16 newNumberOfPastAuctionsForMeanPrice + ); + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * STATE + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + INounsDAOV3 public dao; + INounsAuctionHouseV2 public auction; + uint16 public numberOfPastAuctionsForMeanPrice; + IERC20 public wETH; + IERC20 public stETH; + IERC20 public rETH; + + function initialize( + address owner_, + INounsDAOV3 dao_, + INounsAuctionHouseV2 auction_, + uint16 numberOfPastAuctionsForMeanPrice_, + IERC20 wETH_, + IERC20 stETH_, + IERC20 rETH_ + ) external initializer { + _transferOwnership(owner_); + + dao = dao_; + auction = auction_; + numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; + wETH = wETH_; + stETH = stETH_; + rETH = rETH_; + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * PUBLIC FUNCTIONS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + function excessETH() public view returns (uint256) { + uint256 expectedTreasuryValue = meanAuctionPrice() * dao.adjustedTotalSupply(); + return min(treasuryValueInETH() - expectedTreasuryValue, owner().balance); + } + + function meanAuctionPrice() public view returns (uint256) { + uint16 numberOfPastAuctionsForMeanPrice_ = numberOfPastAuctionsForMeanPrice; + INounsAuctionHouseV2.Settlement[] memory settlements = auction.prices(numberOfPastAuctionsForMeanPrice_); + + if (settlements.length < numberOfPastAuctionsForMeanPrice_) revert NotEnoughAuctionHistory(); + + uint256 sum = 0; + for (uint16 i = 0; i < numberOfPastAuctionsForMeanPrice_; i++) { + sum += settlements[i].amount; + } + + return sum / numberOfPastAuctionsForMeanPrice_; + } + + function treasuryValueInETH() public view returns (uint256) { + address owner_ = owner(); + return owner_.balance + stETH.balanceOf(owner_) + wETH.balanceOf(owner_) + rETHBalanceInETH(); + } + + function rETHBalanceInETH() public view returns (uint256) { + return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(owner())); + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * OWNER FUNCTIONS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + function setAuction(INounsAuctionHouseV2 newAuction) public onlyOwner { + emit AuctionSet(address(auction), address(newAuction)); + + auction = newAuction; + } + + function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) public onlyOwner { + emit NumberOfPastAuctionsForMeanPriceSet(numberOfPastAuctionsForMeanPrice, newNumberOfPastAuctionsForMeanPrice); + + numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; + } + + function setWETH(IERC20 newWETH) public onlyOwner { + wETH = newWETH; + } + + function setSTETH(IERC20 newSTETH) public onlyOwner { + stETH = newSTETH; + } + + function setRETH(IERC20 newRETH) public onlyOwner { + rETH = newRETH; + } + + /** + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + * INTERNAL FUNCTIONS + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + */ + + /** + * @dev Reverts when `msg.sender` is not the owner of this contract; in the case of Noun DAOs it should be the + * DAO's treasury contract. + */ + function _authorizeUpgrade(address) internal view override onlyOwner {} + + 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 index 9b97f69be8..7b98830f74 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -2,8 +2,7 @@ /// @title The Nouns DAO executor and treasury, supporting DAO fork -/** - * +/********************************* * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * * ░░░░░░█████████░░█████████░░░ * @@ -14,8 +13,7 @@ * ░░░░░░█████████░░█████████░░░ * * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - * - */ + *********************************/ // LICENSE // NounsDAOExecutor2.sol is a modified version of Compound Lab's Timelock.sol: @@ -46,27 +44,14 @@ import { SafeERC20 } from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s 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 { INounsAuctionHouse } from '../interfaces/INounsAuctionHouse.sol'; - -interface RocketETH { - function getEthValue(uint256 _rethAmount) external view returns (uint256); -} - -interface INounsDAOV3 { - function adjustedTotalSupply() external view returns (uint256); -} - -interface INounsAuctionHouseV2 is INounsAuctionHouse { - function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); -} +import { IExcessETH } from '../interfaces/IExcessETH.sol'; +import { Burn } from '../libs/Burn.sol'; contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { using SafeERC20 for IERC20; using Address for address payable; - error NotEnoughAuctionHistory(); - error RocketETHConversionRateTooLow(); - error OnlyNounsDAOExecutor(); + error ExcessETHHelperNotSet(); event NewAdmin(address indexed newAdmin); event NewPendingAdmin(address indexed newPendingAdmin); @@ -97,11 +82,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { ); event ETHSent(address indexed to, uint256 amount); event ERC20Sent(address indexed to, address indexed erc20Token, uint256 amount); - event AuctionSet(address oldAuction, address newAuction); - event NumberOfPastAuctionsForMeanPriceSet( - uint16 oldNumberOfPastAuctionsForMeanPrice, - uint16 newNumberOfPastAuctionsForMeanPrice - ); + event ETHBurned(uint256 amount); string public constant NAME = 'NounsDAOExecutorV3'; @@ -113,14 +94,12 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { address public admin; address public pendingAdmin; uint256 public delay; - INounsAuctionHouseV2 public auction; - IERC20 public stETH; - IERC20 public wETH; - IERC20 public rETH; - uint16 public numberOfPastAuctionsForMeanPrice; mapping(bytes32 => bool) public queuedTransactions; + IExcessETH public excessETHHelper; + uint256 public totalETHBurned; + constructor() initializer {} function initialize(address admin_, uint256 delay_) public virtual initializer { @@ -140,56 +119,6 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { emit NewDelay(delay_); } - function setAuction(INounsAuctionHouseV2 newAuction) public { - if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); - - emit AuctionSet(address(auction), address(newAuction)); - - auction = newAuction; - } - - function setSTETH(IERC20 newSTETH) public { - if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); - - stETH = newSTETH; - } - - function setWETH(IERC20 newWETH) public { - if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); - - wETH = newWETH; - } - - function setRETH(IERC20 newRETH) public { - if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); - - rETH = newRETH; - } - - function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) public { - if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); - - emit NumberOfPastAuctionsForMeanPriceSet(numberOfPastAuctionsForMeanPrice, newNumberOfPastAuctionsForMeanPrice); - - numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; - } - - function setBurnParams( - INounsAuctionHouseV2 newAuction, - IERC20 newSTETH, - IERC20 newWETH, - IERC20 newRETH, - uint16 newNumberOfPastAuctionsForMeanPrice - ) public { - if (msg.sender != address(this)) revert OnlyNounsDAOExecutor(); - - setAuction(newAuction); - setSTETH(newSTETH); - setWETH(newWETH); - setRETH(newRETH); - setNumberOfPastAuctionsForMeanPrice(newNumberOfPastAuctionsForMeanPrice); - } - function acceptAdmin() public { require(msg.sender == pendingAdmin, 'NounsDAOExecutor::acceptAdmin: Call must come from pendingAdmin.'); admin = msg.sender; @@ -208,6 +137,14 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { emit NewPendingAdmin(pendingAdmin_); } + function setExcessETHHelper(IExcessETH excessETHHelper_) public { + require( + msg.sender == address(this), + 'NounsDAOExecutor::setExcessETHHelper: Call must come from NounsDAOExecutor.' + ); + excessETHHelper = excessETHHelper_; + } + function queueTransaction( address target, uint256 value, @@ -311,36 +248,23 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { emit ERC20Sent(recipient, erc20Token, tokensToSend); } - function burnExcessETH() public { - payable(address(0)).sendValue(min(excessETH(), address(this).balance)); - } - - function excessETH() public view returns (uint256) { - uint256 expectedTreasuryValue = meanAuctionPrice() * INounsDAOV3(admin).adjustedTotalSupply(); - return treasuryValueInETH() - expectedTreasuryValue; - } - - function meanAuctionPrice() public view returns (uint256) { - uint16 numberOfPastAuctionsForMeanPrice_ = numberOfPastAuctionsForMeanPrice; - INounsAuctionHouseV2.Settlement[] memory settlements = auction.prices(numberOfPastAuctionsForMeanPrice_); - - if (settlements.length < numberOfPastAuctionsForMeanPrice_) revert NotEnoughAuctionHistory(); - - uint256 sum = 0; - for (uint16 i = 0; i < numberOfPastAuctionsForMeanPrice_; i++) { - sum += settlements[i].amount; - } - - return sum / numberOfPastAuctionsForMeanPrice_; - } + /** + * @notice Burn excess ETH in the treasury. + * Anyone can call this function to burn excess ETH in the treasury. + * 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`). + * @dev Will revert if `excessETHHelper` is not set. + * @return The amount of ETH burned. + */ + function burnExcessETH() public returns (uint256 amount) { + if (address(excessETHHelper) == address(0)) revert ExcessETHHelperNotSet(); - function treasuryValueInETH() public view returns (uint256) { - address me = address(this); - return me.balance + stETH.balanceOf(me) + wETH.balanceOf(me) + rETHBalanceInETH(); - } + amount = excessETHHelper.excessETH(); + Burn.eth(excessETHHelper.excessETH()); - function rETHBalanceInETH() public view returns (uint256) { - return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(address(this))); + totalETHBurned += amount; + emit ETHBurned(amount); } /** @@ -359,8 +283,4 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { 'NounsDAOExecutor::_authorizeUpgrade: Call must come from NounsDAOExecutor.' ); } - - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } } diff --git a/packages/nouns-contracts/contracts/interfaces/IExcessETH.sol b/packages/nouns-contracts/contracts/interfaces/IExcessETH.sol new file mode 100644 index 0000000000..fb667c6963 --- /dev/null +++ b/packages/nouns-contracts/contracts/interfaces/IExcessETH.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Interface for ExcessETH, the helper contract for burning excess ETH + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +interface IExcessETH { + function excessETH() external view returns (uint256); +} 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))); + } +} From 0f61bc2432b8b11605b8bb41e67f7e20aa119891 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 16:02:04 -0500 Subject: [PATCH 062/115] fix natspec --- .../nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol index 7b98830f74..e8a1af5e66 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -255,7 +255,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { * which is the mean auction price in the last N auctions multiplied by adjusted total supply (see DAO logic for * more info on `adjustedTotalSupply`). * @dev Will revert if `excessETHHelper` is not set. - * @return The amount of ETH burned. + * @return amount The amount of ETH burned. */ function burnExcessETH() public returns (uint256 amount) { if (address(excessETHHelper) == address(0)) revert ExcessETHHelperNotSet(); From 44f4b3bc2e105037809108da1848d6f7e74c008a Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 16:02:13 -0500 Subject: [PATCH 063/115] excesseth: simplify --- .../contracts/governance/ExcessETH.sol | 49 +++++-------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index 809724d9df..226c3eda11 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -20,8 +20,7 @@ pragma solidity ^0.8.19; import { IExcessETH } from '../interfaces/IExcessETH.sol'; -import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; -import { UUPSUpgradeable } from '@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol'; +import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import { INounsAuctionHouse } from '../interfaces/INounsAuctionHouse.sol'; @@ -37,7 +36,7 @@ interface INounsAuctionHouseV2 is INounsAuctionHouse { function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); } -contract ExcessETH is IExcessETH, OwnableUpgradeable, UUPSUpgradeable { +contract ExcessETH is IExcessETH, Ownable { /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * ERRORS @@ -65,30 +64,30 @@ contract ExcessETH is IExcessETH, OwnableUpgradeable, UUPSUpgradeable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ - INounsDAOV3 public dao; - INounsAuctionHouseV2 public auction; + INounsDAOV3 public immutable dao; + INounsAuctionHouseV2 public immutable auction; + IERC20 public immutable wETH; + IERC20 public immutable stETH; + IERC20 public immutable rETH; uint16 public numberOfPastAuctionsForMeanPrice; - IERC20 public wETH; - IERC20 public stETH; - IERC20 public rETH; - function initialize( + constructor( address owner_, INounsDAOV3 dao_, INounsAuctionHouseV2 auction_, - uint16 numberOfPastAuctionsForMeanPrice_, IERC20 wETH_, IERC20 stETH_, - IERC20 rETH_ - ) external initializer { + IERC20 rETH_, + uint16 numberOfPastAuctionsForMeanPrice_ + ) { _transferOwnership(owner_); dao = dao_; auction = auction_; - numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; wETH = wETH_; stETH = stETH_; rETH = rETH_; + numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; } /** @@ -131,42 +130,18 @@ contract ExcessETH is IExcessETH, OwnableUpgradeable, UUPSUpgradeable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ - function setAuction(INounsAuctionHouseV2 newAuction) public onlyOwner { - emit AuctionSet(address(auction), address(newAuction)); - - auction = newAuction; - } - function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) public onlyOwner { emit NumberOfPastAuctionsForMeanPriceSet(numberOfPastAuctionsForMeanPrice, newNumberOfPastAuctionsForMeanPrice); numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; } - function setWETH(IERC20 newWETH) public onlyOwner { - wETH = newWETH; - } - - function setSTETH(IERC20 newSTETH) public onlyOwner { - stETH = newSTETH; - } - - function setRETH(IERC20 newRETH) public onlyOwner { - rETH = newRETH; - } - /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * INTERNAL FUNCTIONS * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ - /** - * @dev Reverts when `msg.sender` is not the owner of this contract; in the case of Noun DAOs it should be the - * DAO's treasury contract. - */ - function _authorizeUpgrade(address) internal view override onlyOwner {} - function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } From 7108a070efb675d63effa12a9de579a9e3573968 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 16:37:44 -0500 Subject: [PATCH 064/115] burn: add waiting period timestamp --- packages/nouns-contracts/contracts/governance/ExcessETH.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index 226c3eda11..bf534e0b89 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -69,6 +69,7 @@ contract ExcessETH is IExcessETH, Ownable { IERC20 public immutable wETH; IERC20 public immutable stETH; IERC20 public immutable rETH; + uint256 public immutable waitingPeriodEnd; uint16 public numberOfPastAuctionsForMeanPrice; constructor( @@ -78,6 +79,7 @@ contract ExcessETH is IExcessETH, Ownable { IERC20 wETH_, IERC20 stETH_, IERC20 rETH_, + uint256 waitingPeriodEnd_, uint16 numberOfPastAuctionsForMeanPrice_ ) { _transferOwnership(owner_); @@ -87,6 +89,7 @@ contract ExcessETH is IExcessETH, Ownable { wETH = wETH_; stETH = stETH_; rETH = rETH_; + waitingPeriodEnd = waitingPeriodEnd_; numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; } @@ -97,6 +100,8 @@ contract ExcessETH is IExcessETH, Ownable { */ function excessETH() public view returns (uint256) { + if (block.timestamp < waitingPeriodEnd) return 0; + uint256 expectedTreasuryValue = meanAuctionPrice() * dao.adjustedTotalSupply(); return min(treasuryValueInETH() - expectedTreasuryValue, owner().balance); } From 083d21a4bb6101fe1dd9a621127cdebde22fac09 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 16:51:55 -0500 Subject: [PATCH 065/115] revert if no excess eth and burn is called --- .../contracts/governance/NounsDAOExecutorV3.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol index e8a1af5e66..14fe5145a7 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -52,6 +52,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { using Address for address payable; error ExcessETHHelperNotSet(); + error NoExcessToBurn(); event NewAdmin(address indexed newAdmin); event NewPendingAdmin(address indexed newPendingAdmin); @@ -261,9 +262,11 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { if (address(excessETHHelper) == address(0)) revert ExcessETHHelperNotSet(); amount = excessETHHelper.excessETH(); - Burn.eth(excessETHHelper.excessETH()); + if (amount == 0) revert NoExcessToBurn(); + Burn.eth(amount); totalETHBurned += amount; + emit ETHBurned(amount); } From 0042e7db3eacc220776e78ab934288b28d789fc4 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 17:24:27 -0500 Subject: [PATCH 066/115] burn: started testing ExcessETH --- .../test/foundry/governance/ExcessETH.t.sol | 103 ++++++++++++++++++ .../foundry/helpers/DeployUtilsExcessETH.sol | 43 ++++++++ .../test/foundry/helpers/ERC20Mock.sol | 16 +++ 3 files changed, 162 insertions(+) create mode 100644 packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol create mode 100644 packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETH.sol diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol new file mode 100644 index 0000000000..552d8ed966 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { DeployUtilsExcessETH } from '../helpers/DeployUtilsExcessETH.sol'; +import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; +import { INounsAuctionHouse } from '../../../contracts/interfaces/INounsAuctionHouse.sol'; +import { ExcessETH, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETH.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; + + function setPrices(uint256[] memory pricesHistory_) external { + // priceHistory.length = priceHistory_.length; + // for (uint256 i = 0; i < priceHistory_.length; i++) { + // priceHistory[i] = priceHistory_[i]; + // } + pricesHistory = pricesHistory_; + } + + function prices(uint256 auctionCount) + external + view + override + returns (INounsAuctionHouse.Settlement[] memory priceHistory_) + { + priceHistory_ = new INounsAuctionHouse.Settlement[](pricesHistory.length); + for (uint256 i; i < pricesHistory.length; ++i) { + priceHistory_[i].amount = pricesHistory[i]; + } + } + + function settleAuction() external {} + + function settleCurrentAndCreateNewAuction() external {} + + function createBid(uint256 nounId) external payable {} + + function pause() external {} + + function unpause() external {} + + function setTimeBuffer(uint256 timeBuffer) external {} + + function setReservePrice(uint256 reservePrice) external {} + + function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external {} +} + +contract ExcessETHTest is DeployUtilsExcessETH { + DAOMock dao = new DAOMock(); + AuctionMock auction = new AuctionMock(); + NounsDAOExecutorV3 treasury; + ExcessETH excessETH; + + uint256 waitingPeriodEnd; + uint16 pastAuctionCount; + + function setUp() public { + waitingPeriodEnd = block.timestamp + 90 days; + pastAuctionCount = 90; + + treasury = _deployExecutorV3(address(dao)); + excessETH = _deployExcessETH(treasury, auction, waitingPeriodEnd, pastAuctionCount); + } + + function test_excessETH_beforeWaitingEnds_returnsZero() public { + vm.deal(address(treasury), 100 ether); + dao.setAdjustedTotalSupply(1); + setMeanPrice(1 ether); + + assertEq(excessETH.excessETH(), 0); + } + + function test_excessETH_afterWaitingEnds_justETHInTreasury_works() public { + vm.deal(address(treasury), 100 ether); + dao.setAdjustedTotalSupply(1); + setMeanPrice(1 ether); + vm.warp(waitingPeriodEnd + 1); + + assertEq(excessETH.excessETH(), 99 ether); + } + + function setMeanPrice(uint256 meanPrice) internal { + uint256[] memory prices = new uint256[](pastAuctionCount); + for (uint256 i = 0; i < pastAuctionCount; i++) { + prices[i] = meanPrice; + } + auction.setPrices(prices); + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETH.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETH.sol new file mode 100644 index 0000000000..3363385354 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETH.sol @@ -0,0 +1,43 @@ +// 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 { ExcessETH, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETH.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 DeployUtilsExcessETH 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 _deployExcessETH( + NounsDAOExecutorV3 owner, + INounsAuctionHouseV2 auction, + uint256 waitingPeriodEnd, + uint16 pastAuctionCount + ) internal returns (ExcessETH excessETH) { + WETH weth = new WETH(); + ERC20Mock stETH = new ERC20Mock(); + RocketETHMock rETH = new RocketETHMock(); + + excessETH = new ExcessETH( + address(owner), + INounsDAOV3(owner.admin()), + auction, + IERC20(address(weth)), + stETH, + rETH, + waitingPeriodEnd, + pastAuctionCount + ); + } +} 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; + } +} From 16981af87465571c7c09f56bf1f3186a25dfcd86 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 17:44:52 -0500 Subject: [PATCH 067/115] burn: handle expectedValue > current --- .../nouns-contracts/contracts/governance/ExcessETH.sol | 6 +++++- .../test/foundry/governance/ExcessETH.t.sol | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index bf534e0b89..bd6ffcb90f 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -103,7 +103,11 @@ contract ExcessETH is IExcessETH, Ownable { if (block.timestamp < waitingPeriodEnd) return 0; uint256 expectedTreasuryValue = meanAuctionPrice() * dao.adjustedTotalSupply(); - return min(treasuryValueInETH() - expectedTreasuryValue, owner().balance); + uint256 treasuryValue = treasuryValueInETH(); + + if (expectedTreasuryValue >= treasuryValue) return 0; + + return min(treasuryValue - expectedTreasuryValue, owner().balance); } function meanAuctionPrice() public view returns (uint256) { diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol index 552d8ed966..613f4d1d4b 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -93,6 +93,15 @@ contract ExcessETHTest is DeployUtilsExcessETH { assertEq(excessETH.excessETH(), 99 ether); } + function test_excessETH_expectedValueGreaterThanTreasury_returnsZero() public { + vm.deal(address(treasury), 10 ether); + dao.setAdjustedTotalSupply(100); + setMeanPrice(1 ether); + vm.warp(waitingPeriodEnd + 1); + + assertEq(excessETH.excessETH(), 0); + } + function setMeanPrice(uint256 meanPrice) internal { uint256[] memory prices = new uint256[](pastAuctionCount); for (uint256 i = 0; i < pastAuctionCount; i++) { From f8348279815cc896f2f0c992096eb9ccfd0bcdfc Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 28 Sep 2023 17:50:56 -0500 Subject: [PATCH 068/115] add excess eth test --- .../test/foundry/governance/ExcessETH.t.sol | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol index 613f4d1d4b..471e1b9030 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -6,6 +6,7 @@ import { DeployUtilsExcessETH } from '../helpers/DeployUtilsExcessETH.sol'; import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; import { INounsAuctionHouse } from '../../../contracts/interfaces/INounsAuctionHouse.sol'; import { ExcessETH, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETH.sol'; +import { ERC20Mock, RocketETHMock } from '../helpers/ERC20Mock.sol'; contract DAOMock { uint256 adjustedSupply; @@ -23,19 +24,10 @@ contract AuctionMock is INounsAuctionHouseV2 { uint256[] pricesHistory; function setPrices(uint256[] memory pricesHistory_) external { - // priceHistory.length = priceHistory_.length; - // for (uint256 i = 0; i < priceHistory_.length; i++) { - // priceHistory[i] = priceHistory_[i]; - // } pricesHistory = pricesHistory_; } - function prices(uint256 auctionCount) - external - view - override - returns (INounsAuctionHouse.Settlement[] memory priceHistory_) - { + function prices(uint256) external view override returns (INounsAuctionHouse.Settlement[] memory priceHistory_) { priceHistory_ = new INounsAuctionHouse.Settlement[](pricesHistory.length); for (uint256 i; i < pricesHistory.length; ++i) { priceHistory_[i].amount = pricesHistory[i]; @@ -102,6 +94,16 @@ contract ExcessETHTest is DeployUtilsExcessETH { assertEq(excessETH.excessETH(), 0); } + function test_excessETH_excessGreaterThanBalance_returnsBalance() public { + vm.deal(address(treasury), 1 ether); + dao.setAdjustedTotalSupply(1); + setMeanPrice(1 ether); + vm.warp(waitingPeriodEnd + 1); + ERC20Mock(address(excessETH.stETH())).mint(address(treasury), 100 ether); + + assertEq(excessETH.excessETH(), 1 ether); + } + function setMeanPrice(uint256 meanPrice) internal { uint256[] memory prices = new uint256[](pastAuctionCount); for (uint256 i = 0; i < pastAuctionCount; i++) { From f1a18fd10870c94ef90a28b6bbecbf176ef8489e Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 08:30:36 -0500 Subject: [PATCH 069/115] minor refactor and some natspec --- .../contracts/governance/ExcessETH.sol | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index bd6ffcb90f..4686fa60e1 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -1,7 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -/// @title A helpder contract for calculating Nouns excess ETH - /** * * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * @@ -36,6 +34,11 @@ interface INounsAuctionHouseV2 is INounsAuctionHouse { function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); } +/** + * @title ExcessETH Helper + * @notice A helpder contract for calculating Nouns excess ETH, used by NounsDAOExecutorV3 to burn excess ETH. + * @dev Owner is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. + */ contract ExcessETH is IExcessETH, Ownable { /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ @@ -44,8 +47,8 @@ contract ExcessETH is IExcessETH, Ownable { */ error NotEnoughAuctionHistory(); - error RocketETHConversionRateTooLow(); + /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * EVENTS @@ -102,7 +105,7 @@ contract ExcessETH is IExcessETH, Ownable { function excessETH() public view returns (uint256) { if (block.timestamp < waitingPeriodEnd) return 0; - uint256 expectedTreasuryValue = meanAuctionPrice() * dao.adjustedTotalSupply(); + uint256 expectedTreasuryValue = expectedTreasuryValueInETH(); uint256 treasuryValue = treasuryValueInETH(); if (expectedTreasuryValue >= treasuryValue) return 0; @@ -110,6 +113,15 @@ contract ExcessETH is IExcessETH, Ownable { return min(treasuryValue - expectedTreasuryValue, owner().balance); } + function expectedTreasuryValueInETH() public view returns (uint256) { + return meanAuctionPrice() * dao.adjustedTotalSupply(); + } + + function treasuryValueInETH() public view returns (uint256) { + address owner_ = owner(); + return owner_.balance + stETH.balanceOf(owner_) + wETH.balanceOf(owner_) + rETHBalanceInETH(); + } + function meanAuctionPrice() public view returns (uint256) { uint16 numberOfPastAuctionsForMeanPrice_ = numberOfPastAuctionsForMeanPrice; INounsAuctionHouseV2.Settlement[] memory settlements = auction.prices(numberOfPastAuctionsForMeanPrice_); @@ -124,11 +136,6 @@ contract ExcessETH is IExcessETH, Ownable { return sum / numberOfPastAuctionsForMeanPrice_; } - function treasuryValueInETH() public view returns (uint256) { - address owner_ = owner(); - return owner_.balance + stETH.balanceOf(owner_) + wETH.balanceOf(owner_) + rETHBalanceInETH(); - } - function rETHBalanceInETH() public view returns (uint256) { return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(owner())); } @@ -139,7 +146,7 @@ contract ExcessETH is IExcessETH, Ownable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ - function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) public onlyOwner { + function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) external onlyOwner { emit NumberOfPastAuctionsForMeanPriceSet(numberOfPastAuctionsForMeanPrice, newNumberOfPastAuctionsForMeanPrice); numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; From 6be9a90077d50ec487f0dd5ba2428a27acd92ad9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 08:56:06 -0500 Subject: [PATCH 070/115] cleanup --- packages/nouns-contracts/contracts/governance/ExcessETH.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index 4686fa60e1..9e235b3ef5 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -47,7 +47,6 @@ contract ExcessETH is IExcessETH, Ownable { */ error NotEnoughAuctionHistory(); - error RocketETHConversionRateTooLow(); /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ From e3228630e785c6f9acab08caa70025403bbd499d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 08:56:13 -0500 Subject: [PATCH 071/115] add tests --- .../test/foundry/governance/ExcessETH.t.sol | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol index 471e1b9030..ad083b0cdc 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -7,6 +7,7 @@ import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecut import { INounsAuctionHouse } from '../../../contracts/interfaces/INounsAuctionHouse.sol'; import { ExcessETH, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETH.sol'; import { ERC20Mock, RocketETHMock } from '../helpers/ERC20Mock.sol'; +import { WETH } from '../../../contracts/test/WETH.sol'; contract DAOMock { uint256 adjustedSupply; @@ -104,6 +105,50 @@ contract ExcessETHTest is DeployUtilsExcessETH { assertEq(excessETH.excessETH(), 1 ether); } + function test_excessETH_givenBalancesInAllERC20s_takesThemIntoAccount() public { + // expected value = 10 ETH + dao.setAdjustedTotalSupply(10); + setMeanPrice(1 ether); + vm.warp(waitingPeriodEnd + 1); + + // 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(excessETH.stETH())).mint(address(treasury), 1 ether); + + // giving 1 WETH -> excess grows to 3 ETH + WETH weth = WETH(payable(address(excessETH.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(excessETH.rETH())); + reth.setRate(2); + reth.mint(address(treasury), 1 ether); + + assertEq(excessETH.excessETH(), 5 ether); + } + + function test_excessETH_givenRecentAuctionPriceChange_expectedTreasuryValueDropsAsExpected() public { + vm.warp(waitingPeriodEnd + 1); + vm.deal(address(treasury), 100 ether); + + dao.setAdjustedTotalSupply(1); + setMeanPrice(100 ether); + + assertEq(excessETH.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(excessETH.excessETH(), 2 ether); + } + function setMeanPrice(uint256 meanPrice) internal { uint256[] memory prices = new uint256[](pastAuctionCount); for (uint256 i = 0; i < pastAuctionCount; i++) { @@ -111,4 +156,15 @@ contract ExcessETHTest is DeployUtilsExcessETH { } 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); + } } From 504590d87bb2e25729f0a9ec7060d20694da1eb4 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 09:00:12 -0500 Subject: [PATCH 072/115] add tests --- .../test/foundry/governance/ExcessETH.t.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol index ad083b0cdc..b488df9352 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -149,6 +149,20 @@ contract ExcessETHTest is DeployUtilsExcessETH { assertEq(excessETH.excessETH(), 2 ether); } + function test_setNumberOfPastAuctionsForMeanPrice_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + excessETH.setNumberOfPastAuctionsForMeanPrice(1); + } + + function test_setNumberOfPastAuctionsForMeanPrice_worksForOwner() public { + assertTrue(excessETH.numberOfPastAuctionsForMeanPrice() != 142); + + vm.prank(address(treasury)); + excessETH.setNumberOfPastAuctionsForMeanPrice(142); + + assertEq(excessETH.numberOfPastAuctionsForMeanPrice(), 142); + } + function setMeanPrice(uint256 meanPrice) internal { uint256[] memory prices = new uint256[](pastAuctionCount); for (uint256 i = 0; i < pastAuctionCount; i++) { From caa551d1aa70a43294d405aba04ce1f4ae360452 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 09:06:00 -0500 Subject: [PATCH 073/115] add test for insufficient auction history --- .../test/foundry/governance/ExcessETH.t.sol | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol index b488df9352..9adb977205 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -149,6 +149,16 @@ contract ExcessETHTest is DeployUtilsExcessETH { assertEq(excessETH.excessETH(), 2 ether); } + function test_excessETH_givenInsufficientAuctionHistory_reverts() public { + vm.warp(waitingPeriodEnd + 1); + vm.deal(address(treasury), 100 ether); + dao.setAdjustedTotalSupply(1); + setPriceHistory(1 ether, pastAuctionCount - 1); + + vm.expectRevert(abi.encodeWithSelector(ExcessETH.NotEnoughAuctionHistory.selector)); + excessETH.excessETH(); + } + function test_setNumberOfPastAuctionsForMeanPrice_revertsForNonOwner() public { vm.expectRevert('Ownable: caller is not the owner'); excessETH.setNumberOfPastAuctionsForMeanPrice(1); @@ -164,8 +174,12 @@ contract ExcessETHTest is DeployUtilsExcessETH { } function setMeanPrice(uint256 meanPrice) internal { - uint256[] memory prices = new uint256[](pastAuctionCount); - for (uint256 i = 0; i < pastAuctionCount; i++) { + 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); From 8bf4e5c6dc83801a6c6560eca61b9ab5fe076c4b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 09:08:47 -0500 Subject: [PATCH 074/115] add min past auction count constraint --- packages/nouns-contracts/contracts/governance/ExcessETH.sol | 5 +++++ .../nouns-contracts/test/foundry/governance/ExcessETH.t.sol | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index 9e235b3ef5..322b0c0331 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -47,6 +47,7 @@ contract ExcessETH is IExcessETH, Ownable { */ error NotEnoughAuctionHistory(); + error PastAuctionCountTooLow(); /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ @@ -66,6 +67,8 @@ contract ExcessETH is IExcessETH, Ownable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ + uint256 public constant MIN_PAST_AUCTIONS = 2; + INounsDAOV3 public immutable dao; INounsAuctionHouseV2 public immutable auction; IERC20 public immutable wETH; @@ -146,6 +149,8 @@ contract ExcessETH is IExcessETH, Ownable { */ function setNumberOfPastAuctionsForMeanPrice(uint16 newNumberOfPastAuctionsForMeanPrice) external onlyOwner { + if (newNumberOfPastAuctionsForMeanPrice < MIN_PAST_AUCTIONS) revert PastAuctionCountTooLow(); + emit NumberOfPastAuctionsForMeanPriceSet(numberOfPastAuctionsForMeanPrice, newNumberOfPastAuctionsForMeanPrice); numberOfPastAuctionsForMeanPrice = newNumberOfPastAuctionsForMeanPrice; diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol index 9adb977205..1f5c4f22d5 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -173,6 +173,12 @@ contract ExcessETHTest is DeployUtilsExcessETH { assertEq(excessETH.numberOfPastAuctionsForMeanPrice(), 142); } + function test_setNumberOfPastAuctionsForMeanPrice_revertsIfValueIsTooLow() public { + vm.prank(address(treasury)); + vm.expectRevert(abi.encodeWithSelector(ExcessETH.PastAuctionCountTooLow.selector)); + excessETH.setNumberOfPastAuctionsForMeanPrice(1); + } + function setMeanPrice(uint256 meanPrice) internal { setPriceHistory(meanPrice, pastAuctionCount); } From 6ab1a8e9118e7362a62974f8770bd0586ec06106 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 09:16:22 -0500 Subject: [PATCH 075/115] add natspec --- .../contracts/governance/ExcessETH.sol | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index 322b0c0331..e57084f522 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -104,6 +104,15 @@ contract ExcessETH is IExcessETH, Ownable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ + /** + * @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) { if (block.timestamp < waitingPeriodEnd) return 0; @@ -115,15 +124,28 @@ contract ExcessETH is IExcessETH, Ownable { 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(); } + /** + * @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; INounsAuctionHouseV2.Settlement[] memory settlements = auction.prices(numberOfPastAuctionsForMeanPrice_); @@ -138,6 +160,9 @@ contract ExcessETH is IExcessETH, Ownable { return sum / numberOfPastAuctionsForMeanPrice_; } + /** + * @notice Get the rETH balance of the treasury, denominated in ETH. + */ function rETHBalanceInETH() public view returns (uint256) { return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(owner())); } @@ -148,6 +173,11 @@ contract ExcessETH is IExcessETH, Ownable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ + /** + * @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(); @@ -162,6 +192,9 @@ contract ExcessETH is IExcessETH, Ownable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ + /** + * @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; } From da51dcbe7b5eb6cb01dfeb5bd38b0659fcb24b42 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 09:44:21 -0500 Subject: [PATCH 076/115] add tests for treasury v3 burn --- .../governance/NounsDAOExecutorV3.t.sol | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol 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..8acc34204e --- /dev/null +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { DeployUtilsExcessETH } from '../helpers/DeployUtilsExcessETH.sol'; +import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; +import { IExcessETH } from '../../../contracts/interfaces/IExcessETH.sol'; + +contract ExcessETHMock is IExcessETH { + uint256 excess; + + function setExcess(uint256 excess_) public { + excess = excess_; + } + + function excessETH() public view returns (uint256) { + return excess; + } +} + +contract NounsDAOExecutorV3Test is DeployUtilsExcessETH { + event ETHBurned(uint256 amount); + + NounsDAOExecutorV3 treasury; + ExcessETHMock excessETH; + + address dao = makeAddr('dao'); + + function setUp() public { + treasury = _deployExecutorV3(dao); + excessETH = new ExcessETHMock(); + + vm.prank(address(treasury)); + treasury.setExcessETHHelper(excessETH); + } + + function test_setExcessETHHelper_revertsForNonTreasury() public { + vm.expectRevert('NounsDAOExecutor::setExcessETHHelper: Call must come from NounsDAOExecutor.'); + treasury.setExcessETHHelper(excessETH); + } + + function test_setExcessETHHelper_worksForTreasury() public { + ExcessETHMock newExcessETH = new ExcessETHMock(); + + assertTrue(address(treasury.excessETHHelper()) != address(newExcessETH)); + + vm.prank(address(treasury)); + treasury.setExcessETHHelper(newExcessETH); + + assertEq(address(treasury.excessETHHelper()), address(newExcessETH)); + } + + function test_burnExcessETH_givenHelperNotSet_reverts() public { + vm.prank(address(treasury)); + treasury.setExcessETHHelper(IExcessETH(address(0))); + + vm.expectRevert(abi.encodeWithSelector(NounsDAOExecutorV3.ExcessETHHelperNotSet.selector)); + treasury.burnExcessETH(); + } + + function test_burnExcessETH_givenExcessIsZero_reverts() public { + excessETH.setExcess(0); + + vm.expectRevert(abi.encodeWithSelector(NounsDAOExecutorV3.NoExcessToBurn.selector)); + treasury.burnExcessETH(); + } + + function test_burnExcessETH_givenExcess_burnsAddsToTotalAndEmits() public { + vm.deal(address(treasury), 142 ether); + excessETH.setExcess(100 ether); + + vm.expectEmit(true, true, true, true); + emit ETHBurned(100 ether); + + treasury.burnExcessETH(); + assertEq(treasury.totalETHBurned(), 100 ether); + + excessETH.setExcess(42 ether); + + vm.expectEmit(true, true, true, true); + emit ETHBurned(42 ether); + + treasury.burnExcessETH(); + assertEq(treasury.totalETHBurned(), 142 ether); + } +} From 4d43645020471d892e3ccec710af8b579e7574c9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 11:15:09 -0500 Subject: [PATCH 077/115] add WIP excess burn test with treasury upgrade --- .../governance/NounsDAOExecutorV3.t.sol | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index 8acc34204e..2b1b36f704 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -5,6 +5,13 @@ import 'forge-std/Test.sol'; import { DeployUtilsExcessETH } from '../helpers/DeployUtilsExcessETH.sol'; import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; import { IExcessETH } from '../../../contracts/interfaces/IExcessETH.sol'; +import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; +import { NounsDAOExecutorV2 } from '../../../contracts/governance/NounsDAOExecutorV2.sol'; +import { NounsTokenLike } from '../../../contracts/governance/NounsDAOInterfaces.sol'; +import { ExcessETH, INounsAuctionHouseV2 } from '../../../contracts/governance/ExcessETH.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; +import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseV2 } from '../../../contracts/NounsAuctionHouseV2.sol'; contract ExcessETHMock is IExcessETH { uint256 excess; @@ -84,3 +91,121 @@ contract NounsDAOExecutorV3Test is DeployUtilsExcessETH { assertEq(treasury.totalETHBurned(), 142 ether); } } + +contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { + 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(); + + NounsTokenLike nouns = dao.nouns(); + + vm.startPrank(nouns.minter()); + for (uint256 i = 0; i < 2; i++) { + uint256 tokenId = nouns.mint(); + nouns.transferFrom(nouns.minter(), nouner, tokenId); + } + vm.stopPrank(); + vm.roll(block.number + 1); + } + + function test_upgardeViaProposal() public { + generateAuctionHistory(90); + + NounsDAOExecutorV3 newImpl = new NounsDAOExecutorV3(); + + vm.startPrank(nouner); + uint256 proposalId = propose(treasury, 0, 'upgradeTo(address)', abi.encode(address(newImpl))); + getProposalToExecution(proposalId); + vm.stopPrank(); + + ExcessETH excessETH = _deployExcessETH( + NounsDAOExecutorV3(treasury), + INounsAuctionHouseV2(dao.nouns().minter()), + block.timestamp + 90 days, + 90 + ); + + vm.startPrank(nouner); + proposalId = propose(treasury, 0, 'setExcessETHHelper(address)', abi.encode(address(excessETH))); + getProposalToExecution(proposalId); + vm.stopPrank(); + + vm.expectRevert(abi.encodeWithSelector(NounsDAOExecutorV3.NoExcessToBurn.selector)); + NounsDAOExecutorV3(treasury).burnExcessETH(); + + vm.warp(block.timestamp + 91 days); + vm.deal(address(treasury), 100 ether); + + NounsDAOExecutorV3(treasury).burnExcessETH(); + + // TODO assert the right amount was burned + } + + 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))); + _upgradeAuctionHouse( + treasury, + NounsAuctionHouseProxyAdmin(proxyAdminAddress), + NounsAuctionHouseProxy(payable(dao.nouns().minter())) + ); + } + + function generateAuctionHistory(uint256 count) internal { + NounsAuctionHouseV2 auction = NounsAuctionHouseV2(payable(dao.nouns().minter())); + + vm.deal(nouner, count * 0.1 ether); + for (uint256 i = 0; i < count; ++i) { + bidAndWinCurrentAuction(nouner, 0.1 ether); + } + } + + function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { + (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + vm.deal(bidder, bid); + vm.prank(bidder); + auction.createBid{ value: bid }(nounId); + vm.warp(endTime); + auction.settleCurrentAndCreateNewAuction(); + return block.timestamp; + } +} From 91ceaf466d0142f1c76b9f453cd25bf2389d6f8b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 29 Sep 2023 16:08:23 -0500 Subject: [PATCH 078/115] complete treasury upgrade and burn happy flow test --- .../governance/NounsDAOExecutorV3.t.sol | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index 2b1b36f704..a3251f0ce0 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -93,6 +93,8 @@ contract NounsDAOExecutorV3Test is DeployUtilsExcessETH { } contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { + event ETHBurned(uint256 amount); + NounsDAOLogicV3 dao; address payable treasury; NounsAuctionHouseV2 auction; @@ -108,19 +110,19 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { vm.prank(auction.owner()); auction.unpause(); - NounsTokenLike nouns = dao.nouns(); + vm.deal(nouner, 1 ether); - vm.startPrank(nouns.minter()); - for (uint256 i = 0; i < 2; i++) { - uint256 tokenId = nouns.mint(); - nouns.transferFrom(nouns.minter(), nouner, tokenId); - } - vm.stopPrank(); + // 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_upgardeViaProposal() public { - generateAuctionHistory(90); + function test_upgardeViaProposal_andBurnHappyFlow() public { + uint256 meanPrice = 0.1 ether; + generateAuctionHistory(90, meanPrice); NounsDAOExecutorV3 newImpl = new NounsDAOExecutorV3(); @@ -147,9 +149,16 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { vm.warp(block.timestamp + 91 days); vm.deal(address(treasury), 100 ether); - NounsDAOExecutorV3(treasury).burnExcessETH(); + // adjustedSupply: 103 + // meanPrice: 0.1 ether + // expected value: 103 * 0.1 = 10.3 ETH + // treasury size: 100 ETH + // excess: 100 - 10.3 = 89.7 ETH + vm.expectEmit(true, true, true, true); + emit ETHBurned(89.7 ether); - // TODO assert the right amount was burned + uint256 burnedETH = NounsDAOExecutorV3(treasury).burnExcessETH(); + assertEq(burnedETH, 89.7 ether); } function getProposalToExecution(uint256 proposalId) internal { @@ -190,12 +199,11 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { ); } - function generateAuctionHistory(uint256 count) internal { + function generateAuctionHistory(uint256 count, uint256 meanPrice) internal { NounsAuctionHouseV2 auction = NounsAuctionHouseV2(payable(dao.nouns().minter())); - - vm.deal(nouner, count * 0.1 ether); + vm.deal(nouner, count * meanPrice); for (uint256 i = 0; i < count; ++i) { - bidAndWinCurrentAuction(nouner, 0.1 ether); + bidAndWinCurrentAuction(nouner, meanPrice); } } From 23adadb59af2e22f11bcd9ae21c4718be7ec6539 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 2 Oct 2023 08:37:57 -0500 Subject: [PATCH 079/115] fix CR comments --- .../nouns-contracts/contracts/governance/ExcessETH.sol | 9 +++++---- .../test/foundry/governance/ExcessETH.t.sol | 4 ++-- .../test/foundry/governance/NounsDAOExecutorV3.t.sol | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETH.sol index e57084f522..2e60ec6c56 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETH.sol @@ -139,7 +139,7 @@ contract ExcessETH is IExcessETH, Ownable { */ function treasuryValueInETH() public view returns (uint256) { address owner_ = owner(); - return owner_.balance + stETH.balanceOf(owner_) + wETH.balanceOf(owner_) + rETHBalanceInETH(); + return owner_.balance + stETH.balanceOf(owner_) + wETH.balanceOf(owner_) + rETHBalanceInETH(owner_); } /** @@ -161,10 +161,11 @@ contract ExcessETH is IExcessETH, Ownable { } /** - * @notice Get the rETH balance of the treasury, denominated in ETH. + * @notice Get an account's rETH balance, denominated in ETH. + * @param account the account to get the rETH balance of. */ - function rETHBalanceInETH() public view returns (uint256) { - return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(owner())); + function rETHBalanceInETH(address account) public view returns (uint256) { + return RocketETH(address(rETH)).getEthValue(rETH.balanceOf(account)); } /** diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol index 1f5c4f22d5..c8bb735b54 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol @@ -155,7 +155,7 @@ contract ExcessETHTest is DeployUtilsExcessETH { dao.setAdjustedTotalSupply(1); setPriceHistory(1 ether, pastAuctionCount - 1); - vm.expectRevert(abi.encodeWithSelector(ExcessETH.NotEnoughAuctionHistory.selector)); + vm.expectRevert(ExcessETH.NotEnoughAuctionHistory.selector); excessETH.excessETH(); } @@ -175,7 +175,7 @@ contract ExcessETHTest is DeployUtilsExcessETH { function test_setNumberOfPastAuctionsForMeanPrice_revertsIfValueIsTooLow() public { vm.prank(address(treasury)); - vm.expectRevert(abi.encodeWithSelector(ExcessETH.PastAuctionCountTooLow.selector)); + vm.expectRevert(ExcessETH.PastAuctionCountTooLow.selector); excessETH.setNumberOfPastAuctionsForMeanPrice(1); } diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index a3251f0ce0..3ea5bad5e4 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -61,14 +61,14 @@ contract NounsDAOExecutorV3Test is DeployUtilsExcessETH { vm.prank(address(treasury)); treasury.setExcessETHHelper(IExcessETH(address(0))); - vm.expectRevert(abi.encodeWithSelector(NounsDAOExecutorV3.ExcessETHHelperNotSet.selector)); + vm.expectRevert(NounsDAOExecutorV3.ExcessETHHelperNotSet.selector); treasury.burnExcessETH(); } function test_burnExcessETH_givenExcessIsZero_reverts() public { excessETH.setExcess(0); - vm.expectRevert(abi.encodeWithSelector(NounsDAOExecutorV3.NoExcessToBurn.selector)); + vm.expectRevert(NounsDAOExecutorV3.NoExcessToBurn.selector); treasury.burnExcessETH(); } @@ -143,7 +143,7 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { getProposalToExecution(proposalId); vm.stopPrank(); - vm.expectRevert(abi.encodeWithSelector(NounsDAOExecutorV3.NoExcessToBurn.selector)); + vm.expectRevert(NounsDAOExecutorV3.NoExcessToBurn.selector); NounsDAOExecutorV3(treasury).burnExcessETH(); vm.warp(block.timestamp + 91 days); From 46b571bb51f2292fa9764aef553944749d9f30ba Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 2 Oct 2023 11:18:28 -0500 Subject: [PATCH 080/115] burn: update to burn every N noun mints --- .../{ExcessETH.sol => ExcessETHBurner.sol} | 69 ++++- .../governance/NounsDAOExecutorV3.sol | 27 +- .../{IExcessETH.sol => IExcessETHBurner.sol} | 4 +- .../test/foundry/governance/ExcessETH.t.sol | 204 ------------- .../foundry/governance/ExcessETHBurner.t.sol | 276 ++++++++++++++++++ .../governance/NounsDAOExecutorV3.t.sol | 115 ++++---- ...ETH.sol => DeployUtilsExcessETHBurner.sol} | 16 +- 7 files changed, 411 insertions(+), 300 deletions(-) rename packages/nouns-contracts/contracts/governance/{ExcessETH.sol => ExcessETHBurner.sol} (75%) rename packages/nouns-contracts/contracts/interfaces/{IExcessETH.sol => IExcessETHBurner.sol} (90%) delete mode 100644 packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol create mode 100644 packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol rename packages/nouns-contracts/test/foundry/helpers/{DeployUtilsExcessETH.sol => DeployUtilsExcessETHBurner.sol} (73%) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETH.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol similarity index 75% rename from packages/nouns-contracts/contracts/governance/ExcessETH.sol rename to packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index 2e60ec6c56..9fff1f702e 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETH.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -17,7 +17,7 @@ pragma solidity ^0.8.19; -import { IExcessETH } from '../interfaces/IExcessETH.sol'; +import { IExcessETHBurner } from '../interfaces/IExcessETHBurner.sol'; import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import { INounsAuctionHouse } from '../interfaces/INounsAuctionHouse.sol'; @@ -32,20 +32,28 @@ interface INounsDAOV3 { interface INounsAuctionHouseV2 is INounsAuctionHouse { function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); + + function auction() external view returns (INounsAuctionHouse.AuctionV2 memory); +} + +interface IExecutorV3 { + function burnExcessETH(uint256 amount) external; } /** - * @title ExcessETH Helper - * @notice A helpder contract for calculating Nouns excess ETH, used by NounsDAOExecutorV3 to burn excess ETH. + * @title ExcessETH Burner + * @notice A helpder contract for burning Nouns excess ETH with NounsDAOExecutorV3. * @dev Owner is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. */ -contract ExcessETH is IExcessETH, Ownable { +contract ExcessETHBurner is IExcessETHBurner, Ownable { /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * ERRORS * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ + error NotTimeToBurnYet(); + error NoExcessToBurn(); error NotEnoughAuctionHistory(); error PastAuctionCountTooLow(); @@ -56,6 +64,8 @@ contract ExcessETH is IExcessETH, Ownable { */ event AuctionSet(address oldAuction, address newAuction); + event NextBurnNounIDSet(uint128 nextBurnNounID, uint128 newNextBurnNounID); + event MinNewNounsBetweenBurnsSet(uint128 minNewNounsBetweenBurns, uint128 newMinNewNounsBetweenBurns); event NumberOfPastAuctionsForMeanPriceSet( uint16 oldNumberOfPastAuctionsForMeanPrice, uint16 newNumberOfPastAuctionsForMeanPrice @@ -74,7 +84,8 @@ contract ExcessETH is IExcessETH, Ownable { IERC20 public immutable wETH; IERC20 public immutable stETH; IERC20 public immutable rETH; - uint256 public immutable waitingPeriodEnd; + uint128 public nextBurnNounID; + uint128 public minNewNounsBetweenBurns; uint16 public numberOfPastAuctionsForMeanPrice; constructor( @@ -84,7 +95,8 @@ contract ExcessETH is IExcessETH, Ownable { IERC20 wETH_, IERC20 stETH_, IERC20 rETH_, - uint256 waitingPeriodEnd_, + uint128 burnStartNounID_, + uint128 minNewNounsBetweenBurns_, uint16 numberOfPastAuctionsForMeanPrice_ ) { _transferOwnership(owner_); @@ -94,7 +106,8 @@ contract ExcessETH is IExcessETH, Ownable { wETH = wETH_; stETH = stETH_; rETH = rETH_; - waitingPeriodEnd = waitingPeriodEnd_; + nextBurnNounID = burnStartNounID_; + minNewNounsBetweenBurns = minNewNounsBetweenBurns_; numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; } @@ -104,6 +117,26 @@ contract ExcessETH is IExcessETH, Ownable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ + /** + * @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) { + if (auction.auction().nounId < nextBurnNounID) revert NotTimeToBurnYet(); + + amount = excessETH(); + if (amount == 0) revert NoExcessToBurn(); + + IExecutorV3(owner()).burnExcessETH(amount); + + nextBurnNounID += minNewNounsBetweenBurns; + } + /** * @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, @@ -114,8 +147,6 @@ contract ExcessETH is IExcessETH, Ownable { * @dev Reverts if there is not enough auction history to calculate the mean auction price. */ function excessETH() public view returns (uint256) { - if (block.timestamp < waitingPeriodEnd) return 0; - uint256 expectedTreasuryValue = expectedTreasuryValueInETH(); uint256 treasuryValue = treasuryValueInETH(); @@ -174,6 +205,26 @@ contract ExcessETH is IExcessETH, Ownable { * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ */ + /** + * @notice Set the next Noun ID at which the burn is allowed. + * @param newNextBurnNounID The new next Noun ID at which the burn is allowed. + */ + function setNextBurnNounID(uint128 newNextBurnNounID) external onlyOwner { + emit NextBurnNounIDSet(nextBurnNounID, newNextBurnNounID); + + nextBurnNounID = newNextBurnNounID; + } + + /** + * @notice Set the minimum number of new Nouns between burns. + * @param newMinNewNounsBetweenBurns The new minimum number of new Nouns between burns. + */ + function setMinNewNounsBetweenBurns(uint128 newMinNewNounsBetweenBurns) external onlyOwner { + emit MinNewNounsBetweenBurnsSet(minNewNounsBetweenBurns, newMinNewNounsBetweenBurns); + + minNewNounsBetweenBurns = newMinNewNounsBetweenBurns; + } + /** * @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. diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol index 14fe5145a7..ecdc5dcfc2 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -44,15 +44,13 @@ import { SafeERC20 } from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s 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 { IExcessETH } from '../interfaces/IExcessETH.sol'; import { Burn } from '../libs/Burn.sol'; contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { using SafeERC20 for IERC20; using Address for address payable; - error ExcessETHHelperNotSet(); - error NoExcessToBurn(); + error OnlyExcessETHBurner(); event NewAdmin(address indexed newAdmin); event NewPendingAdmin(address indexed newPendingAdmin); @@ -83,6 +81,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { ); 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'; @@ -98,7 +97,7 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { mapping(bytes32 => bool) public queuedTransactions; - IExcessETH public excessETHHelper; + address public excessETHBurner; uint256 public totalETHBurned; constructor() initializer {} @@ -138,12 +137,15 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { emit NewPendingAdmin(pendingAdmin_); } - function setExcessETHHelper(IExcessETH excessETHHelper_) public { + function setExcessETHBurner(address newExcessETHBurner) public { require( msg.sender == address(this), - 'NounsDAOExecutor::setExcessETHHelper: Call must come from NounsDAOExecutor.' + 'NounsDAOExecutor::setExcessETHBurner: Call must come from NounsDAOExecutor.' ); - excessETHHelper = excessETHHelper_; + + emit ExcessETHBurnerSet(excessETHBurner, newExcessETHBurner); + + excessETHBurner = newExcessETHBurner; } function queueTransaction( @@ -251,18 +253,13 @@ contract NounsDAOExecutorV3 is UUPSUpgradeable, Initializable { /** * @notice Burn excess ETH in the treasury. - * Anyone can call this function to 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`). - * @dev Will revert if `excessETHHelper` is not set. - * @return amount The amount of ETH burned. */ - function burnExcessETH() public returns (uint256 amount) { - if (address(excessETHHelper) == address(0)) revert ExcessETHHelperNotSet(); - - amount = excessETHHelper.excessETH(); - if (amount == 0) revert NoExcessToBurn(); + function burnExcessETH(uint256 amount) public { + if (msg.sender != excessETHBurner) revert OnlyExcessETHBurner(); Burn.eth(amount); totalETHBurned += amount; diff --git a/packages/nouns-contracts/contracts/interfaces/IExcessETH.sol b/packages/nouns-contracts/contracts/interfaces/IExcessETHBurner.sol similarity index 90% rename from packages/nouns-contracts/contracts/interfaces/IExcessETH.sol rename to packages/nouns-contracts/contracts/interfaces/IExcessETHBurner.sol index fb667c6963..f4ba06c13e 100644 --- a/packages/nouns-contracts/contracts/interfaces/IExcessETH.sol +++ b/packages/nouns-contracts/contracts/interfaces/IExcessETHBurner.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 -/// @title Interface for ExcessETH, the helper contract for burning excess ETH +/// @title Interface for ExcessETHBurner, the helper contract for burning excess ETH /********************************* * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * @@ -17,6 +17,6 @@ pragma solidity ^0.8.19; -interface IExcessETH { +interface IExcessETHBurner { function excessETH() external view returns (uint256); } diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol deleted file mode 100644 index c8bb735b54..0000000000 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETH.t.sol +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.19; - -import 'forge-std/Test.sol'; -import { DeployUtilsExcessETH } from '../helpers/DeployUtilsExcessETH.sol'; -import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; -import { INounsAuctionHouse } from '../../../contracts/interfaces/INounsAuctionHouse.sol'; -import { ExcessETH, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETH.sol'; -import { ERC20Mock, RocketETHMock } from '../helpers/ERC20Mock.sol'; -import { WETH } from '../../../contracts/test/WETH.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; - - function setPrices(uint256[] memory pricesHistory_) external { - pricesHistory = pricesHistory_; - } - - function prices(uint256) external view override returns (INounsAuctionHouse.Settlement[] memory priceHistory_) { - priceHistory_ = new INounsAuctionHouse.Settlement[](pricesHistory.length); - for (uint256 i; i < pricesHistory.length; ++i) { - priceHistory_[i].amount = pricesHistory[i]; - } - } - - function settleAuction() external {} - - function settleCurrentAndCreateNewAuction() external {} - - function createBid(uint256 nounId) external payable {} - - function pause() external {} - - function unpause() external {} - - function setTimeBuffer(uint256 timeBuffer) external {} - - function setReservePrice(uint256 reservePrice) external {} - - function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external {} -} - -contract ExcessETHTest is DeployUtilsExcessETH { - DAOMock dao = new DAOMock(); - AuctionMock auction = new AuctionMock(); - NounsDAOExecutorV3 treasury; - ExcessETH excessETH; - - uint256 waitingPeriodEnd; - uint16 pastAuctionCount; - - function setUp() public { - waitingPeriodEnd = block.timestamp + 90 days; - pastAuctionCount = 90; - - treasury = _deployExecutorV3(address(dao)); - excessETH = _deployExcessETH(treasury, auction, waitingPeriodEnd, pastAuctionCount); - } - - function test_excessETH_beforeWaitingEnds_returnsZero() public { - vm.deal(address(treasury), 100 ether); - dao.setAdjustedTotalSupply(1); - setMeanPrice(1 ether); - - assertEq(excessETH.excessETH(), 0); - } - - function test_excessETH_afterWaitingEnds_justETHInTreasury_works() public { - vm.deal(address(treasury), 100 ether); - dao.setAdjustedTotalSupply(1); - setMeanPrice(1 ether); - vm.warp(waitingPeriodEnd + 1); - - assertEq(excessETH.excessETH(), 99 ether); - } - - function test_excessETH_expectedValueGreaterThanTreasury_returnsZero() public { - vm.deal(address(treasury), 10 ether); - dao.setAdjustedTotalSupply(100); - setMeanPrice(1 ether); - vm.warp(waitingPeriodEnd + 1); - - assertEq(excessETH.excessETH(), 0); - } - - function test_excessETH_excessGreaterThanBalance_returnsBalance() public { - vm.deal(address(treasury), 1 ether); - dao.setAdjustedTotalSupply(1); - setMeanPrice(1 ether); - vm.warp(waitingPeriodEnd + 1); - ERC20Mock(address(excessETH.stETH())).mint(address(treasury), 100 ether); - - assertEq(excessETH.excessETH(), 1 ether); - } - - function test_excessETH_givenBalancesInAllERC20s_takesThemIntoAccount() public { - // expected value = 10 ETH - dao.setAdjustedTotalSupply(10); - setMeanPrice(1 ether); - vm.warp(waitingPeriodEnd + 1); - - // 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(excessETH.stETH())).mint(address(treasury), 1 ether); - - // giving 1 WETH -> excess grows to 3 ETH - WETH weth = WETH(payable(address(excessETH.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(excessETH.rETH())); - reth.setRate(2); - reth.mint(address(treasury), 1 ether); - - assertEq(excessETH.excessETH(), 5 ether); - } - - function test_excessETH_givenRecentAuctionPriceChange_expectedTreasuryValueDropsAsExpected() public { - vm.warp(waitingPeriodEnd + 1); - vm.deal(address(treasury), 100 ether); - - dao.setAdjustedTotalSupply(1); - setMeanPrice(100 ether); - - assertEq(excessETH.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(excessETH.excessETH(), 2 ether); - } - - function test_excessETH_givenInsufficientAuctionHistory_reverts() public { - vm.warp(waitingPeriodEnd + 1); - vm.deal(address(treasury), 100 ether); - dao.setAdjustedTotalSupply(1); - setPriceHistory(1 ether, pastAuctionCount - 1); - - vm.expectRevert(ExcessETH.NotEnoughAuctionHistory.selector); - excessETH.excessETH(); - } - - function test_setNumberOfPastAuctionsForMeanPrice_revertsForNonOwner() public { - vm.expectRevert('Ownable: caller is not the owner'); - excessETH.setNumberOfPastAuctionsForMeanPrice(1); - } - - function test_setNumberOfPastAuctionsForMeanPrice_worksForOwner() public { - assertTrue(excessETH.numberOfPastAuctionsForMeanPrice() != 142); - - vm.prank(address(treasury)); - excessETH.setNumberOfPastAuctionsForMeanPrice(142); - - assertEq(excessETH.numberOfPastAuctionsForMeanPrice(), 142); - } - - function test_setNumberOfPastAuctionsForMeanPrice_revertsIfValueIsTooLow() public { - vm.prank(address(treasury)); - vm.expectRevert(ExcessETH.PastAuctionCountTooLow.selector); - excessETH.setNumberOfPastAuctionsForMeanPrice(1); - } - - 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/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol new file mode 100644 index 0000000000..3f7fd1dffa --- /dev/null +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -0,0 +1,276 @@ +// 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 { INounsAuctionHouse } from '../../../contracts/interfaces/INounsAuctionHouse.sol'; +import { ExcessETHBurner, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETHBurner.sol'; +import { ERC20Mock, RocketETHMock } from '../helpers/ERC20Mock.sol'; +import { WETH } from '../../../contracts/test/WETH.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; + + function setNounId(uint128 nounId_) external { + nounId = nounId_; + } + + function setPrices(uint256[] memory pricesHistory_) external { + pricesHistory = pricesHistory_; + } + + function prices(uint256) external view override returns (INounsAuctionHouse.Settlement[] memory priceHistory_) { + priceHistory_ = new INounsAuctionHouse.Settlement[](pricesHistory.length); + for (uint256 i; i < pricesHistory.length; ++i) { + priceHistory_[i].amount = pricesHistory[i]; + } + } + + function auction() external view returns (INounsAuctionHouse.AuctionV2 memory) { + return INounsAuctionHouse.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(uint256 timeBuffer) external {} + + function setReservePrice(uint256 reservePrice) external {} + + function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external {} +} + +contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { + DAOMock dao = new DAOMock(); + AuctionMock auction = new AuctionMock(); + NounsDAOExecutorV3 treasury; + ExcessETHBurner burner; + + uint128 burnStartNounId; + uint128 minNewNounsBetweenBurns; + uint16 pastAuctionCount; + + function setUp() public { + burnStartNounId = 1; + minNewNounsBetweenBurns = 100; + pastAuctionCount = 90; + + treasury = _deployExecutorV3(address(dao)); + burner = _deployExcessETHBurner(treasury, auction, burnStartNounId, minNewNounsBetweenBurns, pastAuctionCount); + + vm.prank(address(treasury)); + treasury.setExcessETHBurner(address(burner)); + + auction.setNounId(burnStartNounId); + } + + 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); + + uint256 burnedAmount = burner.burnExcessETH(); + + assertEq(burnedAmount, 99 ether); + assertEq(address(treasury).balance, 1 ether); + assertEq(burner.nextBurnNounID(), burnStartNounId + minNewNounsBetweenBurns); + } + + 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.nextBurnNounID(), burnStartNounId + minNewNounsBetweenBurns); + + vm.deal(address(treasury), 100 ether); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.setNounId(burner.nextBurnNounID()); + uint128 expectedNextBurnID = burner.nextBurnNounID() + minNewNounsBetweenBurns; + assertEq(burner.burnExcessETH(), 99 ether); + assertEq(burner.nextBurnNounID(), expectedNextBurnID); + } + + 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(); + } + + // setMinNewNounsBetweenBurns + + function test_setNextBurnNounID_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + burner.setNextBurnNounID(1); + } + + function test_setNextBurnNounID_worksForOwner() public { + assertTrue(burner.nextBurnNounID() != 142); + + vm.prank(address(treasury)); + burner.setNextBurnNounID(142); + + assertEq(burner.nextBurnNounID(), 142); + } + + function test_setMinNewNounsBetweenBurns_revertsForNonOwner() public { + vm.expectRevert('Ownable: caller is not the owner'); + burner.setMinNewNounsBetweenBurns(1); + } + + function test_setMinNewNounsBetweenBurns_worksForOwner() public { + assertTrue(burner.minNewNounsBetweenBurns() != 142); + + vm.prank(address(treasury)); + burner.setMinNewNounsBetweenBurns(142); + + assertEq(burner.minNewNounsBetweenBurns(), 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 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 index 3ea5bad5e4..ffde243433 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -2,97 +2,79 @@ pragma solidity ^0.8.19; import 'forge-std/Test.sol'; -import { DeployUtilsExcessETH } from '../helpers/DeployUtilsExcessETH.sol'; +import { DeployUtilsExcessETHBurner } from '../helpers/DeployUtilsExcessETHBurner.sol'; import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; -import { IExcessETH } from '../../../contracts/interfaces/IExcessETH.sol'; import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; import { NounsDAOExecutorV2 } from '../../../contracts/governance/NounsDAOExecutorV2.sol'; import { NounsTokenLike } from '../../../contracts/governance/NounsDAOInterfaces.sol'; -import { ExcessETH, INounsAuctionHouseV2 } from '../../../contracts/governance/ExcessETH.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 ExcessETHMock is IExcessETH { - uint256 excess; - - function setExcess(uint256 excess_) public { - excess = excess_; - } - - function excessETH() public view returns (uint256) { - return excess; - } -} - -contract NounsDAOExecutorV3Test is DeployUtilsExcessETH { +contract NounsDAOExecutorV3Test is DeployUtilsExcessETHBurner { event ETHBurned(uint256 amount); NounsDAOExecutorV3 treasury; - ExcessETHMock excessETH; + address burner = makeAddr('burner'); address dao = makeAddr('dao'); function setUp() public { treasury = _deployExecutorV3(dao); - excessETH = new ExcessETHMock(); vm.prank(address(treasury)); - treasury.setExcessETHHelper(excessETH); + treasury.setExcessETHBurner(burner); } - function test_setExcessETHHelper_revertsForNonTreasury() public { - vm.expectRevert('NounsDAOExecutor::setExcessETHHelper: Call must come from NounsDAOExecutor.'); - treasury.setExcessETHHelper(excessETH); + function test_setExcessETHBurner_revertsForNonTreasury() public { + vm.expectRevert('NounsDAOExecutor::setExcessETHBurner: Call must come from NounsDAOExecutor.'); + treasury.setExcessETHBurner(burner); } - function test_setExcessETHHelper_worksForTreasury() public { - ExcessETHMock newExcessETH = new ExcessETHMock(); + function test_setExcessETHBurner_worksForTreasury() public { + address newBurner = makeAddr('newBurner'); - assertTrue(address(treasury.excessETHHelper()) != address(newExcessETH)); + assertTrue(treasury.excessETHBurner() != newBurner); vm.prank(address(treasury)); - treasury.setExcessETHHelper(newExcessETH); + treasury.setExcessETHBurner(newBurner); - assertEq(address(treasury.excessETHHelper()), address(newExcessETH)); + assertEq(treasury.excessETHBurner(), newBurner); } - function test_burnExcessETH_givenHelperNotSet_reverts() public { - vm.prank(address(treasury)); - treasury.setExcessETHHelper(IExcessETH(address(0))); - - vm.expectRevert(NounsDAOExecutorV3.ExcessETHHelperNotSet.selector); - treasury.burnExcessETH(); - } - - function test_burnExcessETH_givenExcessIsZero_reverts() public { - excessETH.setExcess(0); - - vm.expectRevert(NounsDAOExecutorV3.NoExcessToBurn.selector); - treasury.burnExcessETH(); + function test_burnExcessETH_revertsForNonBurner() public { + vm.expectRevert(NounsDAOExecutorV3.OnlyExcessETHBurner.selector); + treasury.burnExcessETH(1); } - function test_burnExcessETH_givenExcess_burnsAddsToTotalAndEmits() public { + function test_burnExcessETH_worksForBurner() public { vm.deal(address(treasury), 142 ether); - excessETH.setExcess(100 ether); vm.expectEmit(true, true, true, true); - emit ETHBurned(100 ether); + emit ETHBurned(142 ether); - treasury.burnExcessETH(); - assertEq(treasury.totalETHBurned(), 100 ether); + vm.prank(burner); + treasury.burnExcessETH(142 ether); - excessETH.setExcess(42 ether); + assertEq(treasury.totalETHBurned(), 142 ether); + assertEq(address(treasury).balance, 0); + } - vm.expectEmit(true, true, true, true); - emit ETHBurned(42 ether); + function test_burnExcessETH_addsToTotalETHBurned() public { + vm.deal(address(treasury), 142 ether); - treasury.burnExcessETH(); + 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 DeployUtilsExcessETH { +contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { event ETHBurned(uint256 amount); NounsDAOLogicV3 dao; @@ -131,34 +113,42 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { getProposalToExecution(proposalId); vm.stopPrank(); - ExcessETH excessETH = _deployExcessETH( + (uint128 currentNounID, , , , , ) = auction.auction(); + + ExcessETHBurner burner = _deployExcessETHBurner( NounsDAOExecutorV3(treasury), INounsAuctionHouseV2(dao.nouns().minter()), - block.timestamp + 90 days, + currentNounID + 100, + 100, 90 ); vm.startPrank(nouner); - proposalId = propose(treasury, 0, 'setExcessETHHelper(address)', abi.encode(address(excessETH))); + proposalId = propose(treasury, 0, 'setExcessETHBurner(address)', abi.encode(address(burner))); getProposalToExecution(proposalId); vm.stopPrank(); - vm.expectRevert(NounsDAOExecutorV3.NoExcessToBurn.selector); - NounsDAOExecutorV3(treasury).burnExcessETH(); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + + auction.settleCurrentAndCreateNewAuction(); + generateAuctionHistory(100, meanPrice); + + vm.expectRevert(ExcessETHBurner.NoExcessToBurn.selector); + burner.burnExcessETH(); - vm.warp(block.timestamp + 91 days); vm.deal(address(treasury), 100 ether); - // adjustedSupply: 103 + // adjustedSupply: 214 // meanPrice: 0.1 ether - // expected value: 103 * 0.1 = 10.3 ETH + // expected value: 214 * 0.1 = 21.4 ETH // treasury size: 100 ETH - // excess: 100 - 10.3 = 89.7 ETH + // excess: 100 - 21.4 = 78.6 ETH vm.expectEmit(true, true, true, true); - emit ETHBurned(89.7 ether); + emit ETHBurned(78.6 ether); - uint256 burnedETH = NounsDAOExecutorV3(treasury).burnExcessETH(); - assertEq(burnedETH, 89.7 ether); + uint256 burnedETH = burner.burnExcessETH(); + assertEq(burnedETH, 78.6 ether); } function getProposalToExecution(uint256 proposalId) internal { @@ -200,7 +190,6 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETH { } function generateAuctionHistory(uint256 count, uint256 meanPrice) internal { - NounsAuctionHouseV2 auction = NounsAuctionHouseV2(payable(dao.nouns().minter())); vm.deal(nouner, count * meanPrice); for (uint256 i = 0; i < count; ++i) { bidAndWinCurrentAuction(nouner, meanPrice); diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETH.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol similarity index 73% rename from packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETH.sol rename to packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol index 3363385354..a57ce257f7 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETH.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.19; import 'forge-std/Test.sol'; import { DeployUtilsV3 } from './DeployUtilsV3.sol'; import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; -import { ExcessETH, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETH.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 DeployUtilsExcessETH is DeployUtilsV3 { +abstract contract DeployUtilsExcessETHBurner is DeployUtilsV3 { function _deployExecutorV3(address dao) internal returns (NounsDAOExecutorV3) { NounsDAOExecutorV3 executor = NounsDAOExecutorV3( payable(address(new ERC1967Proxy(address(new NounsDAOExecutorV3()), ''))) @@ -19,24 +19,26 @@ abstract contract DeployUtilsExcessETH is DeployUtilsV3 { return executor; } - function _deployExcessETH( + function _deployExcessETHBurner( NounsDAOExecutorV3 owner, INounsAuctionHouseV2 auction, - uint256 waitingPeriodEnd, + uint128 burnStartNounID, + uint128 minNewNounsBetweenBurns, uint16 pastAuctionCount - ) internal returns (ExcessETH excessETH) { + ) internal returns (ExcessETHBurner burner) { WETH weth = new WETH(); ERC20Mock stETH = new ERC20Mock(); RocketETHMock rETH = new RocketETHMock(); - excessETH = new ExcessETH( + burner = new ExcessETHBurner( address(owner), INounsDAOV3(owner.admin()), auction, IERC20(address(weth)), stETH, rETH, - waitingPeriodEnd, + burnStartNounID, + minNewNounsBetweenBurns, pastAuctionCount ); } From 0429ca4213048a211bc58a5c2474e1bedfed3aaa Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 3 Oct 2023 12:52:54 -0500 Subject: [PATCH 081/115] ah: more bit packing to save gas updated V1 to V2 migration implementation accordingly --- .../NounsAuctionHousePreV2Migration.sol | 74 +++++++++---- .../contracts/NounsAuctionHouseV2.sol | 75 ++++++++----- .../interfaces/INounsAuctionHouse.sol | 52 --------- .../interfaces/INounsAuctionHouseV2.sol | 101 ++++++++++++++++++ .../test/foundry/NounsAuctionHouseV2.t.sol | 88 +++++++++++---- .../test/foundry/helpers/DeployUtils.sol | 9 +- 6 files changed, 278 insertions(+), 121 deletions(-) create mode 100644 packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol diff --git a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol index eb5bb82cea..c0f796eb4f 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol @@ -15,39 +15,73 @@ * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * *********************************/ -pragma solidity ^0.8.6; +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 { - // See NounsAuctionHouse for docs on these state vars - address public nouns; - address public weth; - uint256 public timeBuffer; - uint256 public reservePrice; - uint8 public minBidIncrementPercentage; - uint256 public duration; - INounsAuctionHouse.Auction public auction; + 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; + bool paused; + } + + uint256 private startSlot; function migrate() public onlyOwner { - INounsAuctionHouse.Auction memory _auction = auction; + OldLayout storage oldLayout = _oldLayout(); + NewLayout storage newLayout = _newLayout(); + OldLayout memory oldLayoutCache = oldLayout; - // nullifying previous storage slots to avoid the risk of leftovers - auction = INounsAuctionHouse.Auction(0, 0, 0, 0, payable(address(0)), false); + // 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); - INounsAuctionHouse.AuctionV2 storage auctionV2; + // 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 + }); + newLayout.paused = paused(); + } + + function _oldLayout() internal pure returns (OldLayout storage layout) { assembly { - auctionV2.slot := auction.slot + layout.slot := startSlot.slot } + } - auctionV2.nounId = uint128(_auction.nounId); - auctionV2.amount = uint128(_auction.amount); - auctionV2.startTime = uint40(_auction.startTime); - auctionV2.endTime = uint40(_auction.endTime); - auctionV2.bidder = _auction.bidder; - auctionV2.settled = _auction.settled; + 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 index bbd5b11a13..05ac50e228 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -22,61 +22,72 @@ // AuctionHouse.sol source code Copyright Zora licensed under the GPL-3.0 license. // With modifications by Nounders DAO. -pragma solidity ^0.8.6; +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 { INounsAuctionHouse } from './interfaces/INounsAuctionHouse.sol'; +import { INounsAuctionHouseV2 } from './interfaces/INounsAuctionHouseV2.sol'; import { INounsToken } from './interfaces/INounsToken.sol'; import { IWETH } from './interfaces/IWETH.sol'; contract NounsAuctionHouseV2 is - INounsAuctionHouse, + INounsAuctionHouseV2, PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable { /// @notice A hard-coded cap on time buffer to prevent accidental auction disabling if set with a very high value. - uint256 public constant MAX_TIME_BUFFER = 1 days; + uint56 public constant MAX_TIME_BUFFER = 1 days; /// @notice The Nouns ERC721 token contract - INounsToken public nouns; + INounsToken public immutable nouns; /// @notice The address of the WETH contract - address public weth; + address public immutable weth; - /// @notice The minimum amount of time left in an auction after a new bid is created - uint256 public timeBuffer; + /// @notice The duration of a single auction + uint256 public immutable duration; /// @notice The minimum price accepted in an auction - uint256 public reservePrice; + 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 duration of a single auction - uint256 public duration; - /// @notice The active auction - INounsAuctionHouse.AuctionV2 public auction; + INounsAuctionHouseV2.AuctionV2 public auction; + + /// @notice Whether this contract is paused or not + /// @dev Replaces the state variable from PausableUpgradeable, to bit pack this bool with `auction` and save gas + bool public __paused; /// @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( - INounsToken _nouns, - address _weth, - uint256 _timeBuffer, - uint256 _reservePrice, - uint8 _minBidIncrementPercentage, - uint256 _duration + uint192 _reservePrice, + uint56 _timeBuffer, + uint8 _minBidIncrementPercentage ) external initializer { __Pausable_init(); __ReentrancyGuard_init(); @@ -84,12 +95,9 @@ contract NounsAuctionHouseV2 is _pause(); - nouns = _nouns; - weth = _weth; - timeBuffer = _timeBuffer; reservePrice = _reservePrice; + timeBuffer = _timeBuffer; minBidIncrementPercentage = _minBidIncrementPercentage; - duration = _duration; } /** @@ -113,7 +121,7 @@ contract NounsAuctionHouseV2 is * @dev This contract only accepts payment in ETH. */ function createBid(uint256 nounId) external payable override { - INounsAuctionHouse.AuctionV2 memory _auction = auction; + INounsAuctionHouseV2.AuctionV2 memory _auction = auction; if (_auction.nounId != nounId) revert NounNotUpForAuction(); if (block.timestamp >= _auction.endTime) revert AuctionExpired(); @@ -149,7 +157,8 @@ contract NounsAuctionHouseV2 is * anyone can settle an ongoing auction. */ function pause() external override onlyOwner { - _pause(); + __paused = true; + emit Paused(_msgSender()); } /** @@ -158,18 +167,26 @@ contract NounsAuctionHouseV2 is * contract is paused. If required, this function will start a new auction. */ function unpause() external override onlyOwner { - _unpause(); + __paused = false; + emit Unpaused(_msgSender()); if (auction.startTime == 0 || auction.settled) { _createAuction(); } } + /** + * @dev Get whether this contract is paused or not. + */ + function paused() public view override returns (bool) { + return __paused; + } + /** * @notice Set the auction time buffer. * @dev Only callable by the owner. */ - function setTimeBuffer(uint256 _timeBuffer) external override onlyOwner { + function setTimeBuffer(uint56 _timeBuffer) external override onlyOwner { if (_timeBuffer > MAX_TIME_BUFFER) revert TimeBufferTooLarge(); timeBuffer = _timeBuffer; @@ -181,7 +198,7 @@ contract NounsAuctionHouseV2 is * @notice Set the auction reserve price. * @dev Only callable by the owner. */ - function setReservePrice(uint256 _reservePrice) external override onlyOwner { + function setReservePrice(uint192 _reservePrice) external override onlyOwner { reservePrice = _reservePrice; emit AuctionReservePriceUpdated(_reservePrice); @@ -228,7 +245,7 @@ contract NounsAuctionHouseV2 is * @dev If there are no bids, the Noun is burned. */ function _settleAuction() internal { - INounsAuctionHouse.AuctionV2 memory _auction = auction; + INounsAuctionHouseV2.AuctionV2 memory _auction = auction; if (_auction.startTime == 0) revert AuctionHasntBegun(); if (_auction.settled) revert AuctionAlreadySettled(); diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol index 85e0ebe219..33d37c9254 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouse.sol @@ -33,42 +33,6 @@ interface INounsAuctionHouse { bool settled; } - 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). - // TODO update accuracy - 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); @@ -83,22 +47,6 @@ interface INounsAuctionHouse { event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage); - error NounNotUpForAuction(); - - error AuctionExpired(); - - error AuctionHasntBegun(); - - error AuctionAlreadySettled(); - - error AuctionNotDone(); - - error MustSendAtLeastReservePrice(); - - error BidDifferenceMustBeGreaterThanMinBidIncrement(); - - error TimeBufferTooLarge(); - function settleAuction() external; function settleCurrentAndCreateNewAuction() external; diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol new file mode 100644 index 0000000000..299defadf7 --- /dev/null +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0 + +/// @title Interface for Noun Auction Houses V2 + +/********************************* + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░██░░░████░░██░░░████░░░ * + * ░░██████░░░████████░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░██░░██░░░████░░██░░░████░░░ * + * ░░░░░░█████████░░█████████░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * + *********************************/ + +pragma solidity ^0.8.19; + +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); + + error NounNotUpForAuction(); + + error AuctionExpired(); + + error AuctionHasntBegun(); + + error AuctionAlreadySettled(); + + error AuctionNotDone(); + + error MustSendAtLeastReservePrice(); + + error BidDifferenceMustBeGreaterThanMinBidIncrement(); + + error TimeBufferTooLarge(); + + 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; +} diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 367e32a5dd..6923472479 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -6,7 +6,7 @@ import { DeployUtils } from './helpers/DeployUtils.sol'; import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; import { NounsAuctionHouse } from '../../contracts/NounsAuctionHouse.sol'; -import { INounsAuctionHouse } from '../../contracts/interfaces/INounsAuctionHouse.sol'; +import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHousePreV2Migration.sol'; import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; @@ -60,10 +60,10 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_revertsGivenWrongNounId() public { (uint128 nounId, , , , , ) = auction.auction(); - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.NounNotUpForAuction.selector)); + vm.expectRevert(INounsAuctionHouseV2.NounNotUpForAuction.selector); auction.createBid(nounId - 1); - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.NounNotUpForAuction.selector)); + vm.expectRevert(INounsAuctionHouseV2.NounNotUpForAuction.selector); auction.createBid(nounId + 1); } @@ -71,7 +71,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { (uint128 nounId, , , uint40 endTime, , ) = auction.auction(); vm.warp(endTime + 1); - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.AuctionExpired.selector)); + vm.expectRevert(INounsAuctionHouseV2.AuctionExpired.selector); auction.createBid(nounId); } @@ -81,7 +81,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { (uint128 nounId, , , , , ) = auction.auction(); - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.MustSendAtLeastReservePrice.selector)); + vm.expectRevert(INounsAuctionHouseV2.MustSendAtLeastReservePrice.selector); auction.createBid{ value: 0.9 ether }(nounId); } @@ -92,7 +92,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { auction.createBid{ value: 1 ether }(nounId); vm.expectRevert( - abi.encodeWithSelector(INounsAuctionHouse.BidDifferenceMustBeGreaterThanMinBidIncrement.selector) + abi.encodeWithSelector(INounsAuctionHouseV2.BidDifferenceMustBeGreaterThanMinBidIncrement.selector) ); auction.createBid{ value: 1.49 ether }(nounId); } @@ -142,7 +142,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_settleAuction_revertsWhenAuctionInProgress() public { - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.AuctionNotDone.selector)); + vm.expectRevert(INounsAuctionHouseV2.AuctionNotDone.selector); auction.settleCurrentAndCreateNewAuction(); } @@ -154,7 +154,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { auction.pause(); auction.settleAuction(); - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.AuctionAlreadySettled.selector)); + vm.expectRevert(INounsAuctionHouseV2.AuctionAlreadySettled.selector); auction.settleAuction(); } @@ -167,10 +167,21 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); auction = NounsAuctionHouseV2(address(auctionProxy)); - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.AuctionHasntBegun.selector)); + vm.expectRevert(INounsAuctionHouseV2.AuctionHasntBegun.selector); auction.settleAuction(); } + function test_settleCurrentAndCreateNewAuction_revertsWhenPaused() public { + (, , , uint40 endTime, , ) = auction.auction(); + 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, @@ -189,6 +200,9 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { vm.prank(bidder); auctionV1.createBid{ value: amount }(nounId); + address nounsBefore = address(auctionV1.nouns()); + address wethBefore = address(auctionV1.weth()); + _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); @@ -207,6 +221,42 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { assertEq(endTimeV2, endTime); assertEq(bidderV2, bidder); assertEq(settledV2, 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(); + + _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); + assertEq(auctionV2.paused(), true); } function test_auctionGetter_compatibleWithV1() public { @@ -249,7 +299,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { address bidder = address(0x4444); bidAndWinCurrentAuction(bidder, 1 ether); - INounsAuctionHouse.Settlement[] memory prices = auction.prices(2); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(2); assertEq(prices.length, 1); assertEq(prices[0].blockTimestamp, uint32(block.timestamp)); @@ -263,7 +313,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { // at 10 decimal points it's 1844674407.3709551615 bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551615999999 ether); - INounsAuctionHouse.Settlement[] memory prices = auction.prices(1); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(1); assertEq(prices.length, 1); assertEq(prices[0].nounId, 1); @@ -274,7 +324,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { function test_prices_overflowsGracefullyOverUint64MaxValue() public { bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551617 ether); - INounsAuctionHouse.Settlement[] memory prices = auction.prices(1); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(1); assertEq(prices.length, 1); assertEq(prices[0].nounId, 1); @@ -288,7 +338,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Settlement[] memory prices = auction.prices(20); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(20); assertEq(prices[0].nounId, 22); assertEq(prices[1].nounId, 21); assertEq(prices[2].nounId, 19); @@ -312,7 +362,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { auction.pause(); auction.settleAuction(); - INounsAuctionHouse.Settlement[] memory prices = auction.prices(2); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(2); assertEq(prices.length, 2); assertEq(prices[0].blockTimestamp, uint32(bid2Timestamp)); @@ -338,7 +388,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, 2 ether); bidAndWinCurrentAuction(bidder, 3 ether); - INounsAuctionHouse.Settlement[] memory prices = auction.prices(3); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(3); assertEq(prices.length, 3); assertEq(prices[0].nounId, 6); assertEq(prices[0].amount, 3 ether); @@ -358,7 +408,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { lastBidTime = bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Settlement[] memory prices = auction.prices(0, 5); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(0, 5); // lastest ID 4 has no settlement data, so it's not included in the result assertEq(prices.length, 3); assertEq(prices[0].nounId, 1); @@ -379,7 +429,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouse.Settlement[] memory prices = auction.prices(7, 12); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(7, 12); assertEq(prices.length, 4); assertEq(prices[0].nounId, 7); assertEq(prices[0].amount, 7 ether); @@ -408,7 +458,7 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, 2 ether); bidAndWinCurrentAuction(bidder, 3 ether); - INounsAuctionHouse.Settlement[] memory prices = auction.prices(1, 7); + INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(1, 7); assertEq(prices.length, 3); assertEq(prices[0].nounId, 1); assertEq(prices[0].amount, 1 ether); @@ -430,7 +480,7 @@ contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { function test_setTimeBuffer_revertsGivenValueAboveMax() public { vm.prank(auction.owner()); - vm.expectRevert(abi.encodeWithSelector(INounsAuctionHouse.TimeBufferTooLarge.selector)); + vm.expectRevert(INounsAuctionHouseV2.TimeBufferTooLarge.selector); auction.setTimeBuffer(1 days + 1); } diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index e2df178cdf..5cc79200eb 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -69,7 +69,13 @@ abstract contract DeployUtils is Test, DescriptorHelpers { NounsAuctionHouseProxyAdmin proxyAdmin, NounsAuctionHouseProxy proxy ) internal { - NounsAuctionHouseV2 newLogic = new NounsAuctionHouseV2(); + NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(proxy)); + + NounsAuctionHouseV2 newLogic = new NounsAuctionHouseV2( + auctionV1.nouns(), + auctionV1.weth(), + auctionV1.duration() + ); NounsAuctionHousePreV2Migration migratorLogic = new NounsAuctionHousePreV2Migration(); vm.startPrank(owner); @@ -84,6 +90,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { vm.stopPrank(); } + uint32 constant LAST_MINUTE_BLOCKS = 10; uint32 constant OBJECTION_PERIOD_BLOCKS = 10; uint32 constant UPDATABLE_PERIOD_BLOCKS = 10; From a62537a66f8a40d030c2503654c13a8b87c1a226 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 3 Oct 2023 12:53:17 -0500 Subject: [PATCH 082/115] rewrite AHV2 logic deployment using foundry --- .../script/DeployAuctionHouseV2.s.sol | 26 +++++++++++ .../tasks/deploy-auctionhouse-v2-logic.ts | 46 ------------------- packages/nouns-contracts/tasks/index.ts | 1 - 3 files changed, 26 insertions(+), 47 deletions(-) create mode 100644 packages/nouns-contracts/script/DeployAuctionHouseV2.s.sol delete mode 100644 packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts diff --git a/packages/nouns-contracts/script/DeployAuctionHouseV2.s.sol b/packages/nouns-contracts/script/DeployAuctionHouseV2.s.sol new file mode 100644 index 0000000000..b5b6ee7d3e --- /dev/null +++ b/packages/nouns-contracts/script/DeployAuctionHouseV2.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'; + +contract DeployAuctionHouseV2 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/tasks/deploy-auctionhouse-v2-logic.ts b/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts deleted file mode 100644 index c45f7747ae..0000000000 --- a/packages/nouns-contracts/tasks/deploy-auctionhouse-v2-logic.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { task } from 'hardhat/config'; -import promptjs from 'prompt'; -import { - getDeploymentConfirmationWithPrompt, - getGasPriceWithPrompt, - printEstimatedCost, -} from './utils'; - -promptjs.colors = false; -promptjs.message = '> '; -promptjs.delimiter = ''; - -task('deploy-auctionhouse-v2-logic', 'Deploys NounsAuctionHouseV2').setAction( - async (_args, { ethers, run }) => { - const gasPrice = await getGasPriceWithPrompt(ethers); - const contractNames = ['NounsAuctionHousePreV2Migration', 'NounsAuctionHouseV2']; - - for (const contractName of contractNames) { - const factory = await ethers.getContractFactory(contractName); - - console.log(`Estimating deployment cost for ${contractName}...`); - await printEstimatedCost(factory, gasPrice); - - const deployConfirmed = await getDeploymentConfirmationWithPrompt(); - if (!deployConfirmed) { - console.log('Exiting'); - return; - } - - console.log('Deploying...'); - const contract = await factory.deploy({ gasPrice }); - await contract.deployed(); - console.log(`Transaction hash: ${contract.deployTransaction.hash} \n`); - console.log(`${contractName} deployed to ${contract.address}`); - - await new Promise(f => setTimeout(f, 60000)); - - console.log('Verifying on Etherscan...'); - await run('verify:verify', { - address: contract.address, - constructorArguments: [], - }); - console.log('Verified'); - } - }, -); diff --git a/packages/nouns-contracts/tasks/index.ts b/packages/nouns-contracts/tasks/index.ts index 2206eb48ec..d3b95f6f8d 100644 --- a/packages/nouns-contracts/tasks/index.ts +++ b/packages/nouns-contracts/tasks/index.ts @@ -23,7 +23,6 @@ export * from './verify-etherscan-daov2'; export * from './update-configs-daov2'; export * from './deploy-short-times-daov1'; export * from './deploy-and-configure-short-times-daov1'; -export * from './deploy-auctionhouse-v2-logic'; export * from './deploy-local-dao-v3'; export * from './run-local-dao-v3'; export * from './deploy-short-times-dao-v3'; From d567ca181d05e84cd6ac91af2dc8f4148c978eff Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 3 Oct 2023 13:04:30 -0500 Subject: [PATCH 083/115] ah: add the setPrices owner function again in case the DAO wants to start the spend or burn sooner --- .../contracts/NounsAuctionHouseV2.sol | 24 ++++++++ .../interfaces/INounsAuctionHouseV2.sol | 2 + .../test/foundry/NounsAuctionHouseV2.t.sol | 56 +++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 05ac50e228..933219e6ae 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -294,6 +294,30 @@ contract NounsAuctionHouseV2 is 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 diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index 299defadf7..919fdb89db 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -67,6 +67,8 @@ interface INounsAuctionHouseV2 { event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage); + event HistoricPricesSet(uint256[] nounIds, uint256[] prices); + error NounNotUpForAuction(); error AuctionExpired(); diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 6923472479..e60229231b 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -470,6 +470,62 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices[2].amount, 3 ether); assertEq(prices[2].winner, bidder); } + + 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.prices(0, 23); + assertEq(actualSettlements.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); + } + } } contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { From 7da643539ba45ebcbc319c069208bebe16c321d9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 3 Oct 2023 13:36:31 -0500 Subject: [PATCH 084/115] excessETH: use new AH V2 interface led to updating AH to have an explicit getter so code using it doesn't receive a tuple --- .../contracts/NounsAuctionHouseV2.sol | 69 ++++++++++--------- .../contracts/governance/ExcessETHBurner.sol | 8 +-- .../interfaces/INounsAuctionHouseV2.sol | 4 ++ .../test/foundry/NounsAuctionHouseV2.t.sol | 68 ++++++++---------- .../foundry/governance/ExcessETHBurner.t.sol | 12 ++-- .../governance/NounsDAOExecutorV3.t.sol | 8 +-- 6 files changed, 82 insertions(+), 87 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 933219e6ae..1a0fd4b7b5 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -60,7 +60,7 @@ contract NounsAuctionHouseV2 is uint8 public minBidIncrementPercentage; /// @notice The active auction - INounsAuctionHouseV2.AuctionV2 public auction; + INounsAuctionHouseV2.AuctionV2 public _auction; /// @notice Whether this contract is paused or not /// @dev Replaces the state variable from PausableUpgradeable, to bit pack this bool with `auction` and save gas @@ -121,35 +121,42 @@ contract NounsAuctionHouseV2 is * @dev This contract only accepts payment in ETH. */ function createBid(uint256 nounId) external payable override { - INounsAuctionHouseV2.AuctionV2 memory _auction = auction; + INounsAuctionHouseV2.AuctionV2 memory auction_ = _auction; - if (_auction.nounId != nounId) revert NounNotUpForAuction(); - if (block.timestamp >= _auction.endTime) revert AuctionExpired(); + if (auction_.nounId != nounId) revert NounNotUpForAuction(); + if (block.timestamp >= auction_.endTime) revert AuctionExpired(); if (msg.value < reservePrice) revert MustSendAtLeastReservePrice(); - if (msg.value < _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100)) + if (msg.value < auction_.amount + ((auction_.amount * minBidIncrementPercentage) / 100)) revert BidDifferenceMustBeGreaterThanMinBidIncrement(); - auction.amount = uint128(msg.value); - auction.bidder = payable(msg.sender); + _auction.amount = uint128(msg.value); + _auction.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; + bool extended = auction_.endTime - block.timestamp < timeBuffer; - emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); + emit AuctionBid(auction_.nounId, msg.sender, msg.value, extended); if (extended) { - auction.endTime = _auction.endTime = uint40(block.timestamp + timeBuffer); - emit AuctionExtended(_auction.nounId, _auction.endTime); + _auction.endTime = auction_.endTime = uint40(block.timestamp + timeBuffer); + emit AuctionExtended(auction_.nounId, auction_.endTime); } - address payable lastBidder = _auction.bidder; + address payable lastBidder = auction_.bidder; // Refund the last bidder, if applicable if (lastBidder != address(0)) { - _safeTransferETHWithFallback(lastBidder, _auction.amount); + _safeTransferETHWithFallback(lastBidder, auction_.amount); } } + /** + * @notice Get the current auction. + */ + function auction() external view returns (AuctionV2 memory) { + return _auction; + } + /** * @notice Pause the Nouns auction house. * @dev This function can only be called by the owner when the @@ -170,7 +177,7 @@ contract NounsAuctionHouseV2 is __paused = false; emit Unpaused(_msgSender()); - if (auction.startTime == 0 || auction.settled) { + if (_auction.startTime == 0 || _auction.settled) { _createAuction(); } } @@ -225,7 +232,7 @@ contract NounsAuctionHouseV2 is uint40 startTime = uint40(block.timestamp); uint40 endTime = startTime + uint40(duration); - auction = AuctionV2({ + _auction = AuctionV2({ nounId: uint128(nounId), amount: 0, startTime: startTime, @@ -245,31 +252,31 @@ contract NounsAuctionHouseV2 is * @dev If there are no bids, the Noun is burned. */ function _settleAuction() internal { - INounsAuctionHouseV2.AuctionV2 memory _auction = auction; + INounsAuctionHouseV2.AuctionV2 memory auction_ = _auction; - if (_auction.startTime == 0) revert AuctionHasntBegun(); - if (_auction.settled) revert AuctionAlreadySettled(); - if (block.timestamp < _auction.endTime) revert AuctionNotDone(); + if (auction_.startTime == 0) revert AuctionHasntBegun(); + if (auction_.settled) revert AuctionAlreadySettled(); + if (block.timestamp < auction_.endTime) revert AuctionNotDone(); - auction.settled = true; + _auction.settled = true; - if (_auction.bidder == address(0)) { - nouns.burn(_auction.nounId); + if (auction_.bidder == address(0)) { + nouns.burn(auction_.nounId); } else { - nouns.transferFrom(address(this), _auction.bidder, _auction.nounId); + nouns.transferFrom(address(this), auction_.bidder, auction_.nounId); } - if (_auction.amount > 0) { - _safeTransferETHWithFallback(owner(), _auction.amount); + if (auction_.amount > 0) { + _safeTransferETHWithFallback(owner(), auction_.amount); } - settlementHistory[_auction.nounId] = SettlementState({ + settlementHistory[auction_.nounId] = SettlementState({ blockTimestamp: uint32(block.timestamp), - amount: ethPriceToUint64(_auction.amount), - winner: _auction.bidder + amount: ethPriceToUint64(auction_.amount), + winner: auction_.bidder }); - emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount); + emit AuctionSettled(auction_.nounId, auction_.bidder, auction_.amount); } /** @@ -341,8 +348,8 @@ contract NounsAuctionHouseV2 is * the Noun ID of that auction, the winning bid amount, and the winner's addreess. */ function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements) { - uint256 latestNounId = auction.nounId; - if (!auction.settled && latestNounId > 0) { + uint256 latestNounId = _auction.nounId; + if (!_auction.settled && latestNounId > 0) { latestNounId -= 1; } diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index 9fff1f702e..d503179262 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -20,7 +20,7 @@ pragma solidity ^0.8.19; import { IExcessETHBurner } from '../interfaces/IExcessETHBurner.sol'; import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import { INounsAuctionHouse } from '../interfaces/INounsAuctionHouse.sol'; +import { INounsAuctionHouseV2 } from '../interfaces/INounsAuctionHouseV2.sol'; interface RocketETH { function getEthValue(uint256 _rethAmount) external view returns (uint256); @@ -30,12 +30,6 @@ interface INounsDAOV3 { function adjustedTotalSupply() external view returns (uint256); } -interface INounsAuctionHouseV2 is INounsAuctionHouse { - function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); - - function auction() external view returns (INounsAuctionHouse.AuctionV2 memory); -} - interface IExecutorV3 { function burnExcessETH(uint256 amount) external; } diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index 919fdb89db..d6c0633ca1 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -100,4 +100,8 @@ interface INounsAuctionHouseV2 { function setReservePrice(uint192 reservePrice) external; function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external; + + function auction() external view returns (AuctionV2 memory); + + function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index e60229231b..e47138c541 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -37,7 +37,8 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { } function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; vm.deal(bidder, bid); vm.prank(bidder); auction.createBid{ value: bid }(nounId); @@ -47,7 +48,8 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { } function bidDontCreateNewAuction(address bidder, uint256 bid) internal returns (uint256) { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; vm.deal(bidder, bid); vm.prank(bidder); auction.createBid{ value: bid }(nounId); @@ -58,7 +60,7 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_revertsGivenWrongNounId() public { - (uint128 nounId, , , , , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; vm.expectRevert(INounsAuctionHouseV2.NounNotUpForAuction.selector); auction.createBid(nounId - 1); @@ -68,7 +70,8 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_createBid_revertsPastEndTime() public { - (uint128 nounId, , , uint40 endTime, , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; vm.warp(endTime + 1); vm.expectRevert(INounsAuctionHouseV2.AuctionExpired.selector); @@ -79,7 +82,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { vm.prank(owner); auction.setReservePrice(1 ether); - (uint128 nounId, , , , , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; vm.expectRevert(INounsAuctionHouseV2.MustSendAtLeastReservePrice.selector); auction.createBid{ value: 0.9 ether }(nounId); @@ -88,7 +91,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_revertsGivenBidLowerThanMinIncrement() public { vm.prank(owner); auction.setMinBidIncrementPercentage(50); - (uint128 nounId, , , , , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; auction.createBid{ value: 1 ether }(nounId); vm.expectRevert( @@ -98,7 +101,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_createBid_refundsPreviousBidder() public { - (uint256 nounId, , , , , ) = auction.auction(); + uint256 nounId = auction.auction().nounId; address bidder1 = address(0x4444); address bidder2 = address(0x5555); @@ -118,7 +121,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_preventsGasGriefingUponRefunding() public { BidderWithGasGriefing badBidder = new BidderWithGasGriefing(); - (uint256 nounId, , , , , ) = auction.auction(); + uint256 nounId = auction.auction().nounId; badBidder.bid{ value: 1 ether }(auction, nounId); @@ -147,7 +150,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_settleAuction_revertsWhenSettled() public { - (, , , uint40 endTime, , ) = auction.auction(); + uint40 endTime = auction.auction().endTime; vm.warp(endTime + 1); vm.prank(owner); @@ -172,7 +175,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_settleCurrentAndCreateNewAuction_revertsWhenPaused() public { - (, , , uint40 endTime, , ) = auction.auction(); + uint40 endTime = auction.auction().endTime; vm.warp(endTime + 1); vm.prank(owner); @@ -206,21 +209,15 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); - ( - uint128 nounIdV2, - uint128 amountV2, - uint40 startTimeV2, - uint40 endTimeV2, - address payable bidderV2, - bool settledV2 - ) = auctionV2.auction(); - - assertEq(nounIdV2, nounId); - assertEq(amountV2, amount); - assertEq(startTimeV2, startTime); - assertEq(endTimeV2, endTime); - assertEq(bidderV2, bidder); - assertEq(settledV2, false); + + 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); @@ -267,14 +264,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(auction)); - ( - uint128 nounIdV2, - uint128 amountV2, - uint40 startTimeV2, - uint40 endTimeV2, - address payable bidderV2, - bool settledV2 - ) = auction.auction(); + INounsAuctionHouseV2.AuctionV2 memory auctionV2 = auction.auction(); ( uint256 nounIdV1, @@ -285,12 +275,12 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { bool settledV1 ) = auctionV1.auction(); - assertEq(nounIdV2, nounIdV1); - assertEq(amountV2, amountV1); - assertEq(startTimeV2, startTimeV1); - assertEq(endTimeV2, endTimeV1); - assertEq(bidderV2, bidderV1); - assertEq(settledV2, settledV1); + assertEq(auctionV2.nounId, nounIdV1); + assertEq(auctionV2.amount, amountV1); + assertEq(auctionV2.startTime, startTimeV1); + assertEq(auctionV2.endTime, endTimeV1); + assertEq(auctionV2.bidder, bidderV1); + assertEq(auctionV2.settled, settledV1); } } diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol index 3f7fd1dffa..65e4fba589 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -33,15 +33,15 @@ contract AuctionMock is INounsAuctionHouseV2 { pricesHistory = pricesHistory_; } - function prices(uint256) external view override returns (INounsAuctionHouse.Settlement[] memory priceHistory_) { - priceHistory_ = new INounsAuctionHouse.Settlement[](pricesHistory.length); + function prices(uint256) external view override returns (INounsAuctionHouseV2.Settlement[] memory priceHistory_) { + priceHistory_ = new INounsAuctionHouseV2.Settlement[](pricesHistory.length); for (uint256 i; i < pricesHistory.length; ++i) { priceHistory_[i].amount = pricesHistory[i]; } } - function auction() external view returns (INounsAuctionHouse.AuctionV2 memory) { - return INounsAuctionHouse.AuctionV2(nounId, 0, 0, 0, payable(address(0)), false); + function auction() external view returns (INounsAuctionHouseV2.AuctionV2 memory) { + return INounsAuctionHouseV2.AuctionV2(nounId, 0, 0, 0, payable(address(0)), false); } function settleAuction() external {} @@ -54,9 +54,9 @@ contract AuctionMock is INounsAuctionHouseV2 { function unpause() external {} - function setTimeBuffer(uint256 timeBuffer) external {} + function setTimeBuffer(uint56 timeBuffer) external {} - function setReservePrice(uint256 reservePrice) external {} + function setReservePrice(uint192 reservePrice) external {} function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external {} } diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index ffde243433..08b95bc796 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -113,7 +113,7 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { getProposalToExecution(proposalId); vm.stopPrank(); - (uint128 currentNounID, , , , , ) = auction.auction(); + uint128 currentNounID = auction.auction().nounId; ExcessETHBurner burner = _deployExcessETHBurner( NounsDAOExecutorV3(treasury), @@ -197,11 +197,11 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { } function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + INounsAuctionHouseV2.AuctionV2 memory auction_ = auction.auction(); vm.deal(bidder, bid); vm.prank(bidder); - auction.createBid{ value: bid }(nounId); - vm.warp(endTime); + auction.createBid{ value: bid }(auction_.nounId); + vm.warp(auction_.endTime); auction.settleCurrentAndCreateNewAuction(); return block.timestamp; } From d5b76f5664f4ac64907e18d625d70737bbe7d22b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 3 Oct 2023 17:26:21 -0500 Subject: [PATCH 085/115] testnet: scripts and config to test the burn on sepolia * mock ERC20 for stETH * mock rETH * deployment scripts for AHV2, ExecutorV3 and the burner contract * update subgraph and webapp config to point to the latest deployment --- .../contracts/test/ERC20Testnet.sol | 20 ++++++ .../contracts/test/RocketETHTestnet.sol | 26 ++++++++ ...2.s.sol => DeployAuctionHouseV2Base.s.sol} | 2 +- .../script/DeployAuctionHouseV2Sepolia.s.sol | 15 +++++ ...ployExecutorV3AndExcessETHBurnerBase.s.sol | 65 +++++++++++++++++++ ...yExecutorV3AndExcessETHBurnerSepolia.s.sol | 33 ++++++++++ .../DeployTestnetTokens.s.sol | 24 +++++++ .../ProposeExecutorV3UpgradeBase.s.sol | 35 ++++++++++ .../ProposeExecutorV3UpgradeMainnet.s.sol | 21 ++++++ .../nouns-sdk/src/contract/addresses.json | 23 ++++--- packages/nouns-subgraph/config/sepolia.json | 16 ++--- packages/nouns-subgraph/package.json | 2 +- packages/nouns-webapp/src/config.ts | 4 +- 13 files changed, 262 insertions(+), 24 deletions(-) create mode 100644 packages/nouns-contracts/contracts/test/ERC20Testnet.sol create mode 100644 packages/nouns-contracts/contracts/test/RocketETHTestnet.sol rename packages/nouns-contracts/script/{DeployAuctionHouseV2.s.sol => DeployAuctionHouseV2Base.s.sol} (94%) create mode 100644 packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol create mode 100644 packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol create mode 100644 packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol create mode 100644 packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployTestnetTokens.s.sol create mode 100644 packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol create mode 100644 packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeMainnet.s.sol 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/DeployAuctionHouseV2.s.sol b/packages/nouns-contracts/script/DeployAuctionHouseV2Base.s.sol similarity index 94% rename from packages/nouns-contracts/script/DeployAuctionHouseV2.s.sol rename to packages/nouns-contracts/script/DeployAuctionHouseV2Base.s.sol index b5b6ee7d3e..9567ffd6f6 100644 --- a/packages/nouns-contracts/script/DeployAuctionHouseV2.s.sol +++ b/packages/nouns-contracts/script/DeployAuctionHouseV2Base.s.sol @@ -6,7 +6,7 @@ import { NounsAuctionHouse } from '../contracts/NounsAuctionHouse.sol'; import { NounsAuctionHouseV2 } from '../contracts/NounsAuctionHouseV2.sol'; import { NounsAuctionHousePreV2Migration } from '../contracts/NounsAuctionHousePreV2Migration.sol'; -contract DeployAuctionHouseV2 is Script { +abstract contract DeployAuctionHouseV2Base is Script { NounsAuctionHouse public immutable auctionV1; constructor(address _auctionHouseProxy) { diff --git a/packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol b/packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol new file mode 100644 index 0000000000..c86e68d053 --- /dev/null +++ b/packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol @@ -0,0 +1,15 @@ +// 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'; + +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..9ab42b923e --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; +import { NounsDAOExecutorV3 } from '../../contracts/governance/NounsDAOExecutorV3.sol'; +import { ExcessETHBurner, INounsDAOV3 } from '../../contracts/governance/ExcessETHBurner.sol'; +import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; +import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; +import { IERC20 } from '@openzeppelin/contracts/interfaces/IERC20.sol'; + +abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { + address public immutable executorProxy; + NounsDAOLogicV3 public immutable daoProxy; + INounsAuctionHouseV2 public immutable auction; + IERC20 wETH; + IERC20 stETH; + IERC20 rETH; + uint128 burnStartNounID; + uint128 minNewNounsBetweenBurns; + uint16 numberOfPastAuctionsForMeanPrice; + + constructor( + address payable executorProxy_, + address wETH_, + address stETH_, + address rETH_, + uint128 burnStartNounID_, + uint128 minNewNounsBetweenBurns_, + uint16 numberOfPastAuctionsForMeanPrice_ + ) { + executorProxy = executorProxy_; + + daoProxy = NounsDAOLogicV3(payable(NounsDAOExecutorV3(executorProxy_).admin())); + auction = INounsAuctionHouseV2(daoProxy.nouns().minter()); + + wETH = IERC20(wETH_); + stETH = IERC20(stETH_); + rETH = IERC20(rETH_); + + burnStartNounID = burnStartNounID_; + minNewNounsBetweenBurns = minNewNounsBetweenBurns_; + 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, + burnStartNounID, + minNewNounsBetweenBurns, + numberOfPastAuctionsForMeanPrice + ); + + vm.stopBroadcast(); + } +} 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..8c2b7db64a --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; +import { DeployExecutorV3AndExcessETHBurnerBase } from './DeployExecutorV3AndExcessETHBurnerBase.s.sol'; +import { NounsDAOExecutorV3 } from '../../contracts/governance/NounsDAOExecutorV3.sol'; +import { ExcessETHBurner, INounsDAOV3 } from '../../contracts/governance/ExcessETHBurner.sol'; +import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; +import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; +import { IERC20 } from '@openzeppelin/contracts/interfaces/IERC20.sol'; + +contract DeployExecutorV3AndExcessETHBurnerSepolia is DeployExecutorV3AndExcessETHBurnerBase { + address payable constant EXECUTOR_PROXY = payable(0x6c2dD53b8DbDD3af1209DeB9dA87D487EaE8E638); + address constant WETH = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; + address constant STETH = 0x7f96dAEF4A54F6A52613d6272560C2BD25e913B8; + address constant RETH = 0xf07dafCC49a9F5E1E73Df6bD6616d0a5bA19e502; + + uint128 BURN_START_NOUN_ID = 10; + uint128 MIN_NOUNS_BETWEEN_BURNS = 10; + uint16 MEAN_AUCTION_COUNT = 10; + + constructor() + DeployExecutorV3AndExcessETHBurnerBase( + EXECUTOR_PROXY, + WETH, + STETH, + RETH, + BURN_START_NOUN_ID, + MIN_NOUNS_BETWEEN_BURNS, + 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..720f3552c9 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Script.sol'; +import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; + +abstract contract ProposeExecutorV3UpgradeBase is Script { + uint256 proposerKey; + string description; + NounsDAOLogicV3 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 = 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..2b76337073 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeMainnet.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Script.sol'; +import { ProposeExecutorV3UpgradeBase } from './ProposeExecutorV3UpgradeBase.s.sol'; +import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; + +contract ProposeDAOV3UpgradeMainnet is ProposeExecutorV3UpgradeBase { + NounsDAOLogicV3 public constant NOUNS_DAO_PROXY_MAINNET = + NounsDAOLogicV3(payable(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-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 c457df013d..0b0d1caf05 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 30be08b567..a1fa63f636 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] }; }; From 3bf4c0657dc8e3526c10642eb61329d3463ce43e Mon Sep 17 00:00:00 2001 From: davidbrai Date: Wed, 4 Oct 2023 16:00:29 +0000 Subject: [PATCH 086/115] add auction house gas snapshots --- packages/nouns-contracts/.gas-snapshot | 31 +++++---- .../interfaces/INounsAuctionHouseV2.sol | 2 + .../NounsAuctionHouseGasSnapshot.t.sol | 63 +++++++++++++++++++ 3 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol diff --git a/packages/nouns-contracts/.gas-snapshot b/packages/nouns-contracts/.gas-snapshot index 74f2b93661..49485f2683 100644 --- a/packages/nouns-contracts/.gas-snapshot +++ b/packages/nouns-contracts/.gas-snapshot @@ -1,11 +1,20 @@ -NounsDAOLogic_GasSnapshot_V2_propose:test_propose_longDescription() (gas: 528733) -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 +NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid:test_createOneBid() (gas: 35090) +NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid:test_createTwoBids() (gas: 93677) +NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid:test_settleCurrentAndCreateNewAuction() (gas: 232680) +NounsAuctionHouseV2_GasSnapshot_createBid:test_createOneBid() (gas: 35090) +NounsAuctionHouseV2_GasSnapshot_createBid:test_createTwoBids() (gas: 93677) +NounsAuctionHouseV2_GasSnapshot_createBid:test_settleCurrentAndCreateNewAuction() (gas: 249780) +NounsAuctionHouse_GasSnapshot_createBid:test_createOneBid() (gas: 81474) +NounsAuctionHouse_GasSnapshot_createBid:test_createTwoBids() (gas: 142736) +NounsAuctionHouse_GasSnapshot_createBid:test_settleCurrentAndCreateNewAuction() (gas: 243008) +NounsDAOLogic_GasSnapshot_V2_propose:test_propose_longDescription() (gas: 528754) +NounsDAOLogic_GasSnapshot_V2_propose:test_propose_shortDescription() (gas: 398387) +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/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index 919fdb89db..5ab991b599 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -100,4 +100,6 @@ interface INounsAuctionHouseV2 { function setReservePrice(uint192 reservePrice) external; function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external; + + function warmUpSettlementState(uint256[] calldata nounIds) external; } 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..1a77c427f5 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -0,0 +1,63 @@ +// 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 { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; + +contract NounsAuctionHouse_GasSnapshot_createBid 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(); + } + + 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_createBid is NounsAuctionHouse_GasSnapshot_createBid { + function setUp() public virtual override { + super.setUp(); + _upgradeAuctionHouse(owner, proxyAdmin, auctionHouseProxy); + } +} + +contract NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid is NounsAuctionHouseV2_GasSnapshot_createBid { + function setUp() public override { + super.setUp(); + nounIds = [1, 2, 3]; + INounsAuctionHouseV2(address(auctionHouse)).warmUpSettlementState(nounIds); + } +} From 936eda9e7f7e5703f3f023eb47bf2ec8b6d037d2 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 12:28:19 +0000 Subject: [PATCH 087/115] change AH getter to return struct --- .../contracts/NounsAuctionHouseV2.sol | 69 ++++++++++--------- .../test/foundry/NounsAuctionHouseV2.t.sol | 69 ++++++++----------- 2 files changed, 67 insertions(+), 71 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 933219e6ae..776458a310 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -60,7 +60,7 @@ contract NounsAuctionHouseV2 is uint8 public minBidIncrementPercentage; /// @notice The active auction - INounsAuctionHouseV2.AuctionV2 public auction; + INounsAuctionHouseV2.AuctionV2 private _auction; /// @notice Whether this contract is paused or not /// @dev Replaces the state variable from PausableUpgradeable, to bit pack this bool with `auction` and save gas @@ -121,35 +121,42 @@ contract NounsAuctionHouseV2 is * @dev This contract only accepts payment in ETH. */ function createBid(uint256 nounId) external payable override { - INounsAuctionHouseV2.AuctionV2 memory _auction = auction; + INounsAuctionHouseV2.AuctionV2 memory auction_ = _auction; - if (_auction.nounId != nounId) revert NounNotUpForAuction(); - if (block.timestamp >= _auction.endTime) revert AuctionExpired(); + if (auction_.nounId != nounId) revert NounNotUpForAuction(); + if (block.timestamp >= auction_.endTime) revert AuctionExpired(); if (msg.value < reservePrice) revert MustSendAtLeastReservePrice(); - if (msg.value < _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100)) + if (msg.value < auction_.amount + ((auction_.amount * minBidIncrementPercentage) / 100)) revert BidDifferenceMustBeGreaterThanMinBidIncrement(); - auction.amount = uint128(msg.value); - auction.bidder = payable(msg.sender); + _auction.amount = uint128(msg.value); + _auction.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; + bool extended = auction_.endTime - block.timestamp < timeBuffer; - emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); + emit AuctionBid(auction_.nounId, msg.sender, msg.value, extended); if (extended) { - auction.endTime = _auction.endTime = uint40(block.timestamp + timeBuffer); - emit AuctionExtended(_auction.nounId, _auction.endTime); + _auction.endTime = auction_.endTime = uint40(block.timestamp + timeBuffer); + emit AuctionExtended(auction_.nounId, auction_.endTime); } - address payable lastBidder = _auction.bidder; + address payable lastBidder = auction_.bidder; // Refund the last bidder, if applicable if (lastBidder != address(0)) { - _safeTransferETHWithFallback(lastBidder, _auction.amount); + _safeTransferETHWithFallback(lastBidder, auction_.amount); } } + /** + * @notice Get the current auction. + */ + function auction() external view returns (AuctionV2 memory) { + return _auction; + } + /** * @notice Pause the Nouns auction house. * @dev This function can only be called by the owner when the @@ -170,7 +177,7 @@ contract NounsAuctionHouseV2 is __paused = false; emit Unpaused(_msgSender()); - if (auction.startTime == 0 || auction.settled) { + if (_auction.startTime == 0 || _auction.settled) { _createAuction(); } } @@ -225,7 +232,7 @@ contract NounsAuctionHouseV2 is uint40 startTime = uint40(block.timestamp); uint40 endTime = startTime + uint40(duration); - auction = AuctionV2({ + _auction = AuctionV2({ nounId: uint128(nounId), amount: 0, startTime: startTime, @@ -245,31 +252,31 @@ contract NounsAuctionHouseV2 is * @dev If there are no bids, the Noun is burned. */ function _settleAuction() internal { - INounsAuctionHouseV2.AuctionV2 memory _auction = auction; + INounsAuctionHouseV2.AuctionV2 memory auction_ = _auction; - if (_auction.startTime == 0) revert AuctionHasntBegun(); - if (_auction.settled) revert AuctionAlreadySettled(); - if (block.timestamp < _auction.endTime) revert AuctionNotDone(); + if (auction_.startTime == 0) revert AuctionHasntBegun(); + if (auction_.settled) revert AuctionAlreadySettled(); + if (block.timestamp < auction_.endTime) revert AuctionNotDone(); - auction.settled = true; + _auction.settled = true; - if (_auction.bidder == address(0)) { - nouns.burn(_auction.nounId); + if (auction_.bidder == address(0)) { + nouns.burn(auction_.nounId); } else { - nouns.transferFrom(address(this), _auction.bidder, _auction.nounId); + nouns.transferFrom(address(this), auction_.bidder, auction_.nounId); } - if (_auction.amount > 0) { - _safeTransferETHWithFallback(owner(), _auction.amount); + if (auction_.amount > 0) { + _safeTransferETHWithFallback(owner(), auction_.amount); } - settlementHistory[_auction.nounId] = SettlementState({ + settlementHistory[auction_.nounId] = SettlementState({ blockTimestamp: uint32(block.timestamp), - amount: ethPriceToUint64(_auction.amount), - winner: _auction.bidder + amount: ethPriceToUint64(auction_.amount), + winner: auction_.bidder }); - emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount); + emit AuctionSettled(auction_.nounId, auction_.bidder, auction_.amount); } /** @@ -341,8 +348,8 @@ contract NounsAuctionHouseV2 is * the Noun ID of that auction, the winning bid amount, and the winner's addreess. */ function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements) { - uint256 latestNounId = auction.nounId; - if (!auction.settled && latestNounId > 0) { + uint256 latestNounId = _auction.nounId; + if (!_auction.settled && latestNounId > 0) { latestNounId -= 1; } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index e60229231b..e8bcc4e361 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -8,7 +8,6 @@ import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctio import { NounsAuctionHouse } from '../../contracts/NounsAuctionHouse.sol'; import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; import { NounsAuctionHouseV2 } from '../../contracts/NounsAuctionHouseV2.sol'; -import { NounsAuctionHousePreV2Migration } from '../../contracts/NounsAuctionHousePreV2Migration.sol'; import { BidderWithGasGriefing } from './helpers/BidderWithGasGriefing.sol'; contract NounsAuctionHouseV2TestBase is Test, DeployUtils { @@ -37,7 +36,8 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { } function bidAndWinCurrentAuction(address bidder, uint256 bid) internal returns (uint256) { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; vm.deal(bidder, bid); vm.prank(bidder); auction.createBid{ value: bid }(nounId); @@ -47,7 +47,8 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { } function bidDontCreateNewAuction(address bidder, uint256 bid) internal returns (uint256) { - (uint256 nounId, , , uint256 endTime, , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; vm.deal(bidder, bid); vm.prank(bidder); auction.createBid{ value: bid }(nounId); @@ -58,7 +59,7 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_revertsGivenWrongNounId() public { - (uint128 nounId, , , , , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; vm.expectRevert(INounsAuctionHouseV2.NounNotUpForAuction.selector); auction.createBid(nounId - 1); @@ -68,7 +69,8 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_createBid_revertsPastEndTime() public { - (uint128 nounId, , , uint40 endTime, , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; + uint40 endTime = auction.auction().endTime; vm.warp(endTime + 1); vm.expectRevert(INounsAuctionHouseV2.AuctionExpired.selector); @@ -79,7 +81,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { vm.prank(owner); auction.setReservePrice(1 ether); - (uint128 nounId, , , , , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; vm.expectRevert(INounsAuctionHouseV2.MustSendAtLeastReservePrice.selector); auction.createBid{ value: 0.9 ether }(nounId); @@ -88,7 +90,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_revertsGivenBidLowerThanMinIncrement() public { vm.prank(owner); auction.setMinBidIncrementPercentage(50); - (uint128 nounId, , , , , ) = auction.auction(); + uint128 nounId = auction.auction().nounId; auction.createBid{ value: 1 ether }(nounId); vm.expectRevert( @@ -98,7 +100,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_createBid_refundsPreviousBidder() public { - (uint256 nounId, , , , , ) = auction.auction(); + uint256 nounId = auction.auction().nounId; address bidder1 = address(0x4444); address bidder2 = address(0x5555); @@ -118,7 +120,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_preventsGasGriefingUponRefunding() public { BidderWithGasGriefing badBidder = new BidderWithGasGriefing(); - (uint256 nounId, , , , , ) = auction.auction(); + uint256 nounId = auction.auction().nounId; badBidder.bid{ value: 1 ether }(auction, nounId); @@ -147,7 +149,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_settleAuction_revertsWhenSettled() public { - (, , , uint40 endTime, , ) = auction.auction(); + uint40 endTime = auction.auction().endTime; vm.warp(endTime + 1); vm.prank(owner); @@ -172,7 +174,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_settleCurrentAndCreateNewAuction_revertsWhenPaused() public { - (, , , uint40 endTime, , ) = auction.auction(); + uint40 endTime = auction.auction().endTime; vm.warp(endTime + 1); vm.prank(owner); @@ -206,21 +208,15 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); - ( - uint128 nounIdV2, - uint128 amountV2, - uint40 startTimeV2, - uint40 endTimeV2, - address payable bidderV2, - bool settledV2 - ) = auctionV2.auction(); - - assertEq(nounIdV2, nounId); - assertEq(amountV2, amount); - assertEq(startTimeV2, startTime); - assertEq(endTimeV2, endTime); - assertEq(bidderV2, bidder); - assertEq(settledV2, false); + + 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); @@ -267,14 +263,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(auction)); - ( - uint128 nounIdV2, - uint128 amountV2, - uint40 startTimeV2, - uint40 endTimeV2, - address payable bidderV2, - bool settledV2 - ) = auction.auction(); + INounsAuctionHouseV2.AuctionV2 memory auctionV2 = auction.auction(); ( uint256 nounIdV1, @@ -285,12 +274,12 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { bool settledV1 ) = auctionV1.auction(); - assertEq(nounIdV2, nounIdV1); - assertEq(amountV2, amountV1); - assertEq(startTimeV2, startTimeV1); - assertEq(endTimeV2, endTimeV1); - assertEq(bidderV2, bidderV1); - assertEq(settledV2, settledV1); + assertEq(auctionV2.nounId, nounIdV1); + assertEq(auctionV2.amount, amountV1); + assertEq(auctionV2.startTime, startTimeV1); + assertEq(auctionV2.endTime, endTimeV1); + assertEq(auctionV2.bidder, bidderV1); + assertEq(auctionV2.settled, settledV1); } } From 8553c80dd3d1ff0fa5631f56569b272fd0bd1cdc Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 12:31:16 +0000 Subject: [PATCH 088/115] change var to public --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 776458a310..1a0fd4b7b5 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -60,7 +60,7 @@ contract NounsAuctionHouseV2 is uint8 public minBidIncrementPercentage; /// @notice The active auction - INounsAuctionHouseV2.AuctionV2 private _auction; + INounsAuctionHouseV2.AuctionV2 public _auction; /// @notice Whether this contract is paused or not /// @dev Replaces the state variable from PausableUpgradeable, to bit pack this bool with `auction` and save gas From f6775834d40900cebd6ef6deb28af88fac80e5e4 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 12:40:42 +0000 Subject: [PATCH 089/115] AH2: add auction and prices to interface --- .../contracts/interfaces/INounsAuctionHouseV2.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index 5ab991b599..98ee179fdf 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -101,5 +101,9 @@ interface INounsAuctionHouseV2 { function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external; + function auction() external view returns (AuctionV2 memory); + + function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); + function warmUpSettlementState(uint256[] calldata nounIds) external; } From ff650bc1ef5506cf7d1980f3f834aac31d3e13f9 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 12:44:35 +0000 Subject: [PATCH 090/115] ah2: minor rename --- .../contracts/NounsAuctionHouseV2.sol | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 1a0fd4b7b5..aee2db25ea 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -121,32 +121,32 @@ contract NounsAuctionHouseV2 is * @dev This contract only accepts payment in ETH. */ function createBid(uint256 nounId) external payable override { - INounsAuctionHouseV2.AuctionV2 memory auction_ = _auction; + INounsAuctionHouseV2.AuctionV2 memory auctionCache = _auction; - if (auction_.nounId != nounId) revert NounNotUpForAuction(); - if (block.timestamp >= auction_.endTime) revert AuctionExpired(); + if (auctionCache.nounId != nounId) revert NounNotUpForAuction(); + if (block.timestamp >= auctionCache.endTime) revert AuctionExpired(); if (msg.value < reservePrice) revert MustSendAtLeastReservePrice(); - if (msg.value < auction_.amount + ((auction_.amount * minBidIncrementPercentage) / 100)) + if (msg.value < auctionCache.amount + ((auctionCache.amount * minBidIncrementPercentage) / 100)) revert BidDifferenceMustBeGreaterThanMinBidIncrement(); _auction.amount = uint128(msg.value); _auction.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; + bool extended = auctionCache.endTime - block.timestamp < timeBuffer; - emit AuctionBid(auction_.nounId, msg.sender, msg.value, extended); + emit AuctionBid(auctionCache.nounId, msg.sender, msg.value, extended); if (extended) { - _auction.endTime = auction_.endTime = uint40(block.timestamp + timeBuffer); - emit AuctionExtended(auction_.nounId, auction_.endTime); + _auction.endTime = auctionCache.endTime = uint40(block.timestamp + timeBuffer); + emit AuctionExtended(auctionCache.nounId, auctionCache.endTime); } - address payable lastBidder = auction_.bidder; + address payable lastBidder = auctionCache.bidder; // Refund the last bidder, if applicable if (lastBidder != address(0)) { - _safeTransferETHWithFallback(lastBidder, auction_.amount); + _safeTransferETHWithFallback(lastBidder, auctionCache.amount); } } From 05eb47a49affc06cef39675a5d80c663ae48df03 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 13:04:19 +0000 Subject: [PATCH 091/115] bugfix: fix bug returning empty settlements in case the settlements were warmed up, the timestamp in change to 1 and the previous check would fail --- .../contracts/NounsAuctionHouseV2.sol | 4 ++-- .../test/foundry/NounsAuctionHouseV2.t.sol | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index aee2db25ea..96383087d4 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -358,7 +358,7 @@ contract NounsAuctionHouseV2 is while (actualCount < auctionCount && latestNounId > 0) { // Skip Nouner reward Nouns, they have no price // Also skips IDs with no price data - if (settlementHistory[latestNounId].blockTimestamp == 0) { + if (settlementHistory[latestNounId].winner == address(0)) { --latestNounId; continue; } @@ -394,7 +394,7 @@ contract NounsAuctionHouseV2 is while (currentId < endId) { // Skip Nouner reward Nouns, they have no price // Also skips IDs with no price data - if (settlementHistory[currentId].blockTimestamp == 0) { + if (settlementHistory[currentId].winner == address(0)) { ++currentId; continue; } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index e8bcc4e361..4d25355671 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -16,6 +16,7 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { address owner = address(0x1111); address noundersDAO = address(0x2222); address minter = address(0x3333); + uint256[] nounIds; NounsAuctionHouseV2 auction; @@ -343,6 +344,27 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { assertEq(prices[19].amount, 1 ether); } + 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 prices = auction.prices(20); + assertEq(prices[0].nounId, 22); + assertEq(prices[1].nounId, 21); + assertEq(prices[2].nounId, 19); + assertEq(prices[10].nounId, 11); + assertEq(prices[11].nounId, 9); + assertEq(prices[19].nounId, 1); + + prices = auction.prices(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); From 79f611a6de8323c8ba7e1063087a4c99eb3e4a80 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 13:37:58 +0000 Subject: [PATCH 092/115] AH2: use error strings instead of custom errors to reduce diff and it's easier to debug in many cases --- .../contracts/NounsAuctionHouseV2.sol | 40 ++++++++++--------- .../interfaces/INounsAuctionHouseV2.sol | 16 -------- .../NounsAuctionHouseGasSnapshot.t.sol | 3 +- .../test/foundry/NounsAuctionHouseV2.t.sol | 29 +++++++------- .../foundry/helpers/AuctionHouseUpgrader.sol | 40 +++++++++++++++++++ .../test/foundry/helpers/DeployUtils.sol | 29 -------------- 6 files changed, 77 insertions(+), 80 deletions(-) create mode 100644 packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 96383087d4..ea5c61f924 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -123,11 +123,13 @@ contract NounsAuctionHouseV2 is function createBid(uint256 nounId) external payable override { INounsAuctionHouseV2.AuctionV2 memory auctionCache = _auction; - if (auctionCache.nounId != nounId) revert NounNotUpForAuction(); - if (block.timestamp >= auctionCache.endTime) revert AuctionExpired(); - if (msg.value < reservePrice) revert MustSendAtLeastReservePrice(); - if (msg.value < auctionCache.amount + ((auctionCache.amount * minBidIncrementPercentage) / 100)) - revert BidDifferenceMustBeGreaterThanMinBidIncrement(); + require(auctionCache.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 >= auctionCache.amount + ((auctionCache.amount * minBidIncrementPercentage) / 100), + 'Must send more than last bid by minBidIncrementPercentage amount' + ); _auction.amount = uint128(msg.value); _auction.bidder = payable(msg.sender); @@ -194,7 +196,7 @@ contract NounsAuctionHouseV2 is * @dev Only callable by the owner. */ function setTimeBuffer(uint56 _timeBuffer) external override onlyOwner { - if (_timeBuffer > MAX_TIME_BUFFER) revert TimeBufferTooLarge(); + require(_timeBuffer <= MAX_TIME_BUFFER, 'timeBuffer too large'); timeBuffer = _timeBuffer; @@ -252,31 +254,31 @@ contract NounsAuctionHouseV2 is * @dev If there are no bids, the Noun is burned. */ function _settleAuction() internal { - INounsAuctionHouseV2.AuctionV2 memory auction_ = _auction; + INounsAuctionHouseV2.AuctionV2 memory auctionCache = _auction; - if (auction_.startTime == 0) revert AuctionHasntBegun(); - if (auction_.settled) revert AuctionAlreadySettled(); - if (block.timestamp < auction_.endTime) revert AuctionNotDone(); + require(auctionCache.startTime != 0, "Auction hasn't begun"); + require(!auctionCache.settled, 'Auction has already been settled'); + require(block.timestamp >= auctionCache.endTime, "Auction hasn't completed"); _auction.settled = true; - if (auction_.bidder == address(0)) { - nouns.burn(auction_.nounId); + if (auctionCache.bidder == address(0)) { + nouns.burn(auctionCache.nounId); } else { - nouns.transferFrom(address(this), auction_.bidder, auction_.nounId); + nouns.transferFrom(address(this), auctionCache.bidder, auctionCache.nounId); } - if (auction_.amount > 0) { - _safeTransferETHWithFallback(owner(), auction_.amount); + if (auctionCache.amount > 0) { + _safeTransferETHWithFallback(owner(), auctionCache.amount); } - settlementHistory[auction_.nounId] = SettlementState({ + settlementHistory[auctionCache.nounId] = SettlementState({ blockTimestamp: uint32(block.timestamp), - amount: ethPriceToUint64(auction_.amount), - winner: auction_.bidder + amount: ethPriceToUint64(auctionCache.amount), + winner: auctionCache.bidder }); - emit AuctionSettled(auction_.nounId, auction_.bidder, auction_.amount); + emit AuctionSettled(auctionCache.nounId, auctionCache.bidder, auctionCache.amount); } /** diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index 98ee179fdf..bf64fa12b9 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -69,22 +69,6 @@ interface INounsAuctionHouseV2 { event HistoricPricesSet(uint256[] nounIds, uint256[] prices); - error NounNotUpForAuction(); - - error AuctionExpired(); - - error AuctionHasntBegun(); - - error AuctionAlreadySettled(); - - error AuctionNotDone(); - - error MustSendAtLeastReservePrice(); - - error BidDifferenceMustBeGreaterThanMinBidIncrement(); - - error TimeBufferTooLarge(); - function settleAuction() external; function settleCurrentAndCreateNewAuction() external; diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol index 1a77c427f5..6d6eec7210 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -5,6 +5,7 @@ import { INounsAuctionHouse } from '../../contracts/interfaces/INounsAuctionHous 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'; @@ -50,7 +51,7 @@ contract NounsAuctionHouse_GasSnapshot_createBid is DeployUtils { contract NounsAuctionHouseV2_GasSnapshot_createBid is NounsAuctionHouse_GasSnapshot_createBid { function setUp() public virtual override { super.setUp(); - _upgradeAuctionHouse(owner, proxyAdmin, auctionHouseProxy); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionHouseProxy); } } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 4d25355671..1f0f6bb1d9 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -3,6 +3,7 @@ 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'; @@ -27,7 +28,7 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { minter ); - _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); auction = NounsAuctionHouseV2(address(auctionProxy)); @@ -62,10 +63,10 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { function test_createBid_revertsGivenWrongNounId() public { uint128 nounId = auction.auction().nounId; - vm.expectRevert(INounsAuctionHouseV2.NounNotUpForAuction.selector); + vm.expectRevert('Noun not up for auction'); auction.createBid(nounId - 1); - vm.expectRevert(INounsAuctionHouseV2.NounNotUpForAuction.selector); + vm.expectRevert('Noun not up for auction'); auction.createBid(nounId + 1); } @@ -74,7 +75,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { uint40 endTime = auction.auction().endTime; vm.warp(endTime + 1); - vm.expectRevert(INounsAuctionHouseV2.AuctionExpired.selector); + vm.expectRevert('Auction expired'); auction.createBid(nounId); } @@ -84,7 +85,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { uint128 nounId = auction.auction().nounId; - vm.expectRevert(INounsAuctionHouseV2.MustSendAtLeastReservePrice.selector); + vm.expectRevert('Must send at least reservePrice'); auction.createBid{ value: 0.9 ether }(nounId); } @@ -94,9 +95,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { uint128 nounId = auction.auction().nounId; auction.createBid{ value: 1 ether }(nounId); - vm.expectRevert( - abi.encodeWithSelector(INounsAuctionHouseV2.BidDifferenceMustBeGreaterThanMinBidIncrement.selector) - ); + vm.expectRevert('Must send more than last bid by minBidIncrementPercentage amount'); auction.createBid{ value: 1.49 ether }(nounId); } @@ -145,7 +144,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_settleAuction_revertsWhenAuctionInProgress() public { - vm.expectRevert(INounsAuctionHouseV2.AuctionNotDone.selector); + vm.expectRevert("Auction hasn't completed"); auction.settleCurrentAndCreateNewAuction(); } @@ -157,7 +156,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { auction.pause(); auction.settleAuction(); - vm.expectRevert(INounsAuctionHouseV2.AuctionAlreadySettled.selector); + vm.expectRevert('Auction has already been settled'); auction.settleAuction(); } @@ -167,10 +166,10 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { noundersDAO, minter ); - _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); auction = NounsAuctionHouseV2(address(auctionProxy)); - vm.expectRevert(INounsAuctionHouseV2.AuctionHasntBegun.selector); + vm.expectRevert("Auction hasn't begun"); auction.settleAuction(); } @@ -206,7 +205,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { address nounsBefore = address(auctionV1.nouns()); address wethBefore = address(auctionV1.weth()); - _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); @@ -250,7 +249,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { vm.prank(owner); auctionV1.pause(); - _upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); + AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); assertEq(auctionV2.paused(), true); @@ -547,7 +546,7 @@ contract NounsAuctionHouseV2_OwnerFunctionsTest is NounsAuctionHouseV2TestBase { function test_setTimeBuffer_revertsGivenValueAboveMax() public { vm.prank(auction.owner()); - vm.expectRevert(INounsAuctionHouseV2.TimeBufferTooLarge.selector); + vm.expectRevert('timeBuffer too large'); auction.setTimeBuffer(1 days + 1); } 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/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index 5cc79200eb..3166412427 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -20,8 +20,6 @@ import { Inflator } from '../../../contracts/Inflator.sol'; import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionHouseProxy.sol'; import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; import { NounsAuctionHouse } from '../../../contracts/NounsAuctionHouse.sol'; -import { NounsAuctionHouseV2 } from '../../../contracts/NounsAuctionHouseV2.sol'; -import { NounsAuctionHousePreV2Migration } from '../../../contracts/NounsAuctionHousePreV2Migration.sol'; import { WETH } from '../../../contracts/test/WETH.sol'; abstract contract DeployUtils is Test, DescriptorHelpers { @@ -64,33 +62,6 @@ abstract contract DeployUtils is Test, DescriptorHelpers { return (proxy, admin); } - 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(); - } - uint32 constant LAST_MINUTE_BLOCKS = 10; uint32 constant OBJECTION_PERIOD_BLOCKS = 10; uint32 constant UPDATABLE_PERIOD_BLOCKS = 10; From a62b08d73a3e4b8bc75c885088d300af3251b21c Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 13:47:14 +0000 Subject: [PATCH 093/115] ah2: rename storage vars to reduce diff compared to AHv1 --- .../contracts/NounsAuctionHouseV2.sol | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index ea5c61f924..83a28b02f4 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -60,7 +60,7 @@ contract NounsAuctionHouseV2 is uint8 public minBidIncrementPercentage; /// @notice The active auction - INounsAuctionHouseV2.AuctionV2 public _auction; + INounsAuctionHouseV2.AuctionV2 public auctionStorage; /// @notice Whether this contract is paused or not /// @dev Replaces the state variable from PausableUpgradeable, to bit pack this bool with `auction` and save gas @@ -121,34 +121,34 @@ contract NounsAuctionHouseV2 is * @dev This contract only accepts payment in ETH. */ function createBid(uint256 nounId) external payable override { - INounsAuctionHouseV2.AuctionV2 memory auctionCache = _auction; + INounsAuctionHouseV2.AuctionV2 memory _auction = auctionStorage; - require(auctionCache.nounId == nounId, 'Noun not up for auction'); + 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 >= auctionCache.amount + ((auctionCache.amount * minBidIncrementPercentage) / 100), + msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100), 'Must send more than last bid by minBidIncrementPercentage amount' ); - _auction.amount = uint128(msg.value); - _auction.bidder = payable(msg.sender); + 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 = auctionCache.endTime - block.timestamp < timeBuffer; + bool extended = _auction.endTime - block.timestamp < timeBuffer; - emit AuctionBid(auctionCache.nounId, msg.sender, msg.value, extended); + emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended); if (extended) { - _auction.endTime = auctionCache.endTime = uint40(block.timestamp + timeBuffer); - emit AuctionExtended(auctionCache.nounId, auctionCache.endTime); + auctionStorage.endTime = _auction.endTime = uint40(block.timestamp + timeBuffer); + emit AuctionExtended(_auction.nounId, _auction.endTime); } - address payable lastBidder = auctionCache.bidder; + address payable lastBidder = _auction.bidder; // Refund the last bidder, if applicable if (lastBidder != address(0)) { - _safeTransferETHWithFallback(lastBidder, auctionCache.amount); + _safeTransferETHWithFallback(lastBidder, _auction.amount); } } @@ -156,7 +156,7 @@ contract NounsAuctionHouseV2 is * @notice Get the current auction. */ function auction() external view returns (AuctionV2 memory) { - return _auction; + return auctionStorage; } /** @@ -179,7 +179,7 @@ contract NounsAuctionHouseV2 is __paused = false; emit Unpaused(_msgSender()); - if (_auction.startTime == 0 || _auction.settled) { + if (auctionStorage.startTime == 0 || auctionStorage.settled) { _createAuction(); } } @@ -234,7 +234,7 @@ contract NounsAuctionHouseV2 is uint40 startTime = uint40(block.timestamp); uint40 endTime = startTime + uint40(duration); - _auction = AuctionV2({ + auctionStorage = AuctionV2({ nounId: uint128(nounId), amount: 0, startTime: startTime, @@ -254,31 +254,31 @@ contract NounsAuctionHouseV2 is * @dev If there are no bids, the Noun is burned. */ function _settleAuction() internal { - INounsAuctionHouseV2.AuctionV2 memory auctionCache = _auction; + INounsAuctionHouseV2.AuctionV2 memory _auction = auctionStorage; - require(auctionCache.startTime != 0, "Auction hasn't begun"); - require(!auctionCache.settled, 'Auction has already been settled'); - require(block.timestamp >= auctionCache.endTime, "Auction hasn't completed"); + 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"); - _auction.settled = true; + auctionStorage.settled = true; - if (auctionCache.bidder == address(0)) { - nouns.burn(auctionCache.nounId); + if (_auction.bidder == address(0)) { + nouns.burn(_auction.nounId); } else { - nouns.transferFrom(address(this), auctionCache.bidder, auctionCache.nounId); + nouns.transferFrom(address(this), _auction.bidder, _auction.nounId); } - if (auctionCache.amount > 0) { - _safeTransferETHWithFallback(owner(), auctionCache.amount); + if (_auction.amount > 0) { + _safeTransferETHWithFallback(owner(), _auction.amount); } - settlementHistory[auctionCache.nounId] = SettlementState({ + settlementHistory[_auction.nounId] = SettlementState({ blockTimestamp: uint32(block.timestamp), - amount: ethPriceToUint64(auctionCache.amount), - winner: auctionCache.bidder + amount: ethPriceToUint64(_auction.amount), + winner: _auction.bidder }); - emit AuctionSettled(auctionCache.nounId, auctionCache.bidder, auctionCache.amount); + emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount); } /** @@ -350,8 +350,8 @@ contract NounsAuctionHouseV2 is * the Noun ID of that auction, the winning bid amount, and the winner's addreess. */ function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements) { - uint256 latestNounId = _auction.nounId; - if (!_auction.settled && latestNounId > 0) { + uint256 latestNounId = auctionStorage.nounId; + if (!auctionStorage.settled && latestNounId > 0) { latestNounId -= 1; } From 3b1256cda779f3623e53c206fe813248e4c8762f Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 5 Oct 2023 13:49:59 +0000 Subject: [PATCH 094/115] fix test post merge --- .../test/foundry/governance/NounsDAOExecutorV3.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index 08b95bc796..bc5a653101 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -3,10 +3,10 @@ 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 { NounsTokenLike } from '../../../contracts/governance/NounsDAOInterfaces.sol'; import { ExcessETHBurner, INounsAuctionHouseV2 } from '../../../contracts/governance/ExcessETHBurner.sol'; import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; import { NounsAuctionHouseProxy } from '../../../contracts/proxies/NounsAuctionHouseProxy.sol'; @@ -182,7 +182,7 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { function upgradeAuction() internal { bytes32 proxyAdminBytes = vm.load(address(dao.nouns().minter()), _ADMIN_SLOT); address proxyAdminAddress = address(uint160(uint256(proxyAdminBytes))); - _upgradeAuctionHouse( + AuctionHouseUpgrader.upgradeAuctionHouse( treasury, NounsAuctionHouseProxyAdmin(proxyAdminAddress), NounsAuctionHouseProxy(payable(dao.nouns().minter())) From 3f766360d5dd269e7131dcac5585dc569198fa34 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 08:08:43 +0000 Subject: [PATCH 095/115] add natspec --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 83a28b02f4..ec849e244a 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -32,6 +32,10 @@ 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, From 6726f230a658b6995b57b955c6f4c3ddfce7bb2d Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 08:12:16 +0000 Subject: [PATCH 096/115] minor: rename test --- packages/nouns-contracts/.gas-snapshot | 18 +++++++++--------- .../foundry/NounsAuctionHouseGasSnapshot.t.sol | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/nouns-contracts/.gas-snapshot b/packages/nouns-contracts/.gas-snapshot index 49485f2683..6269717483 100644 --- a/packages/nouns-contracts/.gas-snapshot +++ b/packages/nouns-contracts/.gas-snapshot @@ -1,12 +1,12 @@ -NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid:test_createOneBid() (gas: 35090) -NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid:test_createTwoBids() (gas: 93677) -NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid:test_settleCurrentAndCreateNewAuction() (gas: 232680) -NounsAuctionHouseV2_GasSnapshot_createBid:test_createOneBid() (gas: 35090) -NounsAuctionHouseV2_GasSnapshot_createBid:test_createTwoBids() (gas: 93677) -NounsAuctionHouseV2_GasSnapshot_createBid:test_settleCurrentAndCreateNewAuction() (gas: 249780) -NounsAuctionHouse_GasSnapshot_createBid:test_createOneBid() (gas: 81474) -NounsAuctionHouse_GasSnapshot_createBid:test_createTwoBids() (gas: 142736) -NounsAuctionHouse_GasSnapshot_createBid:test_settleCurrentAndCreateNewAuction() (gas: 243008) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createOneBid() (gas: 35090) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createTwoBids() (gas: 93677) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 232680) +NounsAuctionHouseV2_GasSnapshot:test_createOneBid() (gas: 35090) +NounsAuctionHouseV2_GasSnapshot:test_createTwoBids() (gas: 93677) +NounsAuctionHouseV2_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 249780) +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: 528754) NounsDAOLogic_GasSnapshot_V2_propose:test_propose_shortDescription() (gas: 398387) NounsDAOLogic_GasSnapshot_V2_vote:test_castVoteWithReason() (gas: 83585) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol index 6d6eec7210..b6b635961f 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -9,7 +9,7 @@ import { AuctionHouseUpgrader } from './helpers/AuctionHouseUpgrader.sol'; import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; -contract NounsAuctionHouse_GasSnapshot_createBid is DeployUtils { +contract NounsAuctionHouse_GasSnapshot is DeployUtils { INounsAuctionHouse auctionHouse; INounsToken nouns; address noundersDAO = makeAddr('noundersDAO'); @@ -48,14 +48,14 @@ contract NounsAuctionHouse_GasSnapshot_createBid is DeployUtils { } } -contract NounsAuctionHouseV2_GasSnapshot_createBid is NounsAuctionHouse_GasSnapshot_createBid { +contract NounsAuctionHouseV2_GasSnapshot is NounsAuctionHouse_GasSnapshot { function setUp() public virtual override { super.setUp(); AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionHouseProxy); } } -contract NounsAuctionHouseV2WarmedUp_GasSnapshot_createBid is NounsAuctionHouseV2_GasSnapshot_createBid { +contract NounsAuctionHouseV2WarmedUp_GasSnapshot is NounsAuctionHouseV2_GasSnapshot { function setUp() public override { super.setUp(); nounIds = [1, 2, 3]; From 1e98e147868d01d62d087c246e151554640135b4 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 08:41:06 +0000 Subject: [PATCH 097/115] ah2: gas shavings --- .../contracts/NounsAuctionHouseV2.sol | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index ec849e244a..9ab8abd270 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -127,11 +127,17 @@ contract NounsAuctionHouseV2 is 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 >= _reservePrice, 'Must send at least reservePrice'); require( - msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100), + msg.value >= _auction.amount + ((_auction.amount * _minBidIncrementPercentage) / 100), 'Must send more than last bid by minBidIncrementPercentage amount' ); @@ -139,12 +145,12 @@ contract NounsAuctionHouseV2 is 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; + 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); + auctionStorage.endTime = _auction.endTime = uint40(block.timestamp + _timeBuffer); emit AuctionExtended(_auction.nounId, _auction.endTime); } From 3b7e1eeff7360a5c64b693023d0b17ec353413c8 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 09:07:17 +0000 Subject: [PATCH 098/115] ah2: gas optimize prices functions --- packages/nouns-contracts/.gas-snapshot | 10 +++-- .../contracts/NounsAuctionHouseV2.sol | 18 +++++---- .../interfaces/INounsAuctionHouseV2.sol | 2 + .../NounsAuctionHouseGasSnapshot.t.sol | 40 ++++++++++++++++++- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/packages/nouns-contracts/.gas-snapshot b/packages/nouns-contracts/.gas-snapshot index 6269717483..d88bc3ade6 100644 --- a/packages/nouns-contracts/.gas-snapshot +++ b/packages/nouns-contracts/.gas-snapshot @@ -1,9 +1,11 @@ -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createOneBid() (gas: 35090) -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createTwoBids() (gas: 93677) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createOneBid() (gas: 34941) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createTwoBids() (gas: 93379) NounsAuctionHouseV2WarmedUp_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 232680) -NounsAuctionHouseV2_GasSnapshot:test_createOneBid() (gas: 35090) -NounsAuctionHouseV2_GasSnapshot:test_createTwoBids() (gas: 93677) +NounsAuctionHouseV2_GasSnapshot:test_createOneBid() (gas: 34941) +NounsAuctionHouseV2_GasSnapshot:test_createTwoBids() (gas: 93379) NounsAuctionHouseV2_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 249780) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_prices_90() (gas: 385106) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_prices_range_90() (gas: 377540) NounsAuctionHouse_GasSnapshot:test_createOneBid() (gas: 81474) NounsAuctionHouse_GasSnapshot:test_createTwoBids() (gas: 142736) NounsAuctionHouse_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 243008) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 9ab8abd270..fe3d7c666a 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -368,17 +368,18 @@ contract NounsAuctionHouseV2 is 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 (settlementHistory[latestNounId].winner == address(0)) { + if (settlementState.winner == address(0)) { --latestNounId; continue; } settlements[actualCount] = Settlement({ - blockTimestamp: settlementHistory[latestNounId].blockTimestamp, - amount: uint64PriceToUint256(settlementHistory[latestNounId].amount), - winner: settlementHistory[latestNounId].winner, + blockTimestamp: settlementState.blockTimestamp, + amount: uint64PriceToUint256(settlementState.amount), + winner: settlementState.winner, nounId: latestNounId }); ++actualCount; @@ -404,17 +405,18 @@ contract NounsAuctionHouseV2 is 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 (settlementHistory[currentId].winner == address(0)) { + if (settlementState.winner == address(0)) { ++currentId; continue; } settlements[actualCount] = Settlement({ - blockTimestamp: settlementHistory[currentId].blockTimestamp, - amount: uint64PriceToUint256(settlementHistory[currentId].amount), - winner: settlementHistory[currentId].winner, + blockTimestamp: settlementState.blockTimestamp, + amount: uint64PriceToUint256(settlementState.amount), + winner: settlementState.winner, nounId: currentId }); ++actualCount; diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index bf64fa12b9..f47401ed36 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -89,5 +89,7 @@ interface INounsAuctionHouseV2 { function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); + function prices(uint256 startId, uint256 endId) external view returns (Settlement[] memory settlements); + function warmUpSettlementState(uint256[] calldata nounIds) external; } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol index b6b635961f..0b8f69fc4a 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -9,7 +9,7 @@ import { AuctionHouseUpgrader } from './helpers/AuctionHouseUpgrader.sol'; import { NounsAuctionHouseProxy } from '../../contracts/proxies/NounsAuctionHouseProxy.sol'; import { NounsAuctionHouseProxyAdmin } from '../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; -contract NounsAuctionHouse_GasSnapshot is DeployUtils { +abstract contract NounsAuctionHouseBaseTest is DeployUtils { INounsAuctionHouse auctionHouse; INounsToken nouns; address noundersDAO = makeAddr('noundersDAO'); @@ -31,7 +31,9 @@ contract NounsAuctionHouse_GasSnapshot is DeployUtils { vm.prank(owner); auctionHouse.unpause(); } +} +contract NounsAuctionHouse_GasSnapshot is NounsAuctionHouseBaseTest { function test_createOneBid() public { auctionHouse.createBid{ value: 1 ether }(1); } @@ -62,3 +64,39 @@ contract NounsAuctionHouseV2WarmedUp_GasSnapshot is NounsAuctionHouseV2_GasSnaps 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_prices_90() public { + INounsAuctionHouseV2.Settlement[] memory prices = auctionHouseV2.prices(90); + assertEq(prices.length, 90); + } + + function test_prices_range_90() public { + INounsAuctionHouseV2.Settlement[] memory prices = auctionHouseV2.prices(1, 100); + assertEq(prices.length, 90); + } +} From 3b536b041cf34876815393aeabefebf076d58e79 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 10:44:51 +0000 Subject: [PATCH 099/115] noracle: add function to return only prices --- packages/nouns-contracts/.gas-snapshot | 18 +- .../contracts/NounsAuctionHouseV2.sol | 85 +++++- .../interfaces/INounsAuctionHouseV2.sol | 8 +- .../NounsAuctionHouseGasSnapshot.t.sol | 18 +- .../test/foundry/NounsAuctionHouseV2.t.sol | 244 +++++++++++------- 5 files changed, 256 insertions(+), 117 deletions(-) diff --git a/packages/nouns-contracts/.gas-snapshot b/packages/nouns-contracts/.gas-snapshot index d88bc3ade6..0c32aba6e9 100644 --- a/packages/nouns-contracts/.gas-snapshot +++ b/packages/nouns-contracts/.gas-snapshot @@ -1,11 +1,13 @@ -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createOneBid() (gas: 34941) -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createTwoBids() (gas: 93379) -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 232680) -NounsAuctionHouseV2_GasSnapshot:test_createOneBid() (gas: 34941) -NounsAuctionHouseV2_GasSnapshot:test_createTwoBids() (gas: 93379) -NounsAuctionHouseV2_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 249780) -NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_prices_90() (gas: 385106) -NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_prices_range_90() (gas: 377540) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createOneBid() (gas: 34963) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createTwoBids() (gas: 93423) +NounsAuctionHouseV2WarmedUp_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 232686) +NounsAuctionHouseV2_GasSnapshot:test_createOneBid() (gas: 34963) +NounsAuctionHouseV2_GasSnapshot:test_createTwoBids() (gas: 93423) +NounsAuctionHouseV2_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 249786) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getPrices_90() (gas: 310063) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getPrices_range_90() (gas: 300578) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getSettlements_90() (gas: 385762) +NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getSettlements_range_90() (gas: 378168) NounsAuctionHouse_GasSnapshot:test_createOneBid() (gas: 81474) NounsAuctionHouse_GasSnapshot:test_createTwoBids() (gas: 142736) NounsAuctionHouse_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 243008) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index fe3d7c666a..3994b67175 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -353,13 +353,13 @@ contract NounsAuctionHouseV2 is } /** - * @notice Get past auction prices. - * @dev Returns prices in reverse order, meaning settlements[0] will be the most recent auction price. + * @notice Get past auction settlements. + * @dev Returns settlements in reverse order, meaning settlements[0] will be the most recent auction price. * @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 addreess. + * the Noun ID of that auction, the winning bid amount, and the winner's address. */ - function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements) { + function getSettlements(uint256 auctionCount) external view returns (Settlement[] memory settlements) { uint256 latestNounId = auctionStorage.nounId; if (!auctionStorage.settled && latestNounId > 0) { latestNounId -= 1; @@ -395,12 +395,50 @@ contract NounsAuctionHouseV2 is } /** - * @notice Get a range of past auction prices. - * @dev Returns prices in chronological order, as opposed to `prices(count)` which returns prices in reverse order. + * @notice Get past auction prices. + * @dev Returns prices in reverse order, meaning prices[0] will be the most recent auction price. + * @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. * @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 prices(uint256 startId, uint256 endId) external view returns (Settlement[] memory settlements) { + function getSettlements(uint256 startId, uint256 endId) external view returns (Settlement[] memory settlements) { settlements = new Settlement[](endId - startId); uint256 actualCount = 0; uint256 currentId = startId; @@ -431,6 +469,39 @@ contract NounsAuctionHouseV2 is } } + /** + * @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. + * @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. diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index f47401ed36..d043f3e823 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -87,9 +87,13 @@ interface INounsAuctionHouseV2 { function auction() external view returns (AuctionV2 memory); - function prices(uint256 auctionCount) external view returns (Settlement[] memory settlements); + function getSettlements(uint256 auctionCount) external view returns (Settlement[] memory settlements); - function prices(uint256 startId, uint256 endId) 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; } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol index 0b8f69fc4a..99027eb12c 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -90,13 +90,23 @@ contract NounsAuctionHouseV2_HistoricPrices_GasSnapshot is NounsAuctionHouseBase return block.timestamp; } - function test_prices_90() public { - INounsAuctionHouseV2.Settlement[] memory prices = auctionHouseV2.prices(90); + function test_getSettlements_90() public { + INounsAuctionHouseV2.Settlement[] memory prices = auctionHouseV2.getSettlements(90); assertEq(prices.length, 90); } - function test_prices_range_90() public { - INounsAuctionHouseV2.Settlement[] memory prices = auctionHouseV2.prices(1, 100); + 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); } } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 1f0f6bb1d9..3635419162 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -284,17 +284,23 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { + uint256[] expectedPrices; + function test_prices_oneAuction_higherAuctionCountReturnsTheOneAuction() public { address bidder = address(0x4444); bidAndWinCurrentAuction(bidder, 1 ether); - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(2); + 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].blockTimestamp, uint32(block.timestamp)); - assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1 ether); - assertEq(prices[0].winner, bidder); + assertEq(prices[0], 1 ether); } function test_prices_preserves10DecimalsUnderUint64MaxValue() public { @@ -302,23 +308,29 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { // at 10 decimal points it's 1844674407.3709551615 bidAndWinCurrentAuction(makeAddr('bidder'), 1844674407.3709551615999999 ether); - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(1); + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(1); - assertEq(prices.length, 1); - assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1844674407.3709551615 ether); - assertEq(prices[0].winner, makeAddr('bidder')); + 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 prices = auction.prices(1); + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(1); - assertEq(prices.length, 1); - assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1 * 1e8); - assertEq(prices[0].winner, makeAddr('bidder')); + 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 { @@ -327,20 +339,26 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(20); - assertEq(prices[0].nounId, 22); - assertEq(prices[1].nounId, 21); - assertEq(prices[2].nounId, 19); - assertEq(prices[10].nounId, 11); - assertEq(prices[11].nounId, 9); - assertEq(prices[19].nounId, 1); - - assertEq(prices[0].amount, 20 ether); - assertEq(prices[1].amount, 19 ether); - assertEq(prices[2].amount, 18 ether); - assertEq(prices[10].amount, 10 ether); - assertEq(prices[11].amount, 9 ether); - assertEq(prices[19].amount, 1 ether); + 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 { @@ -352,15 +370,24 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(20); - assertEq(prices[0].nounId, 22); - assertEq(prices[1].nounId, 21); - assertEq(prices[2].nounId, 19); - assertEq(prices[10].nounId, 11); - assertEq(prices[11].nounId, 9); - assertEq(prices[19].nounId, 1); + 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.prices(20, 21); + prices = auction.getPrices(20, 21); assertEq(prices.length, 0); } @@ -372,17 +399,21 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { auction.pause(); auction.settleAuction(); - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(2); - - assertEq(prices.length, 2); - assertEq(prices[0].blockTimestamp, uint32(bid2Timestamp)); - assertEq(prices[0].nounId, 2); - assertEq(prices[0].amount, 2 ether); - assertEq(prices[0].winner, makeAddr('bidder 2')); - assertEq(prices[1].blockTimestamp, uint32(bid1Timestamp)); - assertEq(prices[1].nounId, 1); - assertEq(prices[1].amount, 1 ether); - assertEq(prices[1].winner, makeAddr('bidder')); + 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 { @@ -398,17 +429,23 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, 2 ether); bidAndWinCurrentAuction(bidder, 3 ether); - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(3); + 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].nounId, 6); - assertEq(prices[0].amount, 3 ether); - assertEq(prices[0].winner, bidder); - assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2 ether); - assertEq(prices[1].winner, bidder); - assertEq(prices[2].nounId, 1); - assertEq(prices[2].amount, 1 ether); - assertEq(prices[2].winner, bidder); + assertEq(prices[0], 3 ether); + assertEq(prices[1], 2 ether); + assertEq(prices[2], 1 ether); } function test_prices_withRange_givenBiggerRangeThanAuctionsReturnsAuctionsAndZeroObservations() public { @@ -418,19 +455,23 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { lastBidTime = bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(0, 5); + INounsAuctionHouseV2.Settlement[] memory settlements = auction.getSettlements(0, 5); // lastest ID 4 has no settlement data, so it's not included in the result - assertEq(prices.length, 3); - assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1 ether); - assertEq(prices[0].winner, makeAddr('1')); - assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2 ether); - assertEq(prices[1].winner, makeAddr('2')); - assertEq(prices[2].blockTimestamp, uint32(lastBidTime)); - assertEq(prices[2].nounId, 3); - assertEq(prices[2].amount, 3 ether); - assertEq(prices[2].winner, makeAddr('3')); + 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 { @@ -439,20 +480,24 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, i * 1e18); } - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(7, 12); - assertEq(prices.length, 4); - assertEq(prices[0].nounId, 7); - assertEq(prices[0].amount, 7 ether); - assertEq(prices[0].winner, makeAddr('7')); - assertEq(prices[1].nounId, 8); - assertEq(prices[1].amount, 8 ether); - assertEq(prices[1].winner, makeAddr('8')); - assertEq(prices[2].nounId, 9); - assertEq(prices[2].amount, 9 ether); - assertEq(prices[2].winner, makeAddr('9')); - assertEq(prices[3].nounId, 11); - assertEq(prices[3].amount, 10 ether); - assertEq(prices[3].winner, makeAddr('10')); + 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 { @@ -468,17 +513,21 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { bidAndWinCurrentAuction(bidder, 2 ether); bidAndWinCurrentAuction(bidder, 3 ether); - INounsAuctionHouseV2.Settlement[] memory prices = auction.prices(1, 7); - assertEq(prices.length, 3); - assertEq(prices[0].nounId, 1); - assertEq(prices[0].amount, 1 ether); - assertEq(prices[0].winner, bidder); - assertEq(prices[1].nounId, 2); - assertEq(prices[1].amount, 2 ether); - assertEq(prices[1].winner, bidder); - assertEq(prices[2].nounId, 6); - assertEq(prices[2].amount, 3 ether); - assertEq(prices[2].winner, bidder); + 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 { @@ -527,13 +576,16 @@ contract NounsAuctionHouseV2_OracleTest is NounsAuctionHouseV2TestBase { vm.prank(auction.owner()); auction.setPrices(settlements); - INounsAuctionHouseV2.Settlement[] memory actualSettlements = auction.prices(0, 23); + 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]); } } } From 23b84bafa3f9b0105100cb7ac9b7aa0a42d4adbe Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 12:04:42 +0000 Subject: [PATCH 100/115] burn: use new getPrices function from auction house --- .../contracts/governance/ExcessETHBurner.sol | 6 +++--- .../test/foundry/governance/ExcessETHBurner.t.sol | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index d503179262..7e4471127a 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -173,13 +173,13 @@ contract ExcessETHBurner is IExcessETHBurner, Ownable { */ function meanAuctionPrice() public view returns (uint256) { uint16 numberOfPastAuctionsForMeanPrice_ = numberOfPastAuctionsForMeanPrice; - INounsAuctionHouseV2.Settlement[] memory settlements = auction.prices(numberOfPastAuctionsForMeanPrice_); + uint256[] memory prices = auction.getPrices(numberOfPastAuctionsForMeanPrice_); - if (settlements.length < numberOfPastAuctionsForMeanPrice_) revert NotEnoughAuctionHistory(); + if (prices.length < numberOfPastAuctionsForMeanPrice_) revert NotEnoughAuctionHistory(); uint256 sum = 0; for (uint16 i = 0; i < numberOfPastAuctionsForMeanPrice_; i++) { - sum += settlements[i].amount; + sum += prices[i]; } return sum / numberOfPastAuctionsForMeanPrice_; diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol index 6ca05600ca..7f7258852a 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -33,11 +33,8 @@ contract AuctionMock is INounsAuctionHouseV2 { pricesHistory = pricesHistory_; } - function prices(uint256) external view override returns (INounsAuctionHouseV2.Settlement[] memory priceHistory_) { - priceHistory_ = new INounsAuctionHouseV2.Settlement[](pricesHistory.length); - for (uint256 i; i < pricesHistory.length; ++i) { - priceHistory_[i].amount = pricesHistory[i]; - } + function getPrices(uint256) external view override returns (uint256[] memory) { + return pricesHistory; } function auction() external view returns (INounsAuctionHouseV2.AuctionV2 memory) { @@ -60,6 +57,12 @@ contract AuctionMock is INounsAuctionHouseV2 { 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 {} } From 110d7c3ce0792fed05dbc95b128524eec1181944 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 12:24:29 +0000 Subject: [PATCH 101/115] cleanup --- .../contracts/governance/ExcessETHBurner.sol | 3 +-- .../contracts/interfaces/IExcessETHBurner.sol | 22 ------------------- 2 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 packages/nouns-contracts/contracts/interfaces/IExcessETHBurner.sol diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index 7e4471127a..b52273f012 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -17,7 +17,6 @@ pragma solidity ^0.8.19; -import { IExcessETHBurner } from '../interfaces/IExcessETHBurner.sol'; import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import { INounsAuctionHouseV2 } from '../interfaces/INounsAuctionHouseV2.sol'; @@ -39,7 +38,7 @@ interface IExecutorV3 { * @notice A helpder 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 IExcessETHBurner, Ownable { +contract ExcessETHBurner is Ownable { /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * ERRORS diff --git a/packages/nouns-contracts/contracts/interfaces/IExcessETHBurner.sol b/packages/nouns-contracts/contracts/interfaces/IExcessETHBurner.sol deleted file mode 100644 index f4ba06c13e..0000000000 --- a/packages/nouns-contracts/contracts/interfaces/IExcessETHBurner.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -/// @title Interface for ExcessETHBurner, the helper contract for burning excess ETH - -/********************************* - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - * ░░░░░░█████████░░█████████░░░ * - * ░░░░░░██░░░████░░██░░░████░░░ * - * ░░██████░░░████████░░░████░░░ * - * ░░██░░██░░░████░░██░░░████░░░ * - * ░░██░░██░░░████░░██░░░████░░░ * - * ░░░░░░█████████░░█████████░░░ * - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * - *********************************/ - -pragma solidity ^0.8.19; - -interface IExcessETHBurner { - function excessETH() external view returns (uint256); -} From cf7db77f9a6b37fa1c425ff515141cbf492c3163 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 12:33:08 +0000 Subject: [PATCH 102/115] cleanups --- .../DeployExecutorV3AndExcessETHBurnerBase.s.sol | 10 +++++++--- .../ProposeExecutorV3UpgradeBase.s.sol | 15 ++++++++++++--- .../ProposeExecutorV3UpgradeMainnet.s.sol | 5 +---- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol index 9ab42b923e..d745ca9983 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol @@ -3,14 +3,18 @@ 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 { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.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; - NounsDAOLogicV3 public immutable daoProxy; + INounsDAOLogicV3 public immutable daoProxy; INounsAuctionHouseV2 public immutable auction; IERC20 wETH; IERC20 stETH; @@ -30,7 +34,7 @@ abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { ) { executorProxy = executorProxy_; - daoProxy = NounsDAOLogicV3(payable(NounsDAOExecutorV3(executorProxy_).admin())); + daoProxy = INounsDAOLogicV3(payable(NounsDAOExecutorV3(executorProxy_).admin())); auction = INounsAuctionHouseV2(daoProxy.nouns().minter()); wETH = IERC20(wETH_); diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol index 720f3552c9..bdf13e8b30 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeBase.s.sol @@ -2,12 +2,21 @@ pragma solidity ^0.8.15; import 'forge-std/Script.sol'; -import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.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; - NounsDAOLogicV3 daoProxy; + address daoProxy; address executorProxy; address executorV3Impl; @@ -27,7 +36,7 @@ abstract contract ProposeExecutorV3UpgradeBase is Script { signatures[i] = 'upgradeTo(address)'; calldatas[i] = abi.encode(executorV3Impl); - proposalId = daoProxy.propose(targets, values, signatures, calldatas, description); + 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 index 2b76337073..022f995b00 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeMainnet.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/ProposeExecutorV3UpgradeMainnet.s.sol @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.15; -import 'forge-std/Script.sol'; import { ProposeExecutorV3UpgradeBase } from './ProposeExecutorV3UpgradeBase.s.sol'; -import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; contract ProposeDAOV3UpgradeMainnet is ProposeExecutorV3UpgradeBase { - NounsDAOLogicV3 public constant NOUNS_DAO_PROXY_MAINNET = - NounsDAOLogicV3(payable(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d)); + address public constant NOUNS_DAO_PROXY_MAINNET = 0x6f3E6272A167e8AcCb32072d08E0957F9c79223d; address public constant EXECUTOR_PROXY_MAINNET = 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71; address public constant EXECUTOR_V3_IMPL = address(0); From cd2d8f68fb632975b363bc88971f9c57cb02269b Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 6 Oct 2023 12:40:54 +0000 Subject: [PATCH 103/115] cleanup --- .../nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol b/packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol index c86e68d053..ad830108e2 100644 --- a/packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol +++ b/packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol @@ -1,11 +1,6 @@ // 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'; - import { DeployAuctionHouseV2Base } from './DeployAuctionHouseV2Base.s.sol'; contract DeployAuctionHouseV2Sepolia is DeployAuctionHouseV2Base { From d385b0f55f83dcacd3592b733f4a3994bb799d17 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Mon, 9 Oct 2023 14:42:08 +0000 Subject: [PATCH 104/115] add burn upgrade mainnet fork test --- .../DeployAuctionHouseV2Base.s.sol | 6 +- .../DeployAuctionHouseV2Mainnet.s.sol | 10 + .../DeployAuctionHouseV2Sepolia.s.sol | 0 ...ployExecutorV3AndExcessETHBurnerBase.s.sol | 12 +- ...yExecutorV3AndExcessETHBurnerMainnet.s.sol | 27 ++ ...yExecutorV3AndExcessETHBurnerSepolia.s.sol | 6 - .../foundry/BurnUpgradeForkMainnetTest.t.sol | 264 ++++++++++++++++++ 7 files changed, 310 insertions(+), 15 deletions(-) rename packages/nouns-contracts/script/{ => AuctionHouseV2}/DeployAuctionHouseV2Base.s.sol (74%) create mode 100644 packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Mainnet.s.sol rename packages/nouns-contracts/script/{ => AuctionHouseV2}/DeployAuctionHouseV2Sepolia.s.sol (100%) create mode 100644 packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol create mode 100644 packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol diff --git a/packages/nouns-contracts/script/DeployAuctionHouseV2Base.s.sol b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Base.s.sol similarity index 74% rename from packages/nouns-contracts/script/DeployAuctionHouseV2Base.s.sol rename to packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Base.s.sol index 9567ffd6f6..e298e33dcf 100644 --- a/packages/nouns-contracts/script/DeployAuctionHouseV2Base.s.sol +++ b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Base.s.sol @@ -2,9 +2,9 @@ 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'; +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; 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/DeployAuctionHouseV2Sepolia.s.sol b/packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Sepolia.s.sol similarity index 100% rename from packages/nouns-contracts/script/DeployAuctionHouseV2Sepolia.s.sol rename to packages/nouns-contracts/script/AuctionHouseV2/DeployAuctionHouseV2Sepolia.s.sol diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol index d745ca9983..6b42ab0074 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol @@ -16,12 +16,12 @@ abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { address public immutable executorProxy; INounsDAOLogicV3 public immutable daoProxy; INounsAuctionHouseV2 public immutable auction; - IERC20 wETH; - IERC20 stETH; - IERC20 rETH; - uint128 burnStartNounID; - uint128 minNewNounsBetweenBurns; - uint16 numberOfPastAuctionsForMeanPrice; + IERC20 public immutable wETH; + IERC20 public immutable stETH; + IERC20 public immutable rETH; + uint128 public immutable burnStartNounID; + uint128 public immutable minNewNounsBetweenBurns; + uint16 public immutable numberOfPastAuctionsForMeanPrice; constructor( address payable executorProxy_, 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..3994e3de61 --- /dev/null +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol @@ -0,0 +1,27 @@ +// 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; + + uint128 BURN_START_NOUN_ID = type(uint128).max; + uint128 MIN_NOUNS_BETWEEN_BURNS = 0; + uint16 MEAN_AUCTION_COUNT = 1; + + constructor() + DeployExecutorV3AndExcessETHBurnerBase( + EXECUTOR_PROXY, + WETH, + STETH, + RETH, + BURN_START_NOUN_ID, + MIN_NOUNS_BETWEEN_BURNS, + MEAN_AUCTION_COUNT + ) + {} +} diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol index 8c2b7db64a..787667f10b 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol @@ -1,13 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.19; -import 'forge-std/Script.sol'; import { DeployExecutorV3AndExcessETHBurnerBase } from './DeployExecutorV3AndExcessETHBurnerBase.s.sol'; -import { NounsDAOExecutorV3 } from '../../contracts/governance/NounsDAOExecutorV3.sol'; -import { ExcessETHBurner, INounsDAOV3 } from '../../contracts/governance/ExcessETHBurner.sol'; -import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; -import { INounsAuctionHouseV2 } from '../../contracts/interfaces/INounsAuctionHouseV2.sol'; -import { IERC20 } from '@openzeppelin/contracts/interfaces/IERC20.sol'; contract DeployExecutorV3AndExcessETHBurnerSepolia is DeployExecutorV3AndExcessETHBurnerBase { address payable constant EXECUTOR_PROXY = payable(0x6c2dD53b8DbDD3af1209DeB9dA87D487EaE8E638); 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..a070d8e387 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol @@ -0,0 +1,264 @@ +// 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 { + assertGt(burner.nextBurnNounID(), 1000000); + + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); + } + + function test_burn() public { + uint256 proposalId = proposeToTurnOnBurn(880, 10, 20); + voteAndExecuteProposal(proposalId); + + assertEq(burner.nextBurnNounID(), 880); + assertEq(burner.minNewNounsBetweenBurns(), 10); + 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 + assertEq(address(NOUNS_TIMELOCK_V2_MAINNET).balance, 2813.062865210366418892 ether); + + burner.burnExcessETH(); + + assertEq(address(NOUNS_TIMELOCK_V2_MAINNET).balance, 0); + } + + function proposeToTurnOnBurn( + uint128 nextBurnNoundId, + uint128 minNewNounsBetweenBurns, + uint16 numberOfPastAuctionsForMeanPrice + ) internal returns (uint256 proposalId) { + uint8 numTxs = 3; + 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] = 'setNextBurnNounID(uint128)'; + calldatas[i] = abi.encode(nextBurnNoundId); + + i = 1; + targets[i] = address(burner); + values[i] = 0; + signatures[i] = 'setMinNewNounsBetweenBurns(uint128)'; + calldatas[i] = abi.encode(minNewNounsBetweenBurns); + + i = 2; + 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); + } +} From e9f7200e40e6d06d14b21fea28db7d3f0f7d8357 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 10:40:20 +0000 Subject: [PATCH 105/115] burner: add protection in case auction house returns an invalid noun id this is unlikely, but may happen in case of a bad upgrade --- .../contracts/governance/ExcessETHBurner.sol | 7 ++- .../interfaces/INounsAuctionHouseV2.sol | 4 ++ .../foundry/governance/ExcessETHBurner.t.sol | 50 +++++++++++++++++-- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index b52273f012..1a17fc9893 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -120,7 +120,12 @@ contract ExcessETHBurner is Ownable { * @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) { - if (auction.auction().nounId < nextBurnNounID) revert NotTimeToBurnYet(); + uint256 currentNounId = auction.auction().nounId; + + // Make sure this is a valid noun id. This will revert if this id doesn't exist + auction.nouns().ownerOf(currentNounId); + + if (currentNounId < nextBurnNounID) revert NotTimeToBurnYet(); amount = excessETH(); if (amount == 0) revert NoExcessToBurn(); diff --git a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol index d043f3e823..e3ccf2f1be 100644 --- a/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/interfaces/INounsAuctionHouseV2.sol @@ -17,6 +17,8 @@ pragma solidity ^0.8.19; +import { INounsToken } from './INounsToken.sol'; + interface INounsAuctionHouseV2 { struct AuctionV2 { // ID for the Noun (ERC721 token ID) @@ -96,4 +98,6 @@ interface INounsAuctionHouseV2 { 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/test/foundry/governance/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol index 7f7258852a..41c0e65ce4 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -4,10 +4,13 @@ pragma solidity ^0.8.19; import 'forge-std/Test.sol'; import { DeployUtilsExcessETHBurner } from '../helpers/DeployUtilsExcessETHBurner.sol'; import { NounsDAOExecutorV3 } from '../../../contracts/governance/NounsDAOExecutorV3.sol'; -import { INounsAuctionHouse } from '../../../contracts/interfaces/INounsAuctionHouse.sol'; -import { ExcessETHBurner, INounsAuctionHouseV2, INounsDAOV3 } from '../../../contracts/governance/ExcessETHBurner.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; @@ -24,6 +27,11 @@ contract DAOMock { contract AuctionMock is INounsAuctionHouseV2 { uint256[] pricesHistory; uint128 nounId; + INounsToken nounsToken; + + constructor(INounsToken nounsToken_) { + nounsToken = nounsToken_; + } function setNounId(uint128 nounId_) external { nounId = nounId_; @@ -64,11 +72,36 @@ contract AuctionMock is INounsAuctionHouseV2 { 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(); - AuctionMock auction = new AuctionMock(); + NounsTokenMock nounsToken = new NounsTokenMock(); + AuctionMock auction = new AuctionMock(nounsToken); NounsDAOExecutorV3 treasury; ExcessETHBurner burner; @@ -88,6 +121,8 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { treasury.setExcessETHBurner(address(burner)); auction.setNounId(burnStartNounId); + nounsToken.mint(address(1), 0); + nounsToken.mint(address(1), 1); } function test_burnExcessETH_beforeNextBurnNounID_reverts() public { @@ -117,6 +152,13 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { assertEq(burner.nextBurnNounID(), burnStartNounId + minNewNounsBetweenBurns); } + 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); @@ -131,6 +173,8 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { burner.burnExcessETH(); auction.setNounId(burner.nextBurnNounID()); + nounsToken.mint(address(1), burner.nextBurnNounID()); + uint128 expectedNextBurnID = burner.nextBurnNounID() + minNewNounsBetweenBurns; assertEq(burner.burnExcessETH(), 99 ether); assertEq(burner.nextBurnNounID(), expectedNextBurnID); From d1c4c84aa47a7fb88cda18a268e2e0e5148b454a Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 10:42:19 +0000 Subject: [PATCH 106/115] cleanup --- .../test/foundry/governance/ExcessETHBurner.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol index 41c0e65ce4..6395cddc97 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -250,8 +250,6 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { burner.burnExcessETH(); } - // setMinNewNounsBetweenBurns - function test_setNextBurnNounID_revertsForNonOwner() public { vm.expectRevert('Ownable: caller is not the owner'); burner.setNextBurnNounID(1); From 7e6ff8c1c623b2805837261a8c59d6bc58862d52 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 10:45:00 +0000 Subject: [PATCH 107/115] add docs --- .../test/foundry/BurnUpgradeForkMainnetTest.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol b/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol index a070d8e387..ab974c85b0 100644 --- a/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol +++ b/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol @@ -221,7 +221,7 @@ contract BurnUpgradeForkMainnetTest is Test { assertEq(burner.expectedTreasuryValueInETH(), 10550 ether); assertEq(burner.treasuryValueInETH(), 13899.047817579079479237 ether); - // treasury has less ETH than 13899 - 25 * 422 + // 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(); From e82bb20c06722277300a2dcd7d72903e5ee708cb Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 10:59:39 +0000 Subject: [PATCH 108/115] burner: add event --- .../contracts/governance/ExcessETHBurner.sol | 7 +++++-- .../test/foundry/governance/ExcessETHBurner.t.sol | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index 1a17fc9893..77006a7d39 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -63,6 +63,7 @@ contract ExcessETHBurner is Ownable { uint16 oldNumberOfPastAuctionsForMeanPrice, uint16 newNumberOfPastAuctionsForMeanPrice ); + event Burn(uint256 amount, uint128 previousBurnNounId, uint128 nextBurnNounId); /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ @@ -125,14 +126,16 @@ contract ExcessETHBurner is Ownable { // Make sure this is a valid noun id. This will revert if this id doesn't exist auction.nouns().ownerOf(currentNounId); - if (currentNounId < nextBurnNounID) revert NotTimeToBurnYet(); + uint128 nextBurnNounID_ = nextBurnNounID; + if (currentNounId < nextBurnNounID_) revert NotTimeToBurnYet(); amount = excessETH(); if (amount == 0) revert NoExcessToBurn(); IExecutorV3(owner()).burnExcessETH(amount); - nextBurnNounID += minNewNounsBetweenBurns; + nextBurnNounID = nextBurnNounID_ + minNewNounsBetweenBurns; + emit Burn(amount, nextBurnNounID_, nextBurnNounID); } /** diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol index 6395cddc97..cb384f657f 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -109,6 +109,8 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { uint128 minNewNounsBetweenBurns; uint16 pastAuctionCount; + event Burn(uint256 amount, uint128 previousBurnNounId, uint128 nextBurnNounId); + function setUp() public { burnStartNounId = 1; minNewNounsBetweenBurns = 100; @@ -145,6 +147,8 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { dao.setAdjustedTotalSupply(1); vm.deal(address(treasury), 100 ether); + vm.expectEmit(true, true, true, true); + emit Burn(99 ether, 1, 101); uint256 burnedAmount = burner.burnExcessETH(); assertEq(burnedAmount, 99 ether); From a1d52a2ab1b06f321f7e188837a1bf9d35ca6893 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 11:04:46 +0000 Subject: [PATCH 109/115] add natspec --- packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 3994b67175..51f11e7b2b 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -355,6 +355,7 @@ contract NounsAuctionHouseV2 is /** * @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. @@ -397,6 +398,7 @@ contract NounsAuctionHouseV2 is /** * @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. */ @@ -433,6 +435,7 @@ contract NounsAuctionHouseV2 is /** * @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, @@ -472,6 +475,7 @@ contract NounsAuctionHouseV2 is /** * @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. From ee6c04a950e8831f434735a708be12a3ed314790 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 11:44:25 +0000 Subject: [PATCH 110/115] tests: update V3 tests to use upgraded auction house --- .../nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol | 4 ++++ 1 file changed, 4 insertions(+) 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)); From 5da5eff608457103478d28979ccb4f7c2c91c61c Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 11:59:36 +0000 Subject: [PATCH 111/115] natspec --- .../contracts/governance/ExcessETHBurner.sol | 13 ++++++++++++- .../contracts/governance/NounsDAOExecutorV3.sol | 10 ++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index 77006a7d39..62285b55cb 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -35,7 +35,7 @@ interface IExecutorV3 { /** * @title ExcessETH Burner - * @notice A helpder contract for burning Nouns excess ETH with NounsDAOExecutorV3. + * @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 { @@ -82,6 +82,17 @@ contract ExcessETHBurner is Ownable { uint128 public minNewNounsBetweenBurns; 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 burnStartNounID_ A burn will be possible only if the currently auctioned Noun has a greater or equal ID + * @param minNewNounsBetweenBurns_ Number of nouns that need to be minted between burns + * @param numberOfPastAuctionsForMeanPrice_ Number of past auctions to consider when calculating mean price + */ constructor( address owner_, INounsDAOV3 dao_, diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol index ecdc5dcfc2..1d1412f33c 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOExecutorV3.sol @@ -16,7 +16,7 @@ *********************************/ // LICENSE -// NounsDAOExecutor2.sol is a modified version of Compound Lab's Timelock.sol: +// 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. @@ -25,17 +25,19 @@ // Additional conditions of BSD-3-Clause can be found here: https://opensource.org/licenses/BSD-3-Clause // // MODIFICATIONS -// NounsDAOExecutor2.sol is a modified version of NounsDAOExecutor.sol +// 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. // -// -// NounsDAOExecutor2.sol modifications: +// 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; From ea002b57cb3df093d5fc593678896977c46a021c Mon Sep 17 00:00:00 2001 From: davidbrai Date: Wed, 11 Oct 2023 10:05:54 +0000 Subject: [PATCH 112/115] minor renames in burner contract --- .../contracts/governance/ExcessETHBurner.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index 62285b55cb..63f7cb2d0f 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -137,16 +137,17 @@ contract ExcessETHBurner is Ownable { // Make sure this is a valid noun id. This will revert if this id doesn't exist auction.nouns().ownerOf(currentNounId); - uint128 nextBurnNounID_ = nextBurnNounID; - if (currentNounId < nextBurnNounID_) revert NotTimeToBurnYet(); + uint128 currentNextBurnNounID = nextBurnNounID; + if (currentNounId < currentNextBurnNounID) revert NotTimeToBurnYet(); amount = excessETH(); if (amount == 0) revert NoExcessToBurn(); IExecutorV3(owner()).burnExcessETH(amount); - nextBurnNounID = nextBurnNounID_ + minNewNounsBetweenBurns; - emit Burn(amount, nextBurnNounID_, nextBurnNounID); + uint128 newNextBurnNounId = currentNextBurnNounID + minNewNounsBetweenBurns; + nextBurnNounID = newNextBurnNounId; + emit Burn(amount, currentNextBurnNounID, newNextBurnNounId); } /** From 0edf9150847d98056c6dc92a310c05a76ddcf591 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 11 Oct 2023 10:56:50 -0500 Subject: [PATCH 113/115] ah: revert custom paused bool it's not bit-packed when placed after the auction struct and for now we prefer to not force it into the auction struct esp. since bigger gas savings are coming from descriptor storage read optimizations --- .../NounsAuctionHousePreV2Migration.sol | 2 -- .../contracts/NounsAuctionHouseV2.sol | 17 ++--------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol index c0f796eb4f..f2eb7582f8 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHousePreV2Migration.sol @@ -39,7 +39,6 @@ contract NounsAuctionHousePreV2Migration is PausableUpgradeable, ReentrancyGuard uint56 timeBuffer; uint8 minBidIncrementPercentage; INounsAuctionHouseV2.AuctionV2 auction; - bool paused; } uint256 private startSlot; @@ -70,7 +69,6 @@ contract NounsAuctionHousePreV2Migration is PausableUpgradeable, ReentrancyGuard bidder: oldLayoutCache.auction.bidder, settled: oldLayoutCache.auction.settled }); - newLayout.paused = paused(); } function _oldLayout() internal pure returns (OldLayout storage layout) { diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 51f11e7b2b..96a034e432 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -66,10 +66,6 @@ contract NounsAuctionHouseV2 is /// @notice The active auction INounsAuctionHouseV2.AuctionV2 public auctionStorage; - /// @notice Whether this contract is paused or not - /// @dev Replaces the state variable from PausableUpgradeable, to bit pack this bool with `auction` and save gas - bool public __paused; - /// @notice The Nouns price feed state mapping(uint256 => SettlementState) settlementHistory; @@ -176,8 +172,7 @@ contract NounsAuctionHouseV2 is * anyone can settle an ongoing auction. */ function pause() external override onlyOwner { - __paused = true; - emit Paused(_msgSender()); + _pause(); } /** @@ -186,21 +181,13 @@ contract NounsAuctionHouseV2 is * contract is paused. If required, this function will start a new auction. */ function unpause() external override onlyOwner { - __paused = false; - emit Unpaused(_msgSender()); + _unpause(); if (auctionStorage.startTime == 0 || auctionStorage.settled) { _createAuction(); } } - /** - * @dev Get whether this contract is paused or not. - */ - function paused() public view override returns (bool) { - return __paused; - } - /** * @notice Set the auction time buffer. * @dev Only callable by the owner. From f15ea48f5003f70ab142451d1c7539f7bd1b571c Mon Sep 17 00:00:00 2001 From: davidbrai Date: Fri, 13 Oct 2023 15:31:27 +0000 Subject: [PATCH 114/115] burn: change times when burn is possible to use burn windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit following feedback from 9999, we're doing a small modification to the burn spec in regards to when it can be invoked 🔥 The problem 9999 helped us identify: Let's say the current nextBurnNounId is 900 If at noun id 900 there is no excess ETH (which is also the point of the mechanism), then at any moment from then when there is excess ETH, a burn can be invoked immediately, because the current noun id is > 900 We think this defeats the purpose of predictable burn times The suggested fix: There will be a burn window of a few nouns, e.g. 3, during which a burn can be invoked If there was no burn in that window, then the next window will be at a multiple of nounIdsBetweenBurns Example: If we set the initial burn noun id to 1000, and the nounIdsBetweenBurns to 100, and the burn window to 3 The first burn can happen while the auctioned noun has id in the range 1000-1003. Only one burn can happen in a window Next burns can happen in the windows 1100-1103, 1200-1203, etc. regardless of whether a burn happened or not --- .../contracts/governance/ExcessETHBurner.sol | 111 +++++++++++++---- ...ployExecutorV3AndExcessETHBurnerBase.s.sol | 20 +-- ...yExecutorV3AndExcessETHBurnerMainnet.s.sol | 8 +- ...yExecutorV3AndExcessETHBurnerSepolia.s.sol | 8 +- .../foundry/BurnUpgradeForkMainnetTest.t.sol | 31 +++-- .../foundry/governance/ExcessETHBurner.t.sol | 117 ++++++++++++++---- .../governance/NounsDAOExecutorV3.t.sol | 25 ++-- .../helpers/DeployUtilsExcessETHBurner.sol | 8 +- 8 files changed, 235 insertions(+), 93 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol index 63f7cb2d0f..b6835f4050 100644 --- a/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol +++ b/packages/nouns-contracts/contracts/governance/ExcessETHBurner.sol @@ -57,13 +57,14 @@ contract ExcessETHBurner is Ownable { */ event AuctionSet(address oldAuction, address newAuction); - event NextBurnNounIDSet(uint128 nextBurnNounID, uint128 newNextBurnNounID); - event MinNewNounsBetweenBurnsSet(uint128 minNewNounsBetweenBurns, uint128 newMinNewNounsBetweenBurns); + 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 previousBurnNounId, uint128 nextBurnNounId); + event Burn(uint256 amount, uint128 currentBurnWindowStart, uint128 currentNounId, uint128 newInitialBurnNounId); /** * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ @@ -78,8 +79,9 @@ contract ExcessETHBurner is Ownable { IERC20 public immutable wETH; IERC20 public immutable stETH; IERC20 public immutable rETH; - uint128 public nextBurnNounID; - uint128 public minNewNounsBetweenBurns; + uint64 public initialBurnNounId; + uint64 public nounIdsBetweenBurns; + uint16 public burnWindowSize; uint16 public numberOfPastAuctionsForMeanPrice; /** @@ -89,8 +91,9 @@ contract ExcessETHBurner is Ownable { * @param wETH_ Address of WETH token * @param stETH_ Address of Lido stETH token * @param rETH_ Address of RocketPool RETH token - * @param burnStartNounID_ A burn will be possible only if the currently auctioned Noun has a greater or equal ID - * @param minNewNounsBetweenBurns_ Number of nouns that need to be minted between burns + * @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( @@ -100,8 +103,9 @@ contract ExcessETHBurner is Ownable { IERC20 wETH_, IERC20 stETH_, IERC20 rETH_, - uint128 burnStartNounID_, - uint128 minNewNounsBetweenBurns_, + uint64 initialBurnNounId_, + uint64 nounIdsBetweenBurns_, + uint16 burnWindowSize_, uint16 numberOfPastAuctionsForMeanPrice_ ) { _transferOwnership(owner_); @@ -111,8 +115,9 @@ contract ExcessETHBurner is Ownable { wETH = wETH_; stETH = stETH_; rETH = rETH_; - nextBurnNounID = burnStartNounID_; - minNewNounsBetweenBurns = minNewNounsBetweenBurns_; + initialBurnNounId = initialBurnNounId_; + nounIdsBetweenBurns = nounIdsBetweenBurns_; + burnWindowSize = burnWindowSize_; numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; } @@ -132,22 +137,68 @@ contract ExcessETHBurner is Ownable { * @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) { - uint256 currentNounId = auction.auction().nounId; + uint128 currentNounId = currentlyAuctionedNounId(); // Make sure this is a valid noun id. This will revert if this id doesn't exist auction.nouns().ownerOf(currentNounId); - uint128 currentNextBurnNounID = nextBurnNounID; - if (currentNounId < currentNextBurnNounID) revert NotTimeToBurnYet(); + 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); - uint128 newNextBurnNounId = currentNextBurnNounID + minNewNounsBetweenBurns; - nextBurnNounID = newNextBurnNounId; - emit Burn(amount, currentNextBurnNounID, newNextBurnNounId); + 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_; } /** @@ -219,23 +270,29 @@ contract ExcessETHBurner is Ownable { */ /** - * @notice Set the next Noun ID at which the burn is allowed. - * @param newNextBurnNounID The new next Noun ID at which the burn is allowed. + * @notice Sets `initialBurnNounId` + * Can only be called by owner, which is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. */ - function setNextBurnNounID(uint128 newNextBurnNounID) external onlyOwner { - emit NextBurnNounIDSet(nextBurnNounID, newNextBurnNounID); + function setInitialBurnNounId(uint64 newInitialBurnNounId) external onlyOwner { + emit InitialBurnNounIdSet(initialBurnNounId, newInitialBurnNounId); - nextBurnNounID = newNextBurnNounID; + initialBurnNounId = newInitialBurnNounId; } /** - * @notice Set the minimum number of new Nouns between burns. - * @param newMinNewNounsBetweenBurns The new minimum number of new Nouns between burns. + * @notice Sets `nounIdsBetweenBurns` + * Can only be called by owner, which is assumed to be the NounsDAOExecutorV3 contract, i.e. the Nouns treasury. */ - function setMinNewNounsBetweenBurns(uint128 newMinNewNounsBetweenBurns) external onlyOwner { - emit MinNewNounsBetweenBurnsSet(minNewNounsBetweenBurns, newMinNewNounsBetweenBurns); + function setNounIdsBetweenBurns(uint64 newNounIdsBetweenBurns) external onlyOwner { + emit NounIdsBetweenBurnsSet(nounIdsBetweenBurns, newNounIdsBetweenBurns); + + nounIdsBetweenBurns = newNounIdsBetweenBurns; + } + + function setBurnWindowSize(uint16 newBurnWindowSize) external onlyOwner { + emit BurnWindowSizeSet(burnWindowSize, newBurnWindowSize); - minNewNounsBetweenBurns = newMinNewNounsBetweenBurns; + burnWindowSize = newBurnWindowSize; } /** diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol index 6b42ab0074..4d2f23aa80 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerBase.s.sol @@ -19,8 +19,9 @@ abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { IERC20 public immutable wETH; IERC20 public immutable stETH; IERC20 public immutable rETH; - uint128 public immutable burnStartNounID; - uint128 public immutable minNewNounsBetweenBurns; + uint64 public immutable initialBurnNounId; + uint64 public immutable nounIdsBetweenBurns; + uint16 public immutable burnWindowSize; uint16 public immutable numberOfPastAuctionsForMeanPrice; constructor( @@ -28,8 +29,9 @@ abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { address wETH_, address stETH_, address rETH_, - uint128 burnStartNounID_, - uint128 minNewNounsBetweenBurns_, + uint64 initialBurnNounId_, + uint64 nounIdsBetweenBurns_, + uint16 burnWindowSize_, uint16 numberOfPastAuctionsForMeanPrice_ ) { executorProxy = executorProxy_; @@ -41,8 +43,9 @@ abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { stETH = IERC20(stETH_); rETH = IERC20(rETH_); - burnStartNounID = burnStartNounID_; - minNewNounsBetweenBurns = minNewNounsBetweenBurns_; + initialBurnNounId = initialBurnNounId_; + nounIdsBetweenBurns = nounIdsBetweenBurns_; + burnWindowSize = burnWindowSize_; numberOfPastAuctionsForMeanPrice = numberOfPastAuctionsForMeanPrice_; } @@ -59,8 +62,9 @@ abstract contract DeployExecutorV3AndExcessETHBurnerBase is Script { wETH, stETH, rETH, - burnStartNounID, - minNewNounsBetweenBurns, + initialBurnNounId, + nounIdsBetweenBurns, + burnWindowSize, numberOfPastAuctionsForMeanPrice ); diff --git a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol index 3994e3de61..1dce784167 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerMainnet.s.sol @@ -9,8 +9,9 @@ contract DeployExecutorV3AndExcessETHBurnerMainnet is DeployExecutorV3AndExcessE address constant STETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; address constant RETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; - uint128 BURN_START_NOUN_ID = type(uint128).max; - uint128 MIN_NOUNS_BETWEEN_BURNS = 0; + uint64 BURN_START_NOUN_ID = type(uint64).max; + uint64 NOUNS_BETWEEN_BURNS = 0; + uint16 BURN_WINDOW_SIZE = 3; uint16 MEAN_AUCTION_COUNT = 1; constructor() @@ -20,7 +21,8 @@ contract DeployExecutorV3AndExcessETHBurnerMainnet is DeployExecutorV3AndExcessE STETH, RETH, BURN_START_NOUN_ID, - MIN_NOUNS_BETWEEN_BURNS, + 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 index 787667f10b..679725f03d 100644 --- a/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol +++ b/packages/nouns-contracts/script/executorV3AndExcessETHBurner/DeployExecutorV3AndExcessETHBurnerSepolia.s.sol @@ -9,8 +9,9 @@ contract DeployExecutorV3AndExcessETHBurnerSepolia is DeployExecutorV3AndExcessE address constant STETH = 0x7f96dAEF4A54F6A52613d6272560C2BD25e913B8; address constant RETH = 0xf07dafCC49a9F5E1E73Df6bD6616d0a5bA19e502; - uint128 BURN_START_NOUN_ID = 10; - uint128 MIN_NOUNS_BETWEEN_BURNS = 10; + uint64 BURN_START_NOUN_ID = 10; + uint64 NOUNS_BETWEEN_BURNS = 10; + uint16 BURN_WINDOW_SIZE = 3; uint16 MEAN_AUCTION_COUNT = 10; constructor() @@ -20,7 +21,8 @@ contract DeployExecutorV3AndExcessETHBurnerSepolia is DeployExecutorV3AndExcessE STETH, RETH, BURN_START_NOUN_ID, - MIN_NOUNS_BETWEEN_BURNS, + NOUNS_BETWEEN_BURNS, + BURN_WINDOW_SIZE, MEAN_AUCTION_COUNT ) {} diff --git a/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol b/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol index ab974c85b0..8a1d61e053 100644 --- a/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol +++ b/packages/nouns-contracts/test/foundry/BurnUpgradeForkMainnetTest.t.sol @@ -183,18 +183,20 @@ contract BurnUpgradeForkMainnetTest is Test { } function test_burnerIsOffAtFirst() public { - assertGt(burner.nextBurnNounID(), 1000000); + // 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, 20); + uint256 proposalId = proposeToTurnOnBurn(880, 10, 4, 20); voteAndExecuteProposal(proposalId); - assertEq(burner.nextBurnNounID(), 880); - assertEq(burner.minNewNounsBetweenBurns(), 10); + assertEq(burner.initialBurnNounId(), 880); + assertEq(burner.nounIdsBetweenBurns(), 10); + assertEq(burner.burnWindowSize(), 4); assertEq(burner.numberOfPastAuctionsForMeanPrice(), 20); NounsAuctionHouseV2 ah = NounsAuctionHouseV2(auctionHouseProxy); @@ -230,11 +232,12 @@ contract BurnUpgradeForkMainnetTest is Test { } function proposeToTurnOnBurn( - uint128 nextBurnNoundId, - uint128 minNewNounsBetweenBurns, + uint64 initialBurnNounId, + uint64 nounIdsBetweenBurns, + uint16 burnWindowSize, uint16 numberOfPastAuctionsForMeanPrice ) internal returns (uint256 proposalId) { - uint8 numTxs = 3; + uint8 numTxs = 4; address[] memory targets = new address[](numTxs); uint256[] memory values = new uint256[](numTxs); string[] memory signatures = new string[](numTxs); @@ -243,18 +246,24 @@ contract BurnUpgradeForkMainnetTest is Test { uint256 i = 0; targets[i] = address(burner); values[i] = 0; - signatures[i] = 'setNextBurnNounID(uint128)'; - calldatas[i] = abi.encode(nextBurnNoundId); + signatures[i] = 'setInitialBurnNounId(uint64)'; + calldatas[i] = abi.encode(initialBurnNounId); i = 1; targets[i] = address(burner); values[i] = 0; - signatures[i] = 'setMinNewNounsBetweenBurns(uint128)'; - calldatas[i] = abi.encode(minNewNounsBetweenBurns); + 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); diff --git a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol index cb384f657f..ac689a3b7b 100644 --- a/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/ExcessETHBurner.t.sol @@ -105,26 +105,37 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { NounsDAOExecutorV3 treasury; ExcessETHBurner burner; - uint128 burnStartNounId; - uint128 minNewNounsBetweenBurns; + uint64 burnStartNounId; + uint64 nounsBetweenBurns; + uint16 burnWindowSize; uint16 pastAuctionCount; - event Burn(uint256 amount, uint128 previousBurnNounId, uint128 nextBurnNounId); + event Burn(uint256 amount, uint128 currentBurnWindowStart, uint128 currentNounId, uint128 newInitialBurnNounId); function setUp() public { burnStartNounId = 1; - minNewNounsBetweenBurns = 100; + nounsBetweenBurns = 100; pastAuctionCount = 90; + burnWindowSize = 3; treasury = _deployExecutorV3(address(dao)); - burner = _deployExcessETHBurner(treasury, auction, burnStartNounId, minNewNounsBetweenBurns, pastAuctionCount); + burner = _deployExcessETHBurner( + treasury, + auction, + burnStartNounId, + nounsBetweenBurns, + burnWindowSize, + pastAuctionCount + ); vm.prank(address(treasury)); treasury.setExcessETHBurner(address(burner)); auction.setNounId(burnStartNounId); - nounsToken.mint(address(1), 0); - nounsToken.mint(address(1), 1); + + for (uint256 i; i < 400; i++) { + nounsToken.mint(address(1), i); + } } function test_burnExcessETH_beforeNextBurnNounID_reverts() public { @@ -148,12 +159,51 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { vm.deal(address(treasury), 100 ether); vm.expectEmit(true, true, true, true); - emit Burn(99 ether, 1, 101); + emit Burn(99 ether, 1, 1, 101); uint256 burnedAmount = burner.burnExcessETH(); assertEq(burnedAmount, 99 ether); assertEq(address(treasury).balance, 1 ether); - assertEq(burner.nextBurnNounID(), burnStartNounId + minNewNounsBetweenBurns); + 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 { @@ -170,18 +220,19 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { assertEq(burner.burnExcessETH(), 99 ether); assertEq(address(treasury).balance, 1 ether); - assertEq(burner.nextBurnNounID(), burnStartNounId + minNewNounsBetweenBurns); + assertEq(burner.initialBurnNounId(), 101); vm.deal(address(treasury), 100 ether); vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); burner.burnExcessETH(); - auction.setNounId(burner.nextBurnNounID()); - nounsToken.mint(address(1), burner.nextBurnNounID()); + auction.setNounId(2); + vm.expectRevert(ExcessETHBurner.NotTimeToBurnYet.selector); + burner.burnExcessETH(); - uint128 expectedNextBurnID = burner.nextBurnNounID() + minNewNounsBetweenBurns; + auction.setNounId(101); assertEq(burner.burnExcessETH(), 99 ether); - assertEq(burner.nextBurnNounID(), expectedNextBurnID); + assertEq(burner.initialBurnNounId(), 201); } function test_burnExcessETH_givenExpectedValueGreaterThanTreasury_reverts() public { @@ -254,32 +305,32 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { burner.burnExcessETH(); } - function test_setNextBurnNounID_revertsForNonOwner() public { + function test_setInitialBurnNounId_revertsForNonOwner() public { vm.expectRevert('Ownable: caller is not the owner'); - burner.setNextBurnNounID(1); + burner.setInitialBurnNounId(1); } - function test_setNextBurnNounID_worksForOwner() public { - assertTrue(burner.nextBurnNounID() != 142); + function test_setInitialBurnNounId_worksForOwner() public { + assertTrue(burner.initialBurnNounId() != 142); vm.prank(address(treasury)); - burner.setNextBurnNounID(142); + burner.setInitialBurnNounId(142); - assertEq(burner.nextBurnNounID(), 142); + assertEq(burner.initialBurnNounId(), 142); } - function test_setMinNewNounsBetweenBurns_revertsForNonOwner() public { + function test_setNounIdsBetweenBurns_revertsForNonOwner() public { vm.expectRevert('Ownable: caller is not the owner'); - burner.setMinNewNounsBetweenBurns(1); + burner.setNounIdsBetweenBurns(1); } - function test_setMinNewNounsBetweenBurns_worksForOwner() public { - assertTrue(burner.minNewNounsBetweenBurns() != 142); + function test_setNounIdsBetweenBurns_worksForOwner() public { + assertTrue(burner.nounIdsBetweenBurns() != 142); vm.prank(address(treasury)); - burner.setMinNewNounsBetweenBurns(142); + burner.setNounIdsBetweenBurns(142); - assertEq(burner.minNewNounsBetweenBurns(), 142); + assertEq(burner.nounIdsBetweenBurns(), 142); } function test_setNumberOfPastAuctionsForMeanPrice_revertsForNonOwner() public { @@ -302,6 +353,20 @@ contract ExcessETHBurnerTest is DeployUtilsExcessETHBurner { 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); } diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index bc5a653101..d0046e96f9 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -102,7 +102,7 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { vm.roll(block.number + 1); } - function test_upgardeViaProposal_andBurnHappyFlow() public { + function test_upgradeViaProposal_andBurnHappyFlow() public { uint256 meanPrice = 0.1 ether; generateAuctionHistory(90, meanPrice); @@ -113,14 +113,15 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { getProposalToExecution(proposalId); vm.stopPrank(); - uint128 currentNounID = auction.auction().nounId; + assertEq(auction.auction().nounId, 102); ExcessETHBurner burner = _deployExcessETHBurner( NounsDAOExecutorV3(treasury), INounsAuctionHouseV2(dao.nouns().minter()), - currentNounID + 100, - 100, - 90 + 202, // burnStartNounID + 100, // nounsBetweenBurns + 3, // burnWindowSize + 90 // pastAuctionCount ); vm.startPrank(nouner); @@ -132,23 +133,23 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { burner.burnExcessETH(); auction.settleCurrentAndCreateNewAuction(); - generateAuctionHistory(100, meanPrice); + generateAuctionHistory(89, meanPrice); + assertEq(auction.auction().nounId, 202); vm.expectRevert(ExcessETHBurner.NoExcessToBurn.selector); burner.burnExcessETH(); vm.deal(address(treasury), 100 ether); - // adjustedSupply: 214 + // adjustedSupply: 202 // meanPrice: 0.1 ether - // expected value: 214 * 0.1 = 21.4 ETH + // expected value: 202 * 0.1 = 20.2 ETH // treasury size: 100 ETH - // excess: 100 - 21.4 = 78.6 ETH + // excess: 100 - 20.2 = 79.8 ETH vm.expectEmit(true, true, true, true); - emit ETHBurned(78.6 ether); - + emit ETHBurned(79.8 ether); uint256 burnedETH = burner.burnExcessETH(); - assertEq(burnedETH, 78.6 ether); + assertEq(burnedETH, 79.8 ether); } function getProposalToExecution(uint256 proposalId) internal { diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol index a57ce257f7..098808843c 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsExcessETHBurner.sol @@ -22,8 +22,9 @@ abstract contract DeployUtilsExcessETHBurner is DeployUtilsV3 { function _deployExcessETHBurner( NounsDAOExecutorV3 owner, INounsAuctionHouseV2 auction, - uint128 burnStartNounID, - uint128 minNewNounsBetweenBurns, + uint64 burnStartNounID, + uint64 nounsBetweenBurns, + uint16 burnWindowSize, uint16 pastAuctionCount ) internal returns (ExcessETHBurner burner) { WETH weth = new WETH(); @@ -38,7 +39,8 @@ abstract contract DeployUtilsExcessETHBurner is DeployUtilsV3 { stETH, rETH, burnStartNounID, - minNewNounsBetweenBurns, + nounsBetweenBurns, + burnWindowSize, pastAuctionCount ); } From 5d1bea64af093f87459c1cd7bfc28556173b8e7a Mon Sep 17 00:00:00 2001 From: davidbrai Date: Thu, 19 Oct 2023 14:47:41 +0000 Subject: [PATCH 115/115] gas: optimize storage warmup function --- packages/nouns-contracts/.gas-snapshot | 25 ++++++++++--------- .../contracts/NounsAuctionHouseV2.sol | 12 ++++++--- .../NounsAuctionHouseGasSnapshot.t.sol | 8 ++++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/nouns-contracts/.gas-snapshot b/packages/nouns-contracts/.gas-snapshot index 0c32aba6e9..21eb281055 100644 --- a/packages/nouns-contracts/.gas-snapshot +++ b/packages/nouns-contracts/.gas-snapshot @@ -1,18 +1,19 @@ -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createOneBid() (gas: 34963) -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_createTwoBids() (gas: 93423) -NounsAuctionHouseV2WarmedUp_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 232686) -NounsAuctionHouseV2_GasSnapshot:test_createOneBid() (gas: 34963) -NounsAuctionHouseV2_GasSnapshot:test_createTwoBids() (gas: 93423) -NounsAuctionHouseV2_GasSnapshot:test_settleCurrentAndCreateNewAuction() (gas: 249786) -NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getPrices_90() (gas: 310063) -NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getPrices_range_90() (gas: 300578) -NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getSettlements_90() (gas: 385762) -NounsAuctionHouseV2_HistoricPrices_GasSnapshot:test_getSettlements_range_90() (gas: 378168) +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: 528754) -NounsDAOLogic_GasSnapshot_V2_propose:test_propose_shortDescription() (gas: 398387) +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: 83585) NounsDAOLogic_GasSnapshot_V2_vote:test_castVote_against() (gas: 82930) NounsDAOLogic_GasSnapshot_V2_vote:test_castVote_lastMinuteFor() (gas: 83481) diff --git a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol index 96a034e432..a335e7ec1d 100644 --- a/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol +++ b/packages/nouns-contracts/contracts/NounsAuctionHouseV2.sol @@ -332,9 +332,15 @@ contract NounsAuctionHouseV2 is * @param nounIds The list of Noun IDs whose settlement slot to warm up. */ function warmUpSettlementState(uint256[] calldata nounIds) external { - for (uint256 i = 0; i < nounIds.length; ++i) { - if (settlementHistory[nounIds[i]].blockTimestamp == 0) { - settlementHistory[nounIds[i]] = SettlementState({ blockTimestamp: 1, amount: 0, winner: address(0) }); + 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; } } } diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol index 99027eb12c..2aaeec8633 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -109,4 +109,12 @@ contract NounsAuctionHouseV2_HistoricPrices_GasSnapshot is NounsAuctionHouseBase 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); + } }