From dd11029c707a1e72f0bb2280e06aaa817e837b95 Mon Sep 17 00:00:00 2001 From: davidbrai Date: Tue, 10 Oct 2023 14:32:24 +0000 Subject: [PATCH 1/9] poc for requiring noun to be at least a certain age to fork age is counted in noun ids --- .../contracts/governance/NounsDAOInterfaces.sol | 2 ++ .../contracts/governance/NounsDAOLogicV3.sol | 4 ++++ .../contracts/governance/NounsDAOV3Admin.sol | 11 +++++++++++ .../contracts/governance/fork/NounsDAOV3Fork.sol | 9 +++++++++ 4 files changed, 26 insertions(+) diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol b/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol index 8fb0b4d3d4..e605fc3e78 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOInterfaces.sol @@ -696,6 +696,8 @@ contract NounsDAOStorageV3 { INounsDAOForkEscrow forkEscrow; /// @notice address of the DAO's fork deployer contract IForkDAODeployer forkDAODeployer; + /// @notice the age of a noun, in noun ids, for a noun to be allowed to fork + uint16 nounAgeRequiredToFork; /// @notice ERC20 tokens to include when sending funds to a deployed fork address[] erc20TokensToIncludeInFork; /// @notice The treasury contract of the last deployed fork diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol index 00c5ccdc3b..9f4df01f82 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol @@ -803,6 +803,10 @@ contract NounsDAOLogicV3 is NounsDAOStorageV3, NounsDAOEventsV3 { ds._setForkThresholdBPS(newForkThresholdBPS); } + function _setNounAgeRequiredToFork(uint16 newNounAgeRequiredToFork) external { + ds._setNounAgeRequiredToFork(newNounAgeRequiredToFork); + } + /** * @notice Admin function for setting the proposal id at which vote snapshots start using the voting start block * instead of the proposal creation block. diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol b/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol index 6102986b77..4a36a45604 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOV3Admin.sol @@ -108,6 +108,8 @@ library NounsDAOV3Admin { /// @notice Emitted when the main timelock, timelockV1 and admin are set event TimelocksAndAdminSet(address timelock, address timelockV1, address admin); + event NounAgeRequiredToForkSet(uint16 oldNounAgeRequiredToFork, uint16 newNounAgeRequiredToFork); + /// @notice The minimum setable proposal threshold uint256 public constant MIN_PROPOSAL_THRESHOLD_BPS = 1; // 1 basis point or 0.01% @@ -557,6 +559,15 @@ library NounsDAOV3Admin { ds.forkThresholdBPS = newForkThresholdBPS; } + function _setNounAgeRequiredToFork(NounsDAOStorageV3.StorageV3 storage ds, uint16 newNounAgeRequiredToFork) + external + onlyAdmin(ds) + { + emit NounAgeRequiredToForkSet(ds.nounAgeRequiredToFork, newNounAgeRequiredToFork); + + ds.nounAgeRequiredToFork = newNounAgeRequiredToFork; + } + /** * @notice Admin function for setting the timelocks and admin * @param timelock the new timelock contract diff --git a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol index d87ffc701a..488b446f82 100644 --- a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol +++ b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol @@ -20,6 +20,7 @@ pragma solidity ^0.8.19; import { NounsDAOStorageV3, INounsDAOForkEscrow, INounsDAOExecutorV2 } from '../NounsDAOInterfaces.sol'; import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import { NounsTokenFork } from './newdao/token/NounsTokenFork.sol'; +import { INounsAuctionHouseV2 } from '../../interfaces/INounsAuctionHouseV2.sol'; library NounsDAOV3Fork { error ForkThresholdNotMet(); @@ -27,6 +28,7 @@ library NounsDAOV3Fork { error ForkPeriodActive(); error AdminOnly(); error UseAlternativeWithdrawFunction(); + error NounIdNotOldEnough(); /// @notice Emitted when someones adds nouns to the fork escrow event EscrowedToFork( @@ -81,6 +83,7 @@ library NounsDAOV3Fork { INounsDAOForkEscrow forkEscrow = ds.forkEscrow; for (uint256 i = 0; i < tokenIds.length; i++) { + checkNounIdIsAllowedToFork(ds, tokenIds[i]); ds.nouns.safeTransferFrom(msg.sender, address(forkEscrow), tokenIds[i]); } @@ -151,6 +154,7 @@ library NounsDAOV3Fork { sendProRataTreasury(ds, ds.forkDAOTreasury, tokenIds.length, adjustedTotalSupply(ds)); for (uint256 i = 0; i < tokenIds.length; i++) { + checkNounIdIsAllowedToFork(ds, tokenIds[i]); ds.nouns.transferFrom(msg.sender, timelock, tokenIds[i]); } @@ -216,6 +220,11 @@ library NounsDAOV3Fork { return ds.forkEscrow.numTokensInEscrow(); } + function checkNounIdIsAllowedToFork(NounsDAOStorageV3.StorageV3 storage ds, uint256 tokenId) internal view { + uint256 auctionedNounId = INounsAuctionHouseV2(ds.nouns.minter()).auction().nounId; + if (tokenId < auctionedNounId - ds.nounAgeRequiredToFork) revert NounIdNotOldEnough(); + } + /** * @notice Returns the number of nouns in supply minus nouns owned by the DAO, i.e. held in the treasury or in an * escrow after it has closed. From a042c337f8377b606d82a8ad10e1d5d78e92fb04 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 11 Oct 2023 15:48:29 -0500 Subject: [PATCH 2/9] dont check noun age if gov param not set --- .../contracts/governance/fork/NounsDAOV3Fork.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol index 488b446f82..d0cb582f8f 100644 --- a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol +++ b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol @@ -221,6 +221,8 @@ library NounsDAOV3Fork { } function checkNounIdIsAllowedToFork(NounsDAOStorageV3.StorageV3 storage ds, uint256 tokenId) internal view { + if (ds.nounAgeRequiredToFork == 0) return; + uint256 auctionedNounId = INounsAuctionHouseV2(ds.nouns.minter()).auction().nounId; if (tokenId < auctionedNounId - ds.nounAgeRequiredToFork) revert NounIdNotOldEnough(); } From 275aaf46ddef485457ed6822070e76a2937e8d16 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 12 Oct 2023 13:29:59 -0500 Subject: [PATCH 3/9] gas: cache auction ID and nounAgeRequiredToFork --- .../governance/fork/NounsDAOV3Fork.sol | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol index d0cb582f8f..349199c110 100644 --- a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol +++ b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol @@ -82,8 +82,10 @@ library NounsDAOV3Fork { if (isForkPeriodActive(ds)) revert ForkPeriodActive(); INounsDAOForkEscrow forkEscrow = ds.forkEscrow; + uint16 nounAgeRequiredToFork = ds.nounAgeRequiredToFork; + uint256 auctionedNounId = INounsAuctionHouseV2(ds.nouns.minter()).auction().nounId; for (uint256 i = 0; i < tokenIds.length; i++) { - checkNounIdIsAllowedToFork(ds, tokenIds[i]); + checkNounIdIsAllowedToFork(auctionedNounId, nounAgeRequiredToFork, tokenIds[i]); ds.nouns.safeTransferFrom(msg.sender, address(forkEscrow), tokenIds[i]); } @@ -153,8 +155,10 @@ library NounsDAOV3Fork { address timelock = address(ds.timelock); sendProRataTreasury(ds, ds.forkDAOTreasury, tokenIds.length, adjustedTotalSupply(ds)); + uint16 nounAgeRequiredToFork = ds.nounAgeRequiredToFork; + uint256 auctionedNounId = INounsAuctionHouseV2(ds.nouns.minter()).auction().nounId; for (uint256 i = 0; i < tokenIds.length; i++) { - checkNounIdIsAllowedToFork(ds, tokenIds[i]); + checkNounIdIsAllowedToFork(auctionedNounId, nounAgeRequiredToFork, tokenIds[i]); ds.nouns.transferFrom(msg.sender, timelock, tokenIds[i]); } @@ -220,11 +224,13 @@ library NounsDAOV3Fork { return ds.forkEscrow.numTokensInEscrow(); } - function checkNounIdIsAllowedToFork(NounsDAOStorageV3.StorageV3 storage ds, uint256 tokenId) internal view { - if (ds.nounAgeRequiredToFork == 0) return; - - uint256 auctionedNounId = INounsAuctionHouseV2(ds.nouns.minter()).auction().nounId; - if (tokenId < auctionedNounId - ds.nounAgeRequiredToFork) revert NounIdNotOldEnough(); + function checkNounIdIsAllowedToFork( + uint256 auctionedNounId, + uint16 nounAgeRequiredToFork, + uint256 tokenId + ) internal pure { + if (nounAgeRequiredToFork == 0) return; + if (tokenId < auctionedNounId - nounAgeRequiredToFork) revert NounIdNotOldEnough(); } /** From 9c9b15ee0b90e76bcfb8aaecb1738a0e177386d9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 12 Oct 2023 15:15:00 -0500 Subject: [PATCH 4/9] fork: fix noun age check bug --- .../contracts/governance/fork/NounsDAOV3Fork.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol index 349199c110..b208964c52 100644 --- a/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol +++ b/packages/nouns-contracts/contracts/governance/fork/NounsDAOV3Fork.sol @@ -229,8 +229,7 @@ library NounsDAOV3Fork { uint16 nounAgeRequiredToFork, uint256 tokenId ) internal pure { - if (nounAgeRequiredToFork == 0) return; - if (tokenId < auctionedNounId - nounAgeRequiredToFork) revert NounIdNotOldEnough(); + if (tokenId >= auctionedNounId - nounAgeRequiredToFork) revert NounIdNotOldEnough(); } /** From cb253aed70d8d9d2bdc7414f76b49546723b51aa Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 12 Oct 2023 15:15:30 -0500 Subject: [PATCH 5/9] dao: add fork required noun age getter --- .../nouns-contracts/contracts/governance/NounsDAOLogicV3.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol b/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol index 9f4df01f82..a4c42665d0 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOLogicV3.sol @@ -1012,6 +1012,10 @@ contract NounsDAOLogicV3 is NounsDAOStorageV3, NounsDAOEventsV3 { return ds.forkDAODeployer; } + function nounAgeRequiredToFork() public view returns (uint16) { + return ds.nounAgeRequiredToFork; + } + function forkEndTimestamp() public view returns (uint256) { return ds.forkEndTimestamp; } From 5b9d8102546c80c6f2ebd6beeaacf868bae8bb7b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 12 Oct 2023 15:16:19 -0500 Subject: [PATCH 6/9] add deploy and upgrade proposal scripts --- .../script/DAOLogicV3p1/DeployDAOV3p1.s.sol | 15 ++++++++ .../ProposeDAOV3p1UpgradeMainnet.s.sol | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 packages/nouns-contracts/script/DAOLogicV3p1/DeployDAOV3p1.s.sol create mode 100644 packages/nouns-contracts/script/DAOLogicV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol diff --git a/packages/nouns-contracts/script/DAOLogicV3p1/DeployDAOV3p1.s.sol b/packages/nouns-contracts/script/DAOLogicV3p1/DeployDAOV3p1.s.sol new file mode 100644 index 0000000000..4de61d6900 --- /dev/null +++ b/packages/nouns-contracts/script/DAOLogicV3p1/DeployDAOV3p1.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; +import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; + +contract DeployDAOV3p1 is Script { + function run() public returns (NounsDAOLogicV3 newLogic) { + uint256 deployerKey = vm.envUint('DEPLOYER_PRIVATE_KEY'); + + vm.startBroadcast(deployerKey); + newLogic = new NounsDAOLogicV3(); + vm.stopBroadcast(); + } +} diff --git a/packages/nouns-contracts/script/DAOLogicV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol b/packages/nouns-contracts/script/DAOLogicV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol new file mode 100644 index 0000000000..56e368e056 --- /dev/null +++ b/packages/nouns-contracts/script/DAOLogicV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Script.sol'; +import { NounsDAOLogicV1 } from '../../contracts/governance/NounsDAOLogicV1.sol'; +import { NounsDAOForkEscrow } from '../../contracts/governance/fork/NounsDAOForkEscrow.sol'; +import { ForkDAODeployer } from '../../contracts/governance/fork/ForkDAODeployer.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +contract ProposeDAOV3p1UpgradeMainnet is Script { + NounsDAOLogicV1 public constant NOUNS_DAO_PROXY_MAINNET = + NounsDAOLogicV1(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d); + + function run() public returns (uint256 proposalId) { + uint256 proposerKey = vm.envUint('PROPOSER_KEY'); + address newDAOImpl = vm.envAddress('DAO_V3p1_IMPL'); + string memory description = vm.readFile(vm.envString('PROPOSAL_DESCRIPTION_FILE')); + + 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); + + uint256 i = 0; + targets[i] = address(NOUNS_DAO_PROXY_MAINNET); + values[i] = 0; + signatures[i] = '_setImplementation(address)'; + calldatas[i] = abi.encode(newDAOImpl); + + proposalId = NOUNS_DAO_PROXY_MAINNET.propose(targets, values, signatures, calldatas, description); + console.log('Proposed proposalId: %d', proposalId); + + vm.stopBroadcast(); + } +} From a5cfd81bd6a0aec51f0bdabb002c35cfd4abecc1 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 12 Oct 2023 15:17:37 -0500 Subject: [PATCH 7/9] simplify AH upgrader API --- .../NounsAuctionHouseGasSnapshot.t.sol | 13 ++++++------ .../test/foundry/NounsAuctionHouseV2.t.sol | 20 +++++++++---------- .../governance/NounsDAOExecutorV3.t.sol | 2 +- .../foundry/helpers/AuctionHouseUpgrader.sol | 10 +++++----- .../test/foundry/helpers/DeployUtils.sol | 4 ++-- .../test/foundry/helpers/DeployUtilsV3.sol | 2 +- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol index 99027eb12c..95c9fa41aa 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseGasSnapshot.t.sol @@ -14,19 +14,20 @@ abstract contract NounsAuctionHouseBaseTest is DeployUtils { INounsToken nouns; address noundersDAO = makeAddr('noundersDAO'); address owner = makeAddr('owner'); - NounsAuctionHouseProxy auctionHouseProxy; + address auctionHouseProxy; NounsAuctionHouseProxyAdmin proxyAdmin; uint256[] nounIds; function setUp() public virtual { - ( - NounsAuctionHouseProxy auctionHouseProxy_, - NounsAuctionHouseProxyAdmin proxyAdmin_ - ) = _deployAuctionHouseV1AndToken(owner, noundersDAO, address(0)); + (address auctionHouseProxy_, NounsAuctionHouseProxyAdmin proxyAdmin_) = _deployAuctionHouseV1AndToken( + owner, + noundersDAO, + address(0) + ); auctionHouseProxy = auctionHouseProxy_; proxyAdmin = proxyAdmin_; - auctionHouse = INounsAuctionHouse(address(auctionHouseProxy_)); + auctionHouse = INounsAuctionHouse(auctionHouseProxy_); vm.prank(owner); auctionHouse.unpause(); diff --git a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol index 3635419162..ddaaa10546 100644 --- a/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsAuctionHouseV2.t.sol @@ -22,7 +22,7 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { NounsAuctionHouseV2 auction; function setUp() public { - (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + (address auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( owner, noundersDAO, minter @@ -30,7 +30,7 @@ contract NounsAuctionHouseV2TestBase is Test, DeployUtils { AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); - auction = NounsAuctionHouseV2(address(auctionProxy)); + auction = NounsAuctionHouseV2(auctionProxy); vm.prank(owner); auction.unpause(); @@ -161,13 +161,13 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_settleAuction_revertsWhenAuctionHasntBegunYet() public { - (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + (address auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( owner, noundersDAO, minter ); AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); - auction = NounsAuctionHouseV2(address(auctionProxy)); + auction = NounsAuctionHouseV2(auctionProxy); vm.expectRevert("Auction hasn't begun"); auction.settleAuction(); @@ -185,12 +185,12 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_V2Migration_works() public { - (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + (address auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( owner, noundersDAO, minter ); - NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(auctionProxy)); + NounsAuctionHouse auctionV1 = NounsAuctionHouse(auctionProxy); vm.prank(owner); auctionV1.unpause(); vm.roll(block.number + 1); @@ -207,7 +207,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); - NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(auctionProxy); INounsAuctionHouseV2.AuctionV2 memory auctionV2State = auctionV2.auction(); @@ -229,12 +229,12 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { } function test_V2Migration_copiesPausedWhenTrue() public { - (NounsAuctionHouseProxy auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( + (address auctionProxy, NounsAuctionHouseProxyAdmin proxyAdmin) = _deployAuctionHouseV1AndToken( owner, noundersDAO, minter ); - NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(auctionProxy)); + NounsAuctionHouse auctionV1 = NounsAuctionHouse(auctionProxy); vm.prank(owner); auctionV1.unpause(); vm.roll(block.number + 1); @@ -251,7 +251,7 @@ contract NounsAuctionHouseV2Test is NounsAuctionHouseV2TestBase { AuctionHouseUpgrader.upgradeAuctionHouse(owner, proxyAdmin, auctionProxy); - NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(address(auctionProxy)); + NounsAuctionHouseV2 auctionV2 = NounsAuctionHouseV2(auctionProxy); assertEq(auctionV2.paused(), true); } diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index bc5a653101..3e746ef285 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -185,7 +185,7 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { AuctionHouseUpgrader.upgradeAuctionHouse( treasury, NounsAuctionHouseProxyAdmin(proxyAdminAddress), - NounsAuctionHouseProxy(payable(dao.nouns().minter())) + dao.nouns().minter() ); } diff --git a/packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol b/packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol index 6c70290b5c..d7beda47e7 100644 --- a/packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol +++ b/packages/nouns-contracts/test/foundry/helpers/AuctionHouseUpgrader.sol @@ -14,9 +14,9 @@ library AuctionHouseUpgrader { function upgradeAuctionHouse( address owner, NounsAuctionHouseProxyAdmin proxyAdmin, - NounsAuctionHouseProxy proxy + address proxy ) internal { - NounsAuctionHouse auctionV1 = NounsAuctionHouse(address(proxy)); + NounsAuctionHouse auctionV1 = NounsAuctionHouse(proxy); NounsAuctionHouseV2 newLogic = new NounsAuctionHouseV2( auctionV1.nouns(), @@ -30,10 +30,10 @@ library AuctionHouseUpgrader { // 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)); + proxyAdmin.upgrade(NounsAuctionHouseProxy(payable(proxy)), address(migratorLogic)); + NounsAuctionHousePreV2Migration migrator = NounsAuctionHousePreV2Migration(proxy); migrator.migrate(); - proxyAdmin.upgrade(proxy, address(newLogic)); + proxyAdmin.upgrade(NounsAuctionHouseProxy(payable(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 3166412427..27fe263035 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -37,7 +37,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { address owner, address noundersDAO, address minter - ) internal returns (NounsAuctionHouseProxy, NounsAuctionHouseProxyAdmin) { + ) internal returns (address, NounsAuctionHouseProxyAdmin) { NounsAuctionHouse logic = new NounsAuctionHouse(); NounsToken token = deployToken(noundersDAO, minter); WETH weth = new WETH(); @@ -59,7 +59,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { auction.transferOwnership(owner); token.setMinter(address(proxy)); - return (proxy, admin); + return (address(proxy), admin); } uint32 constant LAST_MINUTE_BLOCKS = 10; diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol index 45c8d60703..db607be492 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol @@ -135,7 +135,7 @@ abstract contract DeployUtilsV3 is DeployUtils { NounsAuctionHouse(address(auctionProxy)).initialize(nounsToken, makeAddr('weth'), 2, 0, 1, 10 minutes); vm.prank(address(timelock)); - AuctionHouseUpgrader.upgradeAuctionHouse(address(timelock), auctionAdmin, auctionProxy); + AuctionHouseUpgrader.upgradeAuctionHouse(address(timelock), auctionAdmin, address(auctionProxy)); vm.prank(address(timelock)); timelock.setPendingAdmin(address(dao)); From 774f85b4bb1bb5b96bedf0e71967202e5b7eee01 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 12 Oct 2023 17:33:35 -0500 Subject: [PATCH 8/9] fix DAO tests they used to mint by pranking as minter but with the new noun age logic we need to increment noun ID on auction so tests are now minting via auctions instead of pranking --- .../ForkBlocksProposalExecution.t.sol | 6 +- .../NounsDAOLogicV3BaseTest.sol | 28 +++++- .../NounsDAOLogicV3/NounsDAOV3Fork.t.sol | 10 ++- .../governance/NounsDAOExecutorV3.t.sol | 10 +-- .../governance/fork/ForkingEndToEnd.t.sol | 53 ++++++----- .../governance/fork/NounsDAOLogicV1Fork.t.sol | 89 +++++++++++++------ .../test/foundry/helpers/DeployUtilsV3.sol | 3 + 7 files changed, 129 insertions(+), 70 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/ForkBlocksProposalExecution.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/ForkBlocksProposalExecution.t.sol index fd55572337..10319adb33 100644 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/ForkBlocksProposalExecution.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/ForkBlocksProposalExecution.t.sol @@ -12,11 +12,7 @@ abstract contract ExecutableProposalState is NounsDAOLogicV3BaseTest { function setUp() public virtual override { super.setUp(); - vm.startPrank(minter); - nounsToken.mint(); - nounsToken.transferFrom(minter, user, 1); - vm.stopPrank(); - vm.roll(block.number + 1); + mintTo(user); // prep an executable proposal proposalId = propose(user, makeAddr('target'), 0, '', '', ''); diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOLogicV3BaseTest.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOLogicV3BaseTest.sol index 0aba1f9931..1b27edcab5 100644 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOLogicV3BaseTest.sol +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOLogicV3BaseTest.sol @@ -14,6 +14,11 @@ import { NounsSeeder } from '../../../contracts/NounsSeeder.sol'; import { IProxyRegistry } from '../../../contracts/external/opensea/IProxyRegistry.sol'; import { NounsDAOExecutorV2 } from '../../../contracts/governance/NounsDAOExecutorV2.sol'; import { NounsDAOForkEscrow } from '../../../contracts/governance/fork/NounsDAOForkEscrow.sol'; +import { INounsAuctionHouseV2 } from '../../../contracts/interfaces/INounsAuctionHouseV2.sol'; + +interface INounsAuctionHouseForTest is INounsAuctionHouseV2 { + function owner() external view returns (address); +} abstract contract NounsDAOLogicV3BaseTest is Test, DeployUtilsV3, SigUtils { event ProposalUpdated( @@ -93,11 +98,26 @@ abstract contract NounsDAOLogicV3BaseTest is Test, DeployUtilsV3, SigUtils { } function mintTo(address to) internal returns (uint256 tokenID) { - vm.startPrank(minter); - tokenID = nounsToken.mint(); - nounsToken.transferFrom(minter, to, tokenID); - vm.stopPrank(); + INounsAuctionHouseForTest ah = INounsAuctionHouseForTest(minter); + + uint256 ownerBalanceBefore = ah.owner().balance; + + if (block.timestamp >= ah.auction().endTime) { + ah.settleCurrentAndCreateNewAuction(); + } + + tokenID = ah.auction().nounId; + + ah.createBid{ value: 1 ether }(tokenID); + vm.warp(block.timestamp + ah.auction().endTime + 1); + ah.settleCurrentAndCreateNewAuction(); + + nounsToken.transferFrom(address(this), to, tokenID); vm.roll(block.number + 1); + + // Reverting treasury balance to prevent auction revenue from interfering with existing tests + // that previous used minting without auctions + vm.deal(ah.owner(), ownerBalanceBefore); } function propose( diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOV3Fork.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOV3Fork.t.sol index 8520e1dce7..f5d0e34595 100644 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOV3Fork.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/NounsDAOV3Fork.t.sol @@ -34,12 +34,14 @@ abstract contract DAOForkZeroState is NounsDAOLogicV3BaseTest { erc20Mock.mint(address(timelock), 300e18); // Mint total of 20 tokens. 18 to token holder, 2 to nounders - vm.startPrank(minter); while (nounsToken.totalSupply() < 20) { - nounsToken.mint(); - nounsToken.transferFrom(minter, tokenHolder, nounsToken.totalSupply() - 1); + mintTo(tokenHolder); } - vm.stopPrank(); + + address nounders = nounsToken.noundersDAO(); + vm.prank(nounders); + nounsToken.transferFrom(nounders, tokenHolder, 10); + assertEq(dao.nouns().balanceOf(tokenHolder), 18); escrow = dao.forkEscrow(); diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol index 3e746ef285..df000519eb 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOExecutorV3.t.sol @@ -139,16 +139,16 @@ contract NounsDAOExecutorV3_UpgradeTest is DeployUtilsExcessETHBurner { vm.deal(address(treasury), 100 ether); - // adjustedSupply: 214 + // adjustedSupply: 215 // meanPrice: 0.1 ether - // expected value: 214 * 0.1 = 21.4 ETH + // expected value: 215 * 0.1 = 21.5 ETH // treasury size: 100 ETH - // excess: 100 - 21.4 = 78.6 ETH + // excess: 100 - 21.5 = 78.5 ETH vm.expectEmit(true, true, true, true); - emit ETHBurned(78.6 ether); + emit ETHBurned(78.5 ether); uint256 burnedETH = burner.burnExcessETH(); - assertEq(burnedETH, 78.6 ether); + assertEq(burnedETH, 78.5 ether); } function getProposalToExecution(uint256 proposalId) internal { diff --git a/packages/nouns-contracts/test/foundry/governance/fork/ForkingEndToEnd.t.sol b/packages/nouns-contracts/test/foundry/governance/fork/ForkingEndToEnd.t.sol index 081a10cb68..4415c18abb 100644 --- a/packages/nouns-contracts/test/foundry/governance/fork/ForkingEndToEnd.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/fork/ForkingEndToEnd.t.sol @@ -12,6 +12,7 @@ import { NounsDAOLogicV1Fork } from '../../../../contracts/governance/fork/newda import { NounsAuctionHouseFork } from '../../../../contracts/governance/fork/newdao/NounsAuctionHouseFork.sol'; import { NounsTokenLike } from '../../../../contracts/governance/NounsDAOInterfaces.sol'; import { INounsAuctionHouse } from '../../../../contracts/interfaces/INounsAuctionHouse.sol'; +import { INounsAuctionHouseV2 } from '../../../../contracts/interfaces/INounsAuctionHouseV2.sol'; contract ForkingHappyFlowTest is DeployUtilsFork { address minter; @@ -101,32 +102,19 @@ contract ForkingHappyFlowTest is DeployUtilsFork { function dealNouns() internal { address nounders = ogToken.noundersDAO(); - vm.startPrank(minter); - for (uint256 i = 0; i < 10; i++) { - ogToken.mint(); - } - - changePrank(nounders); + vm.prank(nounders); ogToken.transferFrom(nounders, nounerInEscrow1, 0); - - changePrank(minter); - ogToken.transferFrom(minter, nounerInEscrow1, 1); - ogToken.transferFrom(minter, nounerInEscrow2, 2); - ogToken.transferFrom(minter, nounerInEscrow2, 3); - ogToken.transferFrom(minter, nounerForkJoiner1, 4); - ogToken.transferFrom(minter, nounerForkJoiner1, 5); - ogToken.transferFrom(minter, nounerForkJoiner2, 6); - ogToken.transferFrom(minter, nounerForkJoiner2, 7); - ogToken.transferFrom(minter, nounerNoFork1, 8); - ogToken.transferFrom(minter, nounerNoFork1, 9); - - changePrank(nounders); + assertEq(mintTo(nounerInEscrow1), 1); + assertEq(mintTo(nounerInEscrow2), 2); + assertEq(mintTo(nounerInEscrow2), 3); + assertEq(mintTo(nounerForkJoiner1), 4); + assertEq(mintTo(nounerForkJoiner1), 5); + assertEq(mintTo(nounerForkJoiner2), 6); + assertEq(mintTo(nounerForkJoiner2), 7); + assertEq(mintTo(nounerNoFork1), 8); + assertEq(mintTo(nounerNoFork1), 9); + vm.prank(nounders); ogToken.transferFrom(nounders, nounerNoFork2, 10); - - changePrank(minter); - ogToken.transferFrom(minter, nounerNoFork2, 11); - - vm.stopPrank(); } function escrowToFork(address nouner) internal { @@ -167,6 +155,23 @@ contract ForkingHappyFlowTest is DeployUtilsFork { calldatas[0] = data; proposalId = forkDAO.propose(targets, values, signatures, calldatas, 'my proposal'); } + + function mintTo(address to) internal returns (uint256 tokenID) { + INounsAuctionHouseV2 ah = INounsAuctionHouseV2(minter); + + if (block.timestamp >= ah.auction().endTime) { + ah.settleCurrentAndCreateNewAuction(); + } + + tokenID = ah.auction().nounId; + + ah.createBid{ value: 1 ether }(tokenID); + vm.warp(block.timestamp + ah.auction().endTime + 1); + ah.settleCurrentAndCreateNewAuction(); + + ogToken.transferFrom(address(this), to, tokenID); + vm.roll(block.number + 1); + } } abstract contract ForkDAOBase is DeployUtilsFork { diff --git a/packages/nouns-contracts/test/foundry/governance/fork/NounsDAOLogicV1Fork.t.sol b/packages/nouns-contracts/test/foundry/governance/fork/NounsDAOLogicV1Fork.t.sol index d0de7a734e..d303fc3347 100644 --- a/packages/nouns-contracts/test/foundry/governance/fork/NounsDAOLogicV1Fork.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/fork/NounsDAOLogicV1Fork.t.sol @@ -19,6 +19,31 @@ import { MaliciousForkDAOQuitter } from '../../helpers/MaliciousForkDAOQuitter.s import { NounsAuctionHouse } from '../../../../contracts/NounsAuctionHouse.sol'; import { INounsAuctionHouse } from '../../../../contracts/interfaces/INounsAuctionHouse.sol'; +interface INounsAuctionHouseForTest { + struct Auction { + // ID for the Noun (ERC721 token ID) + uint256 nounId; + // The current highest bid amount + uint256 amount; + // The time that the auction started + uint256 startTime; + // The time that the auction is scheduled to end + uint256 endTime; + // The address of the current highest bid + address payable bidder; + // Whether or not the auction has been settled + bool settled; + } + + function settleCurrentAndCreateNewAuction() external; + + function createBid(uint256 nounId) external payable; + + function auction() external returns (Auction memory); + + function unpause() external; +} + abstract contract NounsDAOLogicV1ForkBase is DeployUtilsFork { NounsDAOLogicV1Fork dao; address timelock; @@ -35,11 +60,27 @@ abstract contract NounsDAOLogicV1ForkBase is DeployUtilsFork { // in the old way of calling getPriorVotes in vote casting. vm.roll(block.number + 1); - vm.startPrank(token.minter()); - token.mint(); - token.transferFrom(token.minter(), proposer, 0); + vm.startPrank(treasuryAddress); + INounsAuctionHouseForTest(token.minter()).unpause(); vm.stopPrank(); + assertEq(mintTo(proposer), 1); + } + + function mintTo(address to) internal returns (uint256 tokenID) { + INounsAuctionHouseForTest ah = INounsAuctionHouseForTest(token.minter()); + + if (block.timestamp >= ah.auction().endTime) { + ah.settleCurrentAndCreateNewAuction(); + } + + tokenID = ah.auction().nounId; + + ah.createBid{ value: 1 ether }(tokenID); + vm.warp(block.timestamp + ah.auction().endTime + 1); + ah.settleCurrentAndCreateNewAuction(); + + token.transferFrom(address(this), to, tokenID); vm.roll(block.number + 1); } @@ -150,18 +191,17 @@ contract NounsDAOLogicV1Fork_cancelProposalUnderThresholdBugFix_Test is NounsDAO function setUp() public override { super.setUp(); - vm.warp(token.forkingPeriodEndTimestamp()); + assertEq(mintTo(proposer), 2); + while (token.totalSupply() < 10) { + mintTo(address(this)); + } + assertEq(token.balanceOf(proposer), 2); + assertEq(token.totalSupply(), 10); vm.prank(timelock); dao._setProposalThresholdBPS(1_000); - vm.startPrank(token.minter()); - for (uint256 i = 0; i < 9; ++i) { - token.mint(); - } - token.transferFrom(token.minter(), proposer, 1); - vm.stopPrank(); - vm.roll(block.number + 1); + vm.warp(token.forkingPeriodEndTimestamp()); proposalId = propose(); } @@ -180,8 +220,8 @@ contract NounsDAOLogicV1Fork_cancelProposalUnderThresholdBugFix_Test is NounsDAO function test_cancel_nonProposerCanCancelWhenProposerBalanceIsLessThanThreshold() public { vm.startPrank(proposer); - token.transferFrom(proposer, address(1), 0); token.transferFrom(proposer, address(1), 1); + token.transferFrom(proposer, address(1), 2); vm.roll(block.number + 1); assertEq(token.getPriorVotes(proposer, block.number - 1), dao.proposalThreshold() - 1); @@ -436,11 +476,11 @@ contract NounsDAOLogicV1Fork_Quit_Test is NounsDAOLogicV1ForkBase { vm.prank(address(dao.timelock())); dao._setErc20TokensToIncludeInQuit(tokens); - // Send ETH to the DAO - vm.deal(address(dao.timelock()), ETH_BALANCE); - mintNounsToQuitter(); + // Set DAO's ETH balance + vm.deal(address(dao.timelock()), ETH_BALANCE); + ethPerNoun = ETH_BALANCE / token.totalSupply(); vm.prank(quitter); @@ -579,22 +619,15 @@ contract NounsDAOLogicV1Fork_Quit_Test is NounsDAOLogicV1ForkBase { } function mintNounsToQuitter() internal { - address minter = token.minter(); - vm.startPrank(minter); - while (token.totalSupply() < 10) { - uint256 tokenId = token.mint(); - address to = proposer; - if (tokenId > 7) { - to = quitter; - quitterTokens.push(tokenId); - } - token.transferFrom(token.minter(), to, tokenId); + while (token.balanceOf(proposer) < 7) { + mintTo(proposer); + } + while (token.balanceOf(quitter) < 2) { + quitterTokens.push(mintTo(quitter)); } - vm.stopPrank(); - - vm.roll(block.number + 1); assertEq(token.totalSupply(), 10); + assertEq(dao.adjustedTotalSupply(), 10); assertEq(token.balanceOf(quitter), 2); } } diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol index db607be492..1dfd4275b6 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol @@ -134,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)); + NounsAuctionHouse(address(auctionProxy)).unpause(); + vm.prank(address(timelock)); AuctionHouseUpgrader.upgradeAuctionHouse(address(timelock), auctionAdmin, address(auctionProxy)); From 87972ac63001bb449a9590b35bf9bafa09c8bac8 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Thu, 12 Oct 2023 17:34:29 -0500 Subject: [PATCH 9/9] add mainnet upgrade test to DAO V3.1 --- .../UpgradeToDAOV3p1ForkMainnetTest.t.sol | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3p1ForkMainnetTest.t.sol diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3p1ForkMainnetTest.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3p1ForkMainnetTest.t.sol new file mode 100644 index 0000000000..f456b92fa3 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3p1ForkMainnetTest.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { Strings } from '@openzeppelin/contracts/utils/Strings.sol'; +import { ProposeDAOV3p1UpgradeMainnet } from '../../../script/DAOLogicV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol'; +import { DeployDAOV3p1 } from '../../../script/DAOLogicV3p1/DeployDAOV3p1.s.sol'; +import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; +import { NounsToken } from '../../../contracts/NounsToken.sol'; +import { IForkDAODeployer, INounsDAOExecutorV2 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; +import { AuctionHouseUpgrader } from '../helpers/AuctionHouseUpgrader.sol'; +import { INounsAuctionHouseV2 } from '../../../contracts/interfaces/INounsAuctionHouseV2.sol'; +import { NounsAuctionHouseProxyAdmin } from '../../../contracts/proxies/NounsAuctionHouseProxyAdmin.sol'; + +contract UpgradeToDAOV3p1ForkMainnetTest is Test { + error NounIdNotOldEnough(); + + NounsDAOLogicV3 public constant NOUNS_DAO_PROXY_MAINNET = + NounsDAOLogicV3(payable(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d)); + INounsDAOExecutorV2 public constant NOUNS_TIMELOCK_MAINNET = + INounsDAOExecutorV2(0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71); + INounsAuctionHouseV2 public constant AUCTION_HOUSE_PROXY_MAINNET = + INounsAuctionHouseV2(0x830BD73E4184ceF73443C15111a1DF14e495C706); + NounsAuctionHouseProxyAdmin public constant AUCTION_HOUSE_PROXY_ADMIN_MAINNET = + NounsAuctionHouseProxyAdmin(0xC1C119932d78aB9080862C5fcb964029f086401e); + address public constant NOUNDERS = 0x2573C60a6D127755aA2DC85e342F7da2378a0Cc5; + NounsToken public nouns = NounsToken(0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03); + uint256 proposalId; + address proposerAddr = vm.addr(0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb); + address whaleAddr = 0xFC218F1164cfEf8e4EC7c2aca8fB019DC8976214; + + NounsDAOLogicV3 newImpl; + IForkDAODeployer forkDeployerBefore; + address[] erc20sBefore; + + function setUp() public { + vm.createSelectFork(vm.envString('RPC_MAINNET'), 18336146); + + // The new code assumes AuctionHouseV2, so upgrading before + AuctionHouseUpgrader.upgradeAuctionHouse( + address(NOUNS_TIMELOCK_MAINNET), + AUCTION_HOUSE_PROXY_ADMIN_MAINNET, + address(AUCTION_HOUSE_PROXY_MAINNET) + ); + + // give ourselves voting power + vm.prank(NOUNDERS); + nouns.delegate(proposerAddr); + + vm.roll(block.number + 1); + + // save state values before the upgrade + forkDeployerBefore = NOUNS_DAO_PROXY_MAINNET.forkDAODeployer(); + erc20sBefore = NOUNS_DAO_PROXY_MAINNET.erc20TokensToIncludeInFork(); + + // deploy contracts + vm.setEnv('DEPLOYER_PRIVATE_KEY', '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + newImpl = new DeployDAOV3p1().run(); + + // propose upgrade + ProposeDAOV3p1UpgradeMainnet upgradePropScript = new ProposeDAOV3p1UpgradeMainnet(); + vm.setEnv('PROPOSER_KEY', '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'); + vm.setEnv('DAO_V3p1_IMPL', Strings.toHexString(uint160(address(newImpl)), 20)); + vm.setEnv('PROPOSAL_DESCRIPTION_FILE', 'test/foundry/NounsDAOLogicV3/proposal-description.txt'); + + proposalId = upgradePropScript.run(); + + // simulate vote & proposal execution + voteAndExecuteProposal(); + + AUCTION_HOUSE_PROXY_MAINNET.settleCurrentAndCreateNewAuction(); + } + + function test_forkDeployerDidNotChange() public { + assertEq(address(NOUNS_DAO_PROXY_MAINNET.forkDAODeployer()), address(forkDeployerBefore)); + } + + function test_erc20sDidNotChange() public { + address[] memory erc20sAfter = NOUNS_DAO_PROXY_MAINNET.erc20TokensToIncludeInFork(); + assertEq(erc20sBefore.length, erc20sAfter.length); + + for (uint256 i = 0; i < erc20sBefore.length; i++) { + assertEq(erc20sBefore[i], erc20sAfter[i]); + } + } + + function test_nounAgeRequiredToForkIsZero() public { + assertEq(NOUNS_DAO_PROXY_MAINNET.nounAgeRequiredToFork(), 0); + } + + function test_givenRequiredAgeZero_MostRecentNounCanEscrow() public { + address forker = makeAddr('forker'); + uint256 nounId = winCurrentAuction(forker); + + escrowToFork(forker, nounId); + } + + function test_givenRequiredAgeGreaterThanZero_AnOldEnoughNounCanEscrow() public { + vm.prank(address(NOUNS_DAO_PROXY_MAINNET.timelock())); + NOUNS_DAO_PROXY_MAINNET._setNounAgeRequiredToFork(1); + + address forker = makeAddr('forker'); + uint256 nounId = winCurrentAuction(forker); + + // skipping an auction to make the noun in question old enough + winCurrentAuction(forker); + + escrowToFork(forker, nounId); + } + + function test_givenRequiredAgeGreaterThanZero_MostRecentNounEscrowReverts() public { + vm.prank(address(NOUNS_DAO_PROXY_MAINNET.timelock())); + NOUNS_DAO_PROXY_MAINNET._setNounAgeRequiredToFork(1); + + address forker = makeAddr('forker'); + uint256 nounId = winCurrentAuction(forker); + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = nounId; + uint256[] memory proposalIds = new uint256[](0); + string memory reason = ''; + + vm.startPrank(forker); + nouns.setApprovalForAll(address(NOUNS_DAO_PROXY_MAINNET), true); + + vm.expectRevert(NounIdNotOldEnough.selector); + NOUNS_DAO_PROXY_MAINNET.escrowToFork(tokenIds, proposalIds, reason); + vm.stopPrank(); + } + + function voteAndExecuteProposal() internal { + vm.roll( + block.number + + NOUNS_DAO_PROXY_MAINNET.proposalUpdatablePeriodInBlocks() + + NOUNS_DAO_PROXY_MAINNET.votingDelay() + + 1 + ); + vm.prank(proposerAddr); + NOUNS_DAO_PROXY_MAINNET.castVote(proposalId, 1); + vm.prank(whaleAddr); + 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_MAINNET.delay()); + NOUNS_DAO_PROXY_MAINNET.execute(proposalId); + } + + function winCurrentAuction(address bidder) internal returns (uint256) { + uint256 bidValue = 0.01 ether; + vm.deal(bidder, bidValue); + uint256 currentAuctionID = AUCTION_HOUSE_PROXY_MAINNET.auction().nounId; + + vm.prank(bidder); + AUCTION_HOUSE_PROXY_MAINNET.createBid{ value: bidValue }(currentAuctionID); + vm.warp(block.timestamp + AUCTION_HOUSE_PROXY_MAINNET.auction().endTime + 1); + AUCTION_HOUSE_PROXY_MAINNET.settleCurrentAndCreateNewAuction(); + + return currentAuctionID; + } + + function escrowToFork(address forker, uint256 nounId) internal { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = nounId; + uint256[] memory proposalIds = new uint256[](0); + string memory reason = ''; + + vm.startPrank(forker); + nouns.setApprovalForAll(address(NOUNS_DAO_PROXY_MAINNET), true); + NOUNS_DAO_PROXY_MAINNET.escrowToFork(tokenIds, proposalIds, reason); + vm.stopPrank(); + } +}