From add8aca15c4643ddc5c628ade617c4863bdfe20b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 7 Nov 2023 14:44:38 -0500 Subject: [PATCH 01/28] fix vote gas refund we accidentally used the old V2 code that refunds msg.sender it should be tx.origin to refund gas payers of multisigs --- .../contracts/governance/NounsDAOV3Votes.sol | 4 +- .../governance/NounsDAO/V3/voteRefund.test.ts | 384 ++++++++++++++++++ packages/nouns-contracts/test/utils.ts | 80 ++++ 3 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts diff --git a/packages/nouns-contracts/contracts/governance/NounsDAOV3Votes.sol b/packages/nouns-contracts/contracts/governance/NounsDAOV3Votes.sol index 3743132bf8..6ac54dfcc4 100644 --- a/packages/nouns-contracts/contracts/governance/NounsDAOV3Votes.sol +++ b/packages/nouns-contracts/contracts/governance/NounsDAOV3Votes.sol @@ -302,8 +302,8 @@ library NounsDAOV3Votes { uint256 gasPrice = min(tx.gasprice, basefee + MAX_REFUND_PRIORITY_FEE); uint256 gasUsed = min(startGas - gasleft() + REFUND_BASE_GAS, MAX_REFUND_GAS_USED); uint256 refundAmount = min(gasPrice * gasUsed, balance); - (bool refundSent, ) = msg.sender.call{ value: refundAmount }(''); - emit RefundableVote(msg.sender, refundAmount, refundSent); + (bool refundSent, ) = tx.origin.call{ value: refundAmount }(''); + emit RefundableVote(tx.origin, refundAmount, refundSent); } } diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts new file mode 100644 index 0000000000..03908d0b24 --- /dev/null +++ b/packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts @@ -0,0 +1,384 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import chai from 'chai'; +import { solidity } from 'ethereum-waffle'; +import { BigNumber, ContractReceipt } from 'ethers'; +import { ethers } from 'hardhat'; +import { + NounsDAOLogicV3__factory, + NounsDAOLogicV3, + NounsDescriptorV2__factory, + NounsToken, + Voter__factory, +} from '../../../../typechain'; +import { MaliciousVoter__factory } from '../../../../typechain/factories/contracts/test/MaliciousVoter__factory'; +import { + address, + advanceBlocks, + deployGovernorV3WithV3Proxy, + deployNounsToken, + encodeParameters, + getSigners, + populateDescriptorV2, + setNextBlockBaseFee, + TestSigners, +} from '../../../utils'; + +chai.use(solidity); +const { expect } = chai; + +const realLongReason = + "Judge: The defense may proceed. Roark: Your Honor, I shall call no witnesses. This will be my testimony and my summation. Judge: Take the oath. Court Clerk: Do you swear to tell the truth, the whole truth, and nothing but the truth, so help you God? Roark: I do. Thousands of years ago, the first man discovered how to make fire. He was probably burned at the stake he had taught his brothers to light, but he left them a gift they had not conceived, and he lifted darkness off the earth. Throughout the centuries, there were men who took first steps down new roads, armed with nothing but their own vision. The great creators -- the thinkers, the artists, the scientists, the inventors -- stood alone against the men of their time. Every new thought was opposed; every new invention was denounced. But the men of unborrowed vision went ahead. They fought, they suffered, and they paid. But they won. No creator was prompted by a desire to please his brothers. His brothers hated the gift he offered. His truth was his only motive. His work was his only goal. His work -- not those who used it. His creation -- not the benefits others derived from it -- the creation which gave form to his truth. He held his truth above all things and against all men. He went ahead whether others agreed with him or not, with his integrity as his only banner. He served nothing and no one. He lived for himself. And only by living for himself was he able to achieve the things which are the glory of mankind. Such is the nature of achievement. Man cannot survive except through his mind. He comes on earth unarmed. His brain is his only weapon. But the mind is an attribute of the individual. There is no such thing as a collective brain. The man who thinks must think and act on his own. The reasoning mind cannot work under any form of compulsion. It cannot be subordinated to the needs, opinions, or wishes of others. It is not an object of sacrifice. The creator stands on his own judgment; the parasite follows the opinions of others. The creator thinks; the parasite copies. The creator produces; the parasite loots. The creator's concern is the conquest of nature; the parasite's concern is the conquest of men. The creator requires independence. He neither serves nor rules. He deals with men by free exchange and voluntary choice. The parasite seeks power. He wants to bind all men together in common action and common slavery. He claims that man is only a tool for the use of others -- that he must think as they think, act as they act, and live in selfless, joyless servitude to any need but his own. Look at history: Everything we have, every great achievement has come from the independent work of some independent mind. Every horror and destruction came from attempts to force men into a herd of brainless, soulless robots -- without personal rights, without person ambition, without will, hope, or dignity. It is an ancient conflict. It has another name: \"The individual against the collective.\" Our country, the noblest country in the history of men, was based on the principle of individualism, the principle of man's \"inalienable rights.\" It was a country where a man was free to seek his own happiness, to gain and produce, not to give up and renounce; to prosper, not to starve; to achieve, not to plunder; to hold as his highest possession a sense of his personal value, and as his highest virtue his self-respect. Look at the results. That is what the collectivists are now asking you to destroy, as much of the earth has been destroyed. I am an architect. I know what is to come by the principle on which it is built. We are approaching a world in which I cannot permit myself to live. My ideas are my property. They were taken from me by force, by breach of contract. No appeal was left to me. It was believed that my work belonged to others, to do with as they pleased. They had a claim upon me without my consent -- that it was my duty to serve them without choice or reward. Now you know why a dynamited Courtland. I designed Courtland. I made it possible. I destroyed it. I agreed to design it for the purpose of it seeing built as I wished. That was the price I set for my work. I was not paid. My building was disfigured at the whim of others who took all the benefits of my work and gave me nothing in return. I came here to say that I do not recognize anyone's right to one minute of my life, nor to any part of my energy, nor to any achievement of mine -- no matter who makes the claim! It had to be said: The world is perishing from an orgy of self-sacrificing. I came here to be heard in the name of every man of independence still left in the world. I wanted to state my terms. I do not care to work or live on any others. My terms are: A man's RIGHT to exist for his own sake."; +const LONG_REASON = realLongReason + realLongReason; +const REFUND_ERROR_MARGIN = ethers.utils.parseEther('0.001'); +const MAX_PRIORITY_FEE_CAP = ethers.utils.parseUnits('2', 'gwei'); +const DEFAULT_GAS_OPTIONS = { maxPriorityFeePerGas: MAX_PRIORITY_FEE_CAP }; +const MAX_REFUND_GAS_USED = BigNumber.from(200_000); +const MAX_REFUND_BASE_FEE = ethers.utils.parseUnits('200', 'gwei'); + +let deployer: SignerWithAddress; +let user: SignerWithAddress; +let user2: SignerWithAddress; +let signers: TestSigners; +let gov: NounsDAOLogicV3; +let token: NounsToken; +let snapshotId: number; + +describe('V3 Vote Refund', () => { + before(async () => { + signers = await getSigners(); + deployer = signers.deployer; + user = signers.account0; + user2 = signers.account1; + + token = await deployNounsToken(deployer); + const descriptor = NounsDescriptorV2__factory.connect(await token.descriptor(), deployer); + await populateDescriptorV2(descriptor); + + await token.connect(deployer).mint(); + await token.connect(deployer).transferFrom(deployer.address, user.address, 0); + await token.connect(deployer).transferFrom(deployer.address, user.address, 1); + + await advanceBlocks(1); + + gov = await deployGovernorV3WithV3Proxy(deployer, token.address); + await submitProposal(user); + }); + + beforeEach(async () => { + snapshotId = await ethers.provider.send('evm_snapshot', []); + }); + + afterEach(async () => { + await ethers.provider.send('evm_revert', [snapshotId]); + }); + + describe('castRefundableVote', () => { + it('refunds users with votes', async () => { + await fundGov(); + const balanceBefore = await user.getBalance(); + const tx = await gov.connect(user).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + + expect(r.gasUsed).to.be.gt(0); + expect(balanceDiff).to.be.closeTo(BigNumber.from(0), REFUND_ERROR_MARGIN); + expectRefundEvent(r, user, await txCostInEth(r)); + await expect(tx).to.emit(gov, 'VoteCast').withArgs(user.address, BigNumber.from(1), 1, 2, ''); + }); + + it('does not refund users with no votes', async () => { + await fundGov(); + const balanceBefore = await user2.getBalance(); + + const tx = await gov.connect(user2).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const balanceDiff = balanceBefore.sub(await user2.getBalance()); + expect(balanceDiff).to.be.eq(await txCostInEth(r)); + await expect(tx).to.changeEtherBalance(gov, 0); + }); + + it('caps refund', async () => { + await fundGov(); + const balanceBefore = await user.getBalance(); + + const tx = await gov.connect(user).castRefundableVote(1, 1, { + maxPriorityFeePerGas: ethers.utils.parseUnits('80', 'gwei'), + }); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.be.closeTo( + await expectedPriorityFeeCappedDiff(r), + REFUND_ERROR_MARGIN, + ); + }); + + it('does not refund when DAO balance is zero', async () => { + expect(await ethers.provider.getBalance(gov.address)).to.eq(0); + const balanceBefore = await user.getBalance(); + const tx = await gov.connect(user).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.be.eq(await txCostInEth(r)); + }); + + it('provides partial refund given insufficient balance', async () => { + await fundGov('0.00001'); + const govBalance = ethers.utils.parseEther('0.00001'); + expect(await ethers.provider.getBalance(gov.address)).to.eq(govBalance); + const balanceBefore = await user.getBalance(); + + const tx = await gov.connect(user).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const expectedDiff = (await txCostInEth(r)).sub(govBalance); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.eq(expectedDiff); + }); + + it('malicious voter trying reentrance does not get refunded', async () => { + const voter = await new MaliciousVoter__factory(deployer).deploy(gov.address, 2, 1, false); + await token.connect(user).transferFrom(user.address, voter.address, 0); + await token.connect(user).transferFrom(user.address, user2.address, 1); + await advanceBlocks(1); + await submitProposal(user2); + const balanceBefore = await user.getBalance(); + + const tx = await voter.connect(user).castVote(DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.be.eq(await txCostInEth(r)); + await expect(tx).to.changeEtherBalance(gov, 0); + }); + }); + + describe('castRefundableVoteWithReason', () => { + it('refunds users with votes', async () => { + await fundGov(); + const balanceBefore = await user.getBalance(); + const tx = await gov + .connect(user) + .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + + expect(r.gasUsed).to.be.gt(0); + expect(balanceDiff).to.be.closeTo(BigNumber.from(0), REFUND_ERROR_MARGIN); + + expectRefundEvent(r, user, await txCostInEth(r)); + await expect(tx) + .to.emit(gov, 'VoteCast') + .withArgs(user.address, BigNumber.from(1), 1, 2, 'some reason'); + }); + + it('does not refund users with no votes', async () => { + await fundGov(); + const balanceBefore = await user2.getBalance(); + + const tx = await gov + .connect(user2) + .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const balanceDiff = balanceBefore.sub(await user2.getBalance()); + expect(balanceDiff).to.be.eq(await txCostInEth(r)); + await expect(tx).to.changeEtherBalance(gov, 0); + }); + + it('caps refund priority fee', async () => { + await fundGov(); + const balanceBefore = await user.getBalance(); + + const tx = await gov.connect(user).castRefundableVoteWithReason(1, 1, 'some reason', { + maxPriorityFeePerGas: ethers.utils.parseUnits('80', 'gwei'), + }); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.be.closeTo( + await expectedPriorityFeeCappedDiff(r), + REFUND_ERROR_MARGIN, + ); + }); + + it('caps gasUsed', async () => { + await fundGov(); + const balanceBefore = await user.getBalance(); + const tx = await gov + .connect(user) + .castRefundableVoteWithReason(1, 1, LONG_REASON, DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + + expect(r.gasUsed).to.be.gt(0); + expect(balanceDiff).to.be.closeTo(await expectedGasUsedCappedDiff(r), REFUND_ERROR_MARGIN); + + expectRefundEvent(r, user, MAX_REFUND_GAS_USED.mul(await latestBasePlusMaxPriority())); + await expect(tx) + .to.emit(gov, 'VoteCast') + .withArgs(user.address, BigNumber.from(1), 1, 2, LONG_REASON); + }); + + it('caps basefee [ @skip-on-coverage ]', async () => { + await fundGov(); + await setNextBlockBaseFee(MAX_REFUND_BASE_FEE.mul(2)); + const balanceBefore = await user.getBalance(); + const tx = await gov + .connect(user) + .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + + expect(r.gasUsed).to.be.gt(0); + expect(balanceDiff).to.be.closeTo(await expectedBaseFeeCappedDiff(r), REFUND_ERROR_MARGIN); + + expectRefundEvent(r, user, r.gasUsed.mul(MAX_REFUND_BASE_FEE.add(MAX_PRIORITY_FEE_CAP))); + await expect(tx) + .to.emit(gov, 'VoteCast') + .withArgs(user.address, BigNumber.from(1), 1, 2, 'some reason'); + }); + + it('does not refund when DAO balance is zero', async () => { + expect(await ethers.provider.getBalance(gov.address)).to.eq(0); + const balanceBefore = await user.getBalance(); + const tx = await gov + .connect(user) + .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.be.eq(await txCostInEth(r)); + }); + + it('provides partial refund given insufficient balance', async () => { + await fundGov('0.00001'); + const govBalance = ethers.utils.parseEther('0.00001'); + expect(await ethers.provider.getBalance(gov.address)).to.eq(govBalance); + const balanceBefore = await user.getBalance(); + + const tx = await gov + .connect(user) + .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + expect(r.gasUsed).to.be.gt(0); + const expectedDiff = (await txCostInEth(r)).sub(govBalance); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.eq(expectedDiff); + }); + + it('malicious voter trying reentrance does not get refunded', async () => { + const voter = await new MaliciousVoter__factory(deployer).deploy(gov.address, 2, 1, true); + await token.connect(user).transferFrom(user.address, voter.address, 0); + await token.connect(user).transferFrom(user.address, user2.address, 1); + await advanceBlocks(1); + await submitProposal(user2); + const balanceBefore = await user.getBalance(); + + const tx = await voter.connect(user).castVote(DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + + const balanceDiff = balanceBefore.sub(await user.getBalance()); + expect(balanceDiff).to.be.eq(await txCostInEth(r)); + await expect(tx).to.changeEtherBalance(gov, 0); + }); + + it('refunds EOA user when voting via a smart contract (tx.origin)', async () => { + await fundGov(); + + const voter = await new Voter__factory(deployer).deploy(gov.address, 2, 1, true); + await token.connect(user).transferFrom(user.address, voter.address, 0); + await token.connect(user).transferFrom(user.address, user2.address, 1); + await advanceBlocks(1); + await submitProposal(user2); + + const balanceBefore = await user.getBalance(); + const tx = await voter.connect(user).castVote(DEFAULT_GAS_OPTIONS); + const r = await tx.wait(); + const balanceDiff = balanceBefore.sub(await user.getBalance()); + + expect(r.gasUsed).to.be.gt(0); + expect(balanceDiff).to.be.closeTo(BigNumber.from(0), REFUND_ERROR_MARGIN); + + expectRefundEvent(r, user, await txCostInEth(r)); + await expect(tx) + .to.emit(gov, 'VoteCast') + .withArgs(voter.address, BigNumber.from(2), 1, 1, 'some reason'); + }); + }); + + async function expectedPriorityFeeCappedDiff(r: ContractReceipt): Promise { + const expectedRefund = await txCostInEth(r); + const txGrossCost = r.gasUsed.mul(r.effectiveGasPrice); + return txGrossCost.sub(expectedRefund); + } + + async function expectedGasUsedCappedDiff(r: ContractReceipt): Promise { + const gasPrice = await latestBasePlusMaxPriority(); + const expectedRefund = MAX_REFUND_GAS_USED.mul(gasPrice); + const txGrossCost = r.gasUsed.mul(gasPrice); + return txGrossCost.sub(expectedRefund); + } + + async function expectedBaseFeeCappedDiff(r: ContractReceipt): Promise { + const expectedRefund = r.gasUsed.mul(MAX_REFUND_BASE_FEE.add(MAX_PRIORITY_FEE_CAP)); + const txGrossCost = r.gasUsed.mul(await latestBasePlusMaxPriority()); + return txGrossCost.sub(expectedRefund); + } + + async function txCostInEth(r: ContractReceipt): Promise { + return r.gasUsed.mul(await latestBasePlusMaxPriority()); + } + + async function latestBasePlusMaxPriority(): Promise { + const block = await ethers.provider.getBlock('latest'); + return block.baseFeePerGas!.add(MAX_PRIORITY_FEE_CAP); + } + + async function fundGov(ethAmount: string = '100') { + await deployer.sendTransaction({ to: gov.address, value: ethers.utils.parseEther(ethAmount) }); + } + + function expectRefundEvent(r: ContractReceipt, u: SignerWithAddress, expectedCost: BigNumber) { + // Not using expect emit because it doesn't support the `closeTo` matcher + // Using longer event parsing because r.events doesn't work when using the Voter contract + // to simulate multisig usage; events are returned undefined + const daoInterface = NounsDAOLogicV3__factory.createInterface(); + const eventId = ethers.utils.id('RefundableVote(address,uint256,bool)'); + const filtered = r.logs.filter(l => l.topics[0] === eventId); + const parsed = filtered.map(e => { + return daoInterface.parseLog(e); + }); + + expect(parsed.length).to.equal(1); + const refundEvent = parsed[0]; + expect(refundEvent).to.not.be.undefined; + expect(refundEvent!.args!.voter).to.equal(u.address); + expect(refundEvent!.args!.refundSent).to.be.true; + expect(refundEvent!.args!.refundAmount).to.be.closeTo(expectedCost, REFUND_ERROR_MARGIN); + } + + async function submitProposal(u: SignerWithAddress) { + await gov + .connect(u) + .propose( + [address(0)], + ['0'], + ['getBalanceOf(address)'], + [encodeParameters(['address'], [address(0)])], + '', + ); + + await advanceBlocks(2); + } +}); diff --git a/packages/nouns-contracts/test/utils.ts b/packages/nouns-contracts/test/utils.ts index ebc9e368d0..6383871c85 100644 --- a/packages/nouns-contracts/test/utils.ts +++ b/packages/nouns-contracts/test/utils.ts @@ -25,6 +25,10 @@ import { NounsDAOExecutor, Inflator__factory, NounsDAOStorageV2, + NounsDAOLogicV3, + NounsDAOLogicV3__factory as NounsDaoLogicV3Factory, + NounsDAOProxyV3__factory as NounsDaoProxyV3Factory, + NounsDAOForkEscrow__factory as NounsDAOForkEscrowFactory, } from '../typechain'; import ImageData from '../files/image-data-v1.json'; import ImageDataV2 from '../files/image-data-v2.json'; @@ -526,3 +530,79 @@ function dataToDescriptorInput(data: string[]): { itemCount, }; } + +export const deployGovernorV3WithV3Proxy = async ( + deployer: SignerWithAddress, + tokenAddress: string, + timelockAddress?: string, + forkEscrowAddress?: string, + forkDAODeployerAddress?: string, + vetoerAddress?: string, + votingPeriod?: number, + votingDelay?: number, + proposalThresholdBPs?: number, + dynamicQuorumParams?: DynamicQuorumParams, +): Promise => { + const NounsDAOV3Proposals = await ( + await ethers.getContractFactory('NounsDAOV3Proposals', deployer) + ).deploy(); + const NounsDAOV3Admin = await ( + await ethers.getContractFactory('NounsDAOV3Admin', deployer) + ).deploy(); + const NounsDAOV3Fork = await ( + await ethers.getContractFactory('NounsDAOV3Fork', deployer) + ).deploy(); + const NounsDAOV3Votes = await ( + await ethers.getContractFactory('NounsDAOV3Votes', deployer) + ).deploy(); + const NounsDAOV3DynamicQuorum = await ( + await ethers.getContractFactory('NounsDAOV3DynamicQuorum', deployer) + ).deploy(); + + const v3LogicContract = await new NounsDaoLogicV3Factory( + { + 'contracts/governance/NounsDAOV3Proposals.sol:NounsDAOV3Proposals': + NounsDAOV3Proposals.address, + 'contracts/governance/NounsDAOV3Admin.sol:NounsDAOV3Admin': NounsDAOV3Admin.address, + 'contracts/governance/fork/NounsDAOV3Fork.sol:NounsDAOV3Fork': NounsDAOV3Fork.address, + 'contracts/governance/NounsDAOV3Votes.sol:NounsDAOV3Votes': NounsDAOV3Votes.address, + 'contracts/governance/NounsDAOV3DynamicQuorum.sol:NounsDAOV3DynamicQuorum': + NounsDAOV3DynamicQuorum.address, + }, + deployer, + ).deploy(); + + const predictedProxyAddress = ethers.utils.getContractAddress({ + from: deployer.address, + nonce: (await deployer.getTransactionCount()) + 1, + }); + + const escrowAddress = ( + await new NounsDAOForkEscrowFactory(deployer).deploy(predictedProxyAddress, tokenAddress) + ).address; + + const proxy = await new NounsDaoProxyV3Factory(deployer).deploy( + timelockAddress || deployer.address, + tokenAddress, + escrowAddress, + forkDAODeployerAddress || deployer.address, + vetoerAddress || deployer.address, + deployer.address, + v3LogicContract.address, + { + votingPeriod: votingPeriod || 7200, + votingDelay: votingDelay || 1, + proposalThresholdBPS: proposalThresholdBPs || 1, + lastMinuteWindowInBlocks: 0, + objectionPeriodDurationInBlocks: 0, + proposalUpdatablePeriodInBlocks: 0, + }, + dynamicQuorumParams || { + minQuorumVotesBPS: MIN_QUORUM_VOTES_BPS, + maxQuorumVotesBPS: MAX_QUORUM_VOTES_BPS, + quorumCoefficient: 0, + }, + ); + + return NounsDaoLogicV3Factory.connect(proxy.address, deployer); +}; From 5ddcd445029d50ea88139f5164436cd36ba2ed26 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 15:07:11 -0500 Subject: [PATCH 02/28] fix CR comment: edit test instead of duplicating we want to stop maintaining tests on prev versions instead make sure all tests run against the latest version --- .../governance/NounsDAO/V2/voteRefund.test.ts | 16 +- .../governance/NounsDAO/V3/voteRefund.test.ts | 384 ------------------ 2 files changed, 8 insertions(+), 392 deletions(-) delete mode 100644 packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/voteRefund.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/voteRefund.test.ts index 1b83776122..03908d0b24 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/voteRefund.test.ts +++ b/packages/nouns-contracts/test/governance/NounsDAO/V2/voteRefund.test.ts @@ -4,8 +4,8 @@ import { solidity } from 'ethereum-waffle'; import { BigNumber, ContractReceipt } from 'ethers'; import { ethers } from 'hardhat'; import { - NounsDAOLogicV2, - NounsDAOLogicV2__factory, + NounsDAOLogicV3__factory, + NounsDAOLogicV3, NounsDescriptorV2__factory, NounsToken, Voter__factory, @@ -14,7 +14,7 @@ import { MaliciousVoter__factory } from '../../../../typechain/factories/contrac import { address, advanceBlocks, - deployGovernorV2WithV2Proxy, + deployGovernorV3WithV3Proxy, deployNounsToken, encodeParameters, getSigners, @@ -29,7 +29,7 @@ const { expect } = chai; const realLongReason = "Judge: The defense may proceed. Roark: Your Honor, I shall call no witnesses. This will be my testimony and my summation. Judge: Take the oath. Court Clerk: Do you swear to tell the truth, the whole truth, and nothing but the truth, so help you God? Roark: I do. Thousands of years ago, the first man discovered how to make fire. He was probably burned at the stake he had taught his brothers to light, but he left them a gift they had not conceived, and he lifted darkness off the earth. Throughout the centuries, there were men who took first steps down new roads, armed with nothing but their own vision. The great creators -- the thinkers, the artists, the scientists, the inventors -- stood alone against the men of their time. Every new thought was opposed; every new invention was denounced. But the men of unborrowed vision went ahead. They fought, they suffered, and they paid. But they won. No creator was prompted by a desire to please his brothers. His brothers hated the gift he offered. His truth was his only motive. His work was his only goal. His work -- not those who used it. His creation -- not the benefits others derived from it -- the creation which gave form to his truth. He held his truth above all things and against all men. He went ahead whether others agreed with him or not, with his integrity as his only banner. He served nothing and no one. He lived for himself. And only by living for himself was he able to achieve the things which are the glory of mankind. Such is the nature of achievement. Man cannot survive except through his mind. He comes on earth unarmed. His brain is his only weapon. But the mind is an attribute of the individual. There is no such thing as a collective brain. The man who thinks must think and act on his own. The reasoning mind cannot work under any form of compulsion. It cannot be subordinated to the needs, opinions, or wishes of others. It is not an object of sacrifice. The creator stands on his own judgment; the parasite follows the opinions of others. The creator thinks; the parasite copies. The creator produces; the parasite loots. The creator's concern is the conquest of nature; the parasite's concern is the conquest of men. The creator requires independence. He neither serves nor rules. He deals with men by free exchange and voluntary choice. The parasite seeks power. He wants to bind all men together in common action and common slavery. He claims that man is only a tool for the use of others -- that he must think as they think, act as they act, and live in selfless, joyless servitude to any need but his own. Look at history: Everything we have, every great achievement has come from the independent work of some independent mind. Every horror and destruction came from attempts to force men into a herd of brainless, soulless robots -- without personal rights, without person ambition, without will, hope, or dignity. It is an ancient conflict. It has another name: \"The individual against the collective.\" Our country, the noblest country in the history of men, was based on the principle of individualism, the principle of man's \"inalienable rights.\" It was a country where a man was free to seek his own happiness, to gain and produce, not to give up and renounce; to prosper, not to starve; to achieve, not to plunder; to hold as his highest possession a sense of his personal value, and as his highest virtue his self-respect. Look at the results. That is what the collectivists are now asking you to destroy, as much of the earth has been destroyed. I am an architect. I know what is to come by the principle on which it is built. We are approaching a world in which I cannot permit myself to live. My ideas are my property. They were taken from me by force, by breach of contract. No appeal was left to me. It was believed that my work belonged to others, to do with as they pleased. They had a claim upon me without my consent -- that it was my duty to serve them without choice or reward. Now you know why a dynamited Courtland. I designed Courtland. I made it possible. I destroyed it. I agreed to design it for the purpose of it seeing built as I wished. That was the price I set for my work. I was not paid. My building was disfigured at the whim of others who took all the benefits of my work and gave me nothing in return. I came here to say that I do not recognize anyone's right to one minute of my life, nor to any part of my energy, nor to any achievement of mine -- no matter who makes the claim! It had to be said: The world is perishing from an orgy of self-sacrificing. I came here to be heard in the name of every man of independence still left in the world. I wanted to state my terms. I do not care to work or live on any others. My terms are: A man's RIGHT to exist for his own sake."; const LONG_REASON = realLongReason + realLongReason; -const REFUND_ERROR_MARGIN = ethers.utils.parseEther('0.00015'); +const REFUND_ERROR_MARGIN = ethers.utils.parseEther('0.001'); const MAX_PRIORITY_FEE_CAP = ethers.utils.parseUnits('2', 'gwei'); const DEFAULT_GAS_OPTIONS = { maxPriorityFeePerGas: MAX_PRIORITY_FEE_CAP }; const MAX_REFUND_GAS_USED = BigNumber.from(200_000); @@ -39,11 +39,11 @@ let deployer: SignerWithAddress; let user: SignerWithAddress; let user2: SignerWithAddress; let signers: TestSigners; -let gov: NounsDAOLogicV2; +let gov: NounsDAOLogicV3; let token: NounsToken; let snapshotId: number; -describe('Vote Refund', () => { +describe('V3 Vote Refund', () => { before(async () => { signers = await getSigners(); deployer = signers.deployer; @@ -60,7 +60,7 @@ describe('Vote Refund', () => { await advanceBlocks(1); - gov = await deployGovernorV2WithV2Proxy(deployer, token.address); + gov = await deployGovernorV3WithV3Proxy(deployer, token.address); await submitProposal(user); }); @@ -353,7 +353,7 @@ describe('Vote Refund', () => { // Not using expect emit because it doesn't support the `closeTo` matcher // Using longer event parsing because r.events doesn't work when using the Voter contract // to simulate multisig usage; events are returned undefined - const daoInterface = NounsDAOLogicV2__factory.createInterface(); + const daoInterface = NounsDAOLogicV3__factory.createInterface(); const eventId = ethers.utils.id('RefundableVote(address,uint256,bool)'); const filtered = r.logs.filter(l => l.topics[0] === eventId); const parsed = filtered.map(e => { diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts deleted file mode 100644 index 03908d0b24..0000000000 --- a/packages/nouns-contracts/test/governance/NounsDAO/V3/voteRefund.test.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; -import { BigNumber, ContractReceipt } from 'ethers'; -import { ethers } from 'hardhat'; -import { - NounsDAOLogicV3__factory, - NounsDAOLogicV3, - NounsDescriptorV2__factory, - NounsToken, - Voter__factory, -} from '../../../../typechain'; -import { MaliciousVoter__factory } from '../../../../typechain/factories/contracts/test/MaliciousVoter__factory'; -import { - address, - advanceBlocks, - deployGovernorV3WithV3Proxy, - deployNounsToken, - encodeParameters, - getSigners, - populateDescriptorV2, - setNextBlockBaseFee, - TestSigners, -} from '../../../utils'; - -chai.use(solidity); -const { expect } = chai; - -const realLongReason = - "Judge: The defense may proceed. Roark: Your Honor, I shall call no witnesses. This will be my testimony and my summation. Judge: Take the oath. Court Clerk: Do you swear to tell the truth, the whole truth, and nothing but the truth, so help you God? Roark: I do. Thousands of years ago, the first man discovered how to make fire. He was probably burned at the stake he had taught his brothers to light, but he left them a gift they had not conceived, and he lifted darkness off the earth. Throughout the centuries, there were men who took first steps down new roads, armed with nothing but their own vision. The great creators -- the thinkers, the artists, the scientists, the inventors -- stood alone against the men of their time. Every new thought was opposed; every new invention was denounced. But the men of unborrowed vision went ahead. They fought, they suffered, and they paid. But they won. No creator was prompted by a desire to please his brothers. His brothers hated the gift he offered. His truth was his only motive. His work was his only goal. His work -- not those who used it. His creation -- not the benefits others derived from it -- the creation which gave form to his truth. He held his truth above all things and against all men. He went ahead whether others agreed with him or not, with his integrity as his only banner. He served nothing and no one. He lived for himself. And only by living for himself was he able to achieve the things which are the glory of mankind. Such is the nature of achievement. Man cannot survive except through his mind. He comes on earth unarmed. His brain is his only weapon. But the mind is an attribute of the individual. There is no such thing as a collective brain. The man who thinks must think and act on his own. The reasoning mind cannot work under any form of compulsion. It cannot be subordinated to the needs, opinions, or wishes of others. It is not an object of sacrifice. The creator stands on his own judgment; the parasite follows the opinions of others. The creator thinks; the parasite copies. The creator produces; the parasite loots. The creator's concern is the conquest of nature; the parasite's concern is the conquest of men. The creator requires independence. He neither serves nor rules. He deals with men by free exchange and voluntary choice. The parasite seeks power. He wants to bind all men together in common action and common slavery. He claims that man is only a tool for the use of others -- that he must think as they think, act as they act, and live in selfless, joyless servitude to any need but his own. Look at history: Everything we have, every great achievement has come from the independent work of some independent mind. Every horror and destruction came from attempts to force men into a herd of brainless, soulless robots -- without personal rights, without person ambition, without will, hope, or dignity. It is an ancient conflict. It has another name: \"The individual against the collective.\" Our country, the noblest country in the history of men, was based on the principle of individualism, the principle of man's \"inalienable rights.\" It was a country where a man was free to seek his own happiness, to gain and produce, not to give up and renounce; to prosper, not to starve; to achieve, not to plunder; to hold as his highest possession a sense of his personal value, and as his highest virtue his self-respect. Look at the results. That is what the collectivists are now asking you to destroy, as much of the earth has been destroyed. I am an architect. I know what is to come by the principle on which it is built. We are approaching a world in which I cannot permit myself to live. My ideas are my property. They were taken from me by force, by breach of contract. No appeal was left to me. It was believed that my work belonged to others, to do with as they pleased. They had a claim upon me without my consent -- that it was my duty to serve them without choice or reward. Now you know why a dynamited Courtland. I designed Courtland. I made it possible. I destroyed it. I agreed to design it for the purpose of it seeing built as I wished. That was the price I set for my work. I was not paid. My building was disfigured at the whim of others who took all the benefits of my work and gave me nothing in return. I came here to say that I do not recognize anyone's right to one minute of my life, nor to any part of my energy, nor to any achievement of mine -- no matter who makes the claim! It had to be said: The world is perishing from an orgy of self-sacrificing. I came here to be heard in the name of every man of independence still left in the world. I wanted to state my terms. I do not care to work or live on any others. My terms are: A man's RIGHT to exist for his own sake."; -const LONG_REASON = realLongReason + realLongReason; -const REFUND_ERROR_MARGIN = ethers.utils.parseEther('0.001'); -const MAX_PRIORITY_FEE_CAP = ethers.utils.parseUnits('2', 'gwei'); -const DEFAULT_GAS_OPTIONS = { maxPriorityFeePerGas: MAX_PRIORITY_FEE_CAP }; -const MAX_REFUND_GAS_USED = BigNumber.from(200_000); -const MAX_REFUND_BASE_FEE = ethers.utils.parseUnits('200', 'gwei'); - -let deployer: SignerWithAddress; -let user: SignerWithAddress; -let user2: SignerWithAddress; -let signers: TestSigners; -let gov: NounsDAOLogicV3; -let token: NounsToken; -let snapshotId: number; - -describe('V3 Vote Refund', () => { - before(async () => { - signers = await getSigners(); - deployer = signers.deployer; - user = signers.account0; - user2 = signers.account1; - - token = await deployNounsToken(deployer); - const descriptor = NounsDescriptorV2__factory.connect(await token.descriptor(), deployer); - await populateDescriptorV2(descriptor); - - await token.connect(deployer).mint(); - await token.connect(deployer).transferFrom(deployer.address, user.address, 0); - await token.connect(deployer).transferFrom(deployer.address, user.address, 1); - - await advanceBlocks(1); - - gov = await deployGovernorV3WithV3Proxy(deployer, token.address); - await submitProposal(user); - }); - - beforeEach(async () => { - snapshotId = await ethers.provider.send('evm_snapshot', []); - }); - - afterEach(async () => { - await ethers.provider.send('evm_revert', [snapshotId]); - }); - - describe('castRefundableVote', () => { - it('refunds users with votes', async () => { - await fundGov(); - const balanceBefore = await user.getBalance(); - const tx = await gov.connect(user).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - - expect(r.gasUsed).to.be.gt(0); - expect(balanceDiff).to.be.closeTo(BigNumber.from(0), REFUND_ERROR_MARGIN); - expectRefundEvent(r, user, await txCostInEth(r)); - await expect(tx).to.emit(gov, 'VoteCast').withArgs(user.address, BigNumber.from(1), 1, 2, ''); - }); - - it('does not refund users with no votes', async () => { - await fundGov(); - const balanceBefore = await user2.getBalance(); - - const tx = await gov.connect(user2).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const balanceDiff = balanceBefore.sub(await user2.getBalance()); - expect(balanceDiff).to.be.eq(await txCostInEth(r)); - await expect(tx).to.changeEtherBalance(gov, 0); - }); - - it('caps refund', async () => { - await fundGov(); - const balanceBefore = await user.getBalance(); - - const tx = await gov.connect(user).castRefundableVote(1, 1, { - maxPriorityFeePerGas: ethers.utils.parseUnits('80', 'gwei'), - }); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.be.closeTo( - await expectedPriorityFeeCappedDiff(r), - REFUND_ERROR_MARGIN, - ); - }); - - it('does not refund when DAO balance is zero', async () => { - expect(await ethers.provider.getBalance(gov.address)).to.eq(0); - const balanceBefore = await user.getBalance(); - const tx = await gov.connect(user).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.be.eq(await txCostInEth(r)); - }); - - it('provides partial refund given insufficient balance', async () => { - await fundGov('0.00001'); - const govBalance = ethers.utils.parseEther('0.00001'); - expect(await ethers.provider.getBalance(gov.address)).to.eq(govBalance); - const balanceBefore = await user.getBalance(); - - const tx = await gov.connect(user).castRefundableVote(1, 1, DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const expectedDiff = (await txCostInEth(r)).sub(govBalance); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.eq(expectedDiff); - }); - - it('malicious voter trying reentrance does not get refunded', async () => { - const voter = await new MaliciousVoter__factory(deployer).deploy(gov.address, 2, 1, false); - await token.connect(user).transferFrom(user.address, voter.address, 0); - await token.connect(user).transferFrom(user.address, user2.address, 1); - await advanceBlocks(1); - await submitProposal(user2); - const balanceBefore = await user.getBalance(); - - const tx = await voter.connect(user).castVote(DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.be.eq(await txCostInEth(r)); - await expect(tx).to.changeEtherBalance(gov, 0); - }); - }); - - describe('castRefundableVoteWithReason', () => { - it('refunds users with votes', async () => { - await fundGov(); - const balanceBefore = await user.getBalance(); - const tx = await gov - .connect(user) - .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - - expect(r.gasUsed).to.be.gt(0); - expect(balanceDiff).to.be.closeTo(BigNumber.from(0), REFUND_ERROR_MARGIN); - - expectRefundEvent(r, user, await txCostInEth(r)); - await expect(tx) - .to.emit(gov, 'VoteCast') - .withArgs(user.address, BigNumber.from(1), 1, 2, 'some reason'); - }); - - it('does not refund users with no votes', async () => { - await fundGov(); - const balanceBefore = await user2.getBalance(); - - const tx = await gov - .connect(user2) - .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const balanceDiff = balanceBefore.sub(await user2.getBalance()); - expect(balanceDiff).to.be.eq(await txCostInEth(r)); - await expect(tx).to.changeEtherBalance(gov, 0); - }); - - it('caps refund priority fee', async () => { - await fundGov(); - const balanceBefore = await user.getBalance(); - - const tx = await gov.connect(user).castRefundableVoteWithReason(1, 1, 'some reason', { - maxPriorityFeePerGas: ethers.utils.parseUnits('80', 'gwei'), - }); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.be.closeTo( - await expectedPriorityFeeCappedDiff(r), - REFUND_ERROR_MARGIN, - ); - }); - - it('caps gasUsed', async () => { - await fundGov(); - const balanceBefore = await user.getBalance(); - const tx = await gov - .connect(user) - .castRefundableVoteWithReason(1, 1, LONG_REASON, DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - - expect(r.gasUsed).to.be.gt(0); - expect(balanceDiff).to.be.closeTo(await expectedGasUsedCappedDiff(r), REFUND_ERROR_MARGIN); - - expectRefundEvent(r, user, MAX_REFUND_GAS_USED.mul(await latestBasePlusMaxPriority())); - await expect(tx) - .to.emit(gov, 'VoteCast') - .withArgs(user.address, BigNumber.from(1), 1, 2, LONG_REASON); - }); - - it('caps basefee [ @skip-on-coverage ]', async () => { - await fundGov(); - await setNextBlockBaseFee(MAX_REFUND_BASE_FEE.mul(2)); - const balanceBefore = await user.getBalance(); - const tx = await gov - .connect(user) - .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - - expect(r.gasUsed).to.be.gt(0); - expect(balanceDiff).to.be.closeTo(await expectedBaseFeeCappedDiff(r), REFUND_ERROR_MARGIN); - - expectRefundEvent(r, user, r.gasUsed.mul(MAX_REFUND_BASE_FEE.add(MAX_PRIORITY_FEE_CAP))); - await expect(tx) - .to.emit(gov, 'VoteCast') - .withArgs(user.address, BigNumber.from(1), 1, 2, 'some reason'); - }); - - it('does not refund when DAO balance is zero', async () => { - expect(await ethers.provider.getBalance(gov.address)).to.eq(0); - const balanceBefore = await user.getBalance(); - const tx = await gov - .connect(user) - .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.be.eq(await txCostInEth(r)); - }); - - it('provides partial refund given insufficient balance', async () => { - await fundGov('0.00001'); - const govBalance = ethers.utils.parseEther('0.00001'); - expect(await ethers.provider.getBalance(gov.address)).to.eq(govBalance); - const balanceBefore = await user.getBalance(); - - const tx = await gov - .connect(user) - .castRefundableVoteWithReason(1, 1, 'some reason', DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - expect(r.gasUsed).to.be.gt(0); - const expectedDiff = (await txCostInEth(r)).sub(govBalance); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.eq(expectedDiff); - }); - - it('malicious voter trying reentrance does not get refunded', async () => { - const voter = await new MaliciousVoter__factory(deployer).deploy(gov.address, 2, 1, true); - await token.connect(user).transferFrom(user.address, voter.address, 0); - await token.connect(user).transferFrom(user.address, user2.address, 1); - await advanceBlocks(1); - await submitProposal(user2); - const balanceBefore = await user.getBalance(); - - const tx = await voter.connect(user).castVote(DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - - const balanceDiff = balanceBefore.sub(await user.getBalance()); - expect(balanceDiff).to.be.eq(await txCostInEth(r)); - await expect(tx).to.changeEtherBalance(gov, 0); - }); - - it('refunds EOA user when voting via a smart contract (tx.origin)', async () => { - await fundGov(); - - const voter = await new Voter__factory(deployer).deploy(gov.address, 2, 1, true); - await token.connect(user).transferFrom(user.address, voter.address, 0); - await token.connect(user).transferFrom(user.address, user2.address, 1); - await advanceBlocks(1); - await submitProposal(user2); - - const balanceBefore = await user.getBalance(); - const tx = await voter.connect(user).castVote(DEFAULT_GAS_OPTIONS); - const r = await tx.wait(); - const balanceDiff = balanceBefore.sub(await user.getBalance()); - - expect(r.gasUsed).to.be.gt(0); - expect(balanceDiff).to.be.closeTo(BigNumber.from(0), REFUND_ERROR_MARGIN); - - expectRefundEvent(r, user, await txCostInEth(r)); - await expect(tx) - .to.emit(gov, 'VoteCast') - .withArgs(voter.address, BigNumber.from(2), 1, 1, 'some reason'); - }); - }); - - async function expectedPriorityFeeCappedDiff(r: ContractReceipt): Promise { - const expectedRefund = await txCostInEth(r); - const txGrossCost = r.gasUsed.mul(r.effectiveGasPrice); - return txGrossCost.sub(expectedRefund); - } - - async function expectedGasUsedCappedDiff(r: ContractReceipt): Promise { - const gasPrice = await latestBasePlusMaxPriority(); - const expectedRefund = MAX_REFUND_GAS_USED.mul(gasPrice); - const txGrossCost = r.gasUsed.mul(gasPrice); - return txGrossCost.sub(expectedRefund); - } - - async function expectedBaseFeeCappedDiff(r: ContractReceipt): Promise { - const expectedRefund = r.gasUsed.mul(MAX_REFUND_BASE_FEE.add(MAX_PRIORITY_FEE_CAP)); - const txGrossCost = r.gasUsed.mul(await latestBasePlusMaxPriority()); - return txGrossCost.sub(expectedRefund); - } - - async function txCostInEth(r: ContractReceipt): Promise { - return r.gasUsed.mul(await latestBasePlusMaxPriority()); - } - - async function latestBasePlusMaxPriority(): Promise { - const block = await ethers.provider.getBlock('latest'); - return block.baseFeePerGas!.add(MAX_PRIORITY_FEE_CAP); - } - - async function fundGov(ethAmount: string = '100') { - await deployer.sendTransaction({ to: gov.address, value: ethers.utils.parseEther(ethAmount) }); - } - - function expectRefundEvent(r: ContractReceipt, u: SignerWithAddress, expectedCost: BigNumber) { - // Not using expect emit because it doesn't support the `closeTo` matcher - // Using longer event parsing because r.events doesn't work when using the Voter contract - // to simulate multisig usage; events are returned undefined - const daoInterface = NounsDAOLogicV3__factory.createInterface(); - const eventId = ethers.utils.id('RefundableVote(address,uint256,bool)'); - const filtered = r.logs.filter(l => l.topics[0] === eventId); - const parsed = filtered.map(e => { - return daoInterface.parseLog(e); - }); - - expect(parsed.length).to.equal(1); - const refundEvent = parsed[0]; - expect(refundEvent).to.not.be.undefined; - expect(refundEvent!.args!.voter).to.equal(u.address); - expect(refundEvent!.args!.refundSent).to.be.true; - expect(refundEvent!.args!.refundAmount).to.be.closeTo(expectedCost, REFUND_ERROR_MARGIN); - } - - async function submitProposal(u: SignerWithAddress) { - await gov - .connect(u) - .propose( - [address(0)], - ['0'], - ['getBalanceOf(address)'], - [encodeParameters(['address'], [address(0)])], - '', - ); - - await advanceBlocks(2); - } -}); From 0444a0f5f00c5560462650477e6d00814766692d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 15:17:48 -0500 Subject: [PATCH 03/28] delete dao v1 voting test there's a test file for V2 with the same logic --- .../test/governance/NounsDAO/castVote.test.ts | 183 ------------------ 1 file changed, 183 deletions(-) delete mode 100644 packages/nouns-contracts/test/governance/NounsDAO/castVote.test.ts diff --git a/packages/nouns-contracts/test/governance/NounsDAO/castVote.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/castVote.test.ts deleted file mode 100644 index 9260012690..0000000000 --- a/packages/nouns-contracts/test/governance/NounsDAO/castVote.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; -import hardhat from 'hardhat'; - -const { ethers } = hardhat; - -import { BigNumber as EthersBN } from 'ethers'; - -import { - deployNounsToken, - getSigners, - TestSigners, - setTotalSupply, - populateDescriptorV2, - propose, -} from '../../utils'; - -import { mineBlock, address } from '../../utils'; - -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { - NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, - NounsDAOLogicV1Harness, - NounsDAOLogicV1Harness__factory as NounsDaoLogicV1HarnessFactory, - NounsDAOProxy__factory as NounsDaoProxyFactory, -} from '../../../typechain'; - -chai.use(solidity); -const { expect } = chai; - -async function deployGovernor( - deployer: SignerWithAddress, - tokenAddress: string, -): Promise { - const { address: govDelegateAddress } = await new NounsDaoLogicV1HarnessFactory( - deployer, - ).deploy(); - const params: Parameters = [ - address(0), - tokenAddress, - deployer.address, - address(0), - govDelegateAddress, - 17280, - 1, - 1, - 1, - ]; - - const { address: _govDelegatorAddress } = await ( - await ethers.getContractFactory('NounsDAOProxy', deployer) - ).deploy(...params); - - return NounsDaoLogicV1HarnessFactory.connect(_govDelegatorAddress, deployer); -} - -let snapshotId: number; - -let token: NounsToken; -let deployer: SignerWithAddress; -let account0: SignerWithAddress; -let account1: SignerWithAddress; -let account2: SignerWithAddress; -let signers: TestSigners; - -let gov: NounsDAOLogicV1Harness; -let targets: string[]; -let values: string[]; -let signatures: string[]; -let callDatas: string[]; -let proposalId: EthersBN; - -async function reset() { - if (snapshotId) { - await ethers.provider.send('evm_revert', [snapshotId]); - snapshotId = await ethers.provider.send('evm_snapshot', []); - return; - } - token = await deployNounsToken(signers.deployer); - - await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), - ); - - await setTotalSupply(token, 10); - - gov = await deployGovernor(deployer, token.address); - snapshotId = await ethers.provider.send('evm_snapshot', []); -} - -describe('NounsDAO#castVote/2', () => { - before(async () => { - signers = await getSigners(); - deployer = signers.deployer; - account0 = signers.account0; - account1 = signers.account1; - account2 = signers.account2; - }); - - describe('We must revert if:', () => { - before(async () => { - await reset(); - proposalId = await propose(gov, deployer); - }); - - it("There does not exist a proposal with matching proposal id where the current block number is between the proposal's start block (exclusive) and end block (inclusive)", async () => { - await expect(gov.castVote(proposalId, 1)).revertedWith( - 'NounsDAO::castVoteInternal: voting is closed', - ); - }); - - it('Such proposal already has an entry in its voters set matching the sender', async () => { - await mineBlock(); - await mineBlock(); - - await token.transferFrom(deployer.address, account0.address, 0); - await token.transferFrom(deployer.address, account1.address, 1); - - await gov.connect(account0).castVote(proposalId, 1); - - await gov.connect(account1).castVoteWithReason(proposalId, 1, ''); - - await expect(gov.connect(account0).castVote(proposalId, 1)).revertedWith( - 'NounsDAO::castVoteInternal: voter already voted', - ); - }); - }); - - describe('Otherwise', () => { - it("we add the sender to the proposal's voters set", async () => { - const voteReceipt1 = await gov.getReceipt(proposalId, account2.address); - expect(voteReceipt1.hasVoted).to.equal(false); - - await gov.connect(account2).castVote(proposalId, 1); - const voteReceipt2 = await gov.getReceipt(proposalId, account2.address); - expect(voteReceipt2.hasVoted).to.equal(true); - }); - - describe("and we take the balance returned by GetPriorVotes for the given sender and the proposal's start block, which may be zero,", () => { - let actor: SignerWithAddress; // an account that will propose, receive tokens, delegate to self, and vote on own proposal - - before(reset); - - it('and we add that ForVotes', async () => { - actor = account0; - - await token.transferFrom(deployer.address, actor.address, 0); - await token.transferFrom(deployer.address, actor.address, 1); - proposalId = await propose(gov, actor); - - const beforeFors = (await gov.proposals(proposalId)).forVotes; - await mineBlock(); - await gov.connect(actor).castVote(proposalId, 1); - - const afterFors = (await gov.proposals(proposalId)).forVotes; - - const balance = (await token.balanceOf(actor.address)).toString(); - - expect(afterFors).to.equal(beforeFors.add(balance)); - }); - - it("or AgainstVotes corresponding to the caller's support flag.", async () => { - actor = account1; - await token.transferFrom(deployer.address, actor.address, 2); - await token.transferFrom(deployer.address, actor.address, 3); - - proposalId = await propose(gov, actor); - - const beforeAgainst = (await gov.proposals(proposalId)).againstVotes; - - await mineBlock(); - await gov.connect(actor).castVote(proposalId, 0); - - const afterAgainst = (await gov.proposals(proposalId)).againstVotes; - - const balance = (await token.balanceOf(actor.address)).toString(); - - expect(afterAgainst).to.equal(beforeAgainst.add(balance)); - }); - }); - }); -}); From 3e47b7853a96ccc597bcc700fd4221936aad8fcd Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 15:21:37 -0500 Subject: [PATCH 04/28] delete old dao logic test this is testing V1 there's a forge test for V2 with the same logic --- .../NounsDAO/inflationHandling.test.ts | 159 ------------------ 1 file changed, 159 deletions(-) delete mode 100644 packages/nouns-contracts/test/governance/NounsDAO/inflationHandling.test.ts diff --git a/packages/nouns-contracts/test/governance/NounsDAO/inflationHandling.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/inflationHandling.test.ts deleted file mode 100644 index cdf6bb0f66..0000000000 --- a/packages/nouns-contracts/test/governance/NounsDAO/inflationHandling.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; - -import { BigNumber as EthersBN } from 'ethers'; - -import { getSigners, TestSigners, setTotalSupply, deployGovAndToken } from '../../utils'; - -import { mineBlock, address, encodeParameters, advanceBlocks } from '../../utils'; - -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { NounsToken, NounsDAOLogicV1 } from '../../../typechain'; - -chai.use(solidity); -const { expect } = chai; - -async function propose(proposer: SignerWithAddress) { - targets = [account0.address]; - values = ['0']; - signatures = ['getBalanceOf(address)']; - callDatas = [encodeParameters(['address'], [account0.address])]; - - await gov.connect(proposer).propose(targets, values, signatures, callDatas, 'do nothing'); - proposalId = await gov.latestProposalIds(proposer.address); -} - -let token: NounsToken; -let deployer: SignerWithAddress; -let account0: SignerWithAddress; -let account1: SignerWithAddress; -let account2: SignerWithAddress; -let signers: TestSigners; - -let gov: NounsDAOLogicV1; -const timelockDelay = 172800; // 2 days - -const proposalThresholdBPS = 678; // 6.78% -const quorumVotesBPS = 1100; // 11% - -let targets: string[]; -let values: string[]; -let signatures: string[]; -let callDatas: string[]; -let proposalId: EthersBN; - -describe('NounsDAO#inflationHandling', () => { - before(async () => { - signers = await getSigners(); - deployer = signers.deployer; - account0 = signers.account0; - account1 = signers.account1; - account2 = signers.account2; - - targets = [account0.address]; - values = ['0']; - signatures = ['getBalanceOf(address)']; - callDatas = [encodeParameters(['address'], [account0.address])]; - - ({ token, gov } = await deployGovAndToken( - deployer, - timelockDelay, - proposalThresholdBPS, - quorumVotesBPS, - )); - }); - - it('set parameters correctly', async () => { - expect(await gov.proposalThresholdBPS()).to.equal(proposalThresholdBPS); - expect(await gov.quorumVotesBPS()).to.equal(quorumVotesBPS); - }); - - it('returns quorum votes and proposal threshold based on Noun total supply', async () => { - // Total Supply = 40 - await setTotalSupply(token, 40); - - await mineBlock(); - - // 6.78% of 40 = 2.712, floored to 2 - expect(await gov.proposalThreshold()).to.equal(2); - // 11% of 40 = 4.4, floored to 4 - expect(await gov.quorumVotes()).to.equal(4); - }); - - it('rejects if proposing below threshold', async () => { - // account0 has 1 token, requires 3 - await token.transferFrom(deployer.address, account0.address, 0); - await mineBlock(); - await expect( - gov.connect(account0).propose(targets, values, signatures, callDatas, 'do nothing'), - ).revertedWith('NounsDAO::propose: proposer votes below proposal threshold'); - }); - it('allows proposing if above threshold', async () => { - // account0 has 3 token, requires 3 - await token.transferFrom(deployer.address, account0.address, 1); - await token.transferFrom(deployer.address, account0.address, 2); - - // account1 has 3 tokens - await token.transferFrom(deployer.address, account1.address, 3); - await token.transferFrom(deployer.address, account1.address, 4); - await token.transferFrom(deployer.address, account1.address, 5); - - // account2 has 5 tokens - await token.transferFrom(deployer.address, account2.address, 6); - await token.transferFrom(deployer.address, account2.address, 7); - await token.transferFrom(deployer.address, account2.address, 8); - await token.transferFrom(deployer.address, account2.address, 9); - await token.transferFrom(deployer.address, account2.address, 10); - - await mineBlock(); - await propose(account0); - }); - - it('sets proposal attributes correctly', async () => { - const proposal = await gov.proposals(proposalId); - expect(proposal.proposalThreshold).to.equal(2); - expect(proposal.quorumVotes).to.equal(4); - }); - - it('returns updated quorum votes and proposal threshold when total supply changes', async () => { - // Total Supply = 80 - await setTotalSupply(token, 80); - - // 6.78% of 80 = 5.424, floored to 5 - expect(await gov.proposalThreshold()).to.equal(5); - // 11% of 80 = 8.88, floored to 8 - expect(await gov.quorumVotes()).to.equal(8); - }); - - it('rejects proposals that were previously above proposal threshold, but due to increasing supply are now below', async () => { - // account1 has 3 tokens, but requires 5 to pass new proposal threshold when totalSupply = 80 and threshold = 5% - await expect( - gov.connect(account1).propose(targets, values, signatures, callDatas, 'do nothing'), - ).revertedWith('NounsDAO::propose: proposer votes below proposal threshold'); - }); - - it('does not change previous proposal attributes when total supply changes', async () => { - const proposal = await gov.proposals(proposalId); - expect(proposal.proposalThreshold).to.equal(2); - expect(proposal.quorumVotes).to.equal(4); - }); - - it('updates for/against votes correctly', async () => { - // Accounts voting for = 5 votes - // forVotes should be greater than quorumVotes - await gov.connect(account0).castVote(proposalId, 1); // 3 - await gov.connect(account1).castVote(proposalId, 1); // 3 - - await gov.connect(account2).castVote(proposalId, 0); // 5 - - const proposal = await gov.proposals(proposalId); - expect(proposal.forVotes).to.equal(6); - expect(proposal.againstVotes).to.equal(5); - }); - - it('succeeds when for forVotes > quorumVotes and againstVotes', async () => { - await advanceBlocks(5760); - const state = await gov.state(proposalId); - expect(state).to.equal(4); - }); -}); From 2080f585bf293726c40615a086418d0e6705649d Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 15:38:29 -0500 Subject: [PATCH 05/28] point castVote tests to V3 --- .../test/governance/NounsDAO/V2/castVote.test.ts | 10 +++++----- packages/nouns-contracts/test/utils.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/castVote.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/castVote.test.ts index e7ed6d153c..1dfcf1591a 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/castVote.test.ts +++ b/packages/nouns-contracts/test/governance/NounsDAO/V2/castVote.test.ts @@ -12,7 +12,7 @@ import { TestSigners, setTotalSupply, propose, - deployGovernorV2WithV2Proxy, + deployGovernorV3WithV3Proxy, populateDescriptorV2, } from '../../../utils'; @@ -21,7 +21,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, NounsDescriptorV2__factory as NounsDescriptorV2Factory, - NounsDAOLogicV2, + NounsDAOLogicV3, } from '../../../../typechain'; chai.use(solidity); @@ -36,7 +36,7 @@ let account1: SignerWithAddress; let account2: SignerWithAddress; let signers: TestSigners; -let gov: NounsDAOLogicV2; +let gov: NounsDAOLogicV3; let proposalId: EthersBN; async function reset() { @@ -53,7 +53,7 @@ async function reset() { await setTotalSupply(token, 10); - gov = await deployGovernorV2WithV2Proxy(deployer, token.address); + gov = await deployGovernorV3WithV3Proxy(deployer, token.address); snapshotId = await ethers.provider.send('evm_snapshot', []); } @@ -90,7 +90,7 @@ describe('NounsDAOV2#castVote/2', () => { await gov.connect(account1).castVoteWithReason(proposalId, 1, ''); await expect(gov.connect(account0).castVote(proposalId, 1)).revertedWith( - 'NounsDAO::castVoteInternal: voter already voted', + 'NounsDAO::castVoteDuringVotingPeriodInternal: voter already voted', ); }); }); diff --git a/packages/nouns-contracts/test/utils.ts b/packages/nouns-contracts/test/utils.ts index 6383871c85..6a3d7b338f 100644 --- a/packages/nouns-contracts/test/utils.ts +++ b/packages/nouns-contracts/test/utils.ts @@ -498,7 +498,7 @@ export const deployGovernorV2AndSetQuorumParams = async ( }; export const propose = async ( - gov: NounsDAOLogicV1 | NounsDAOLogicV2, + gov: NounsDAOLogicV1 | NounsDAOLogicV2 | NounsDAOLogicV3, proposer: SignerWithAddress, stubPropUserAddress: string = address(0), ) => { From d211b97c89d1e42c1be5ec79be8dbc75a76edb67 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 15:44:12 -0500 Subject: [PATCH 06/28] point DQ tests to V3 --- .../NounsDAO/V2/dynamicQuorum.test.ts | 15 +++------- packages/nouns-contracts/test/utils.ts | 29 ++++++++++--------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/dynamicQuorum.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/dynamicQuorum.test.ts index 582e14509d..b6c2a76212 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/dynamicQuorum.test.ts +++ b/packages/nouns-contracts/test/governance/NounsDAO/V2/dynamicQuorum.test.ts @@ -2,28 +2,21 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import chai from 'chai'; import { solidity } from 'ethereum-waffle'; import { parseUnits } from 'ethers/lib/utils'; -import { - NounsDAOLogicV2, - NounsDAOLogicV2__factory as NounsDaoLogicV2Factory, -} from '../../../../typechain'; -import { getSigners, TestSigners } from '../../../utils'; +import { NounsDAOLogicV3 } from '../../../../typechain'; +import { deployGovernorV3, getSigners, TestSigners } from '../../../utils'; chai.use(solidity); const { expect } = chai; let deployer: SignerWithAddress; let signers: TestSigners; -let gov: NounsDAOLogicV2; - -async function deployGovernorV2(deployer: SignerWithAddress): Promise { - return await new NounsDaoLogicV2Factory(deployer).deploy(); -} +let gov: NounsDAOLogicV3; describe('Dynamic Quorum', () => { before(async () => { signers = await getSigners(); deployer = signers.deployer; - gov = await deployGovernorV2(deployer); + gov = await deployGovernorV3(deployer); }); it('coefficient set to zero', async () => { diff --git a/packages/nouns-contracts/test/utils.ts b/packages/nouns-contracts/test/utils.ts index 6a3d7b338f..9c9d3daf48 100644 --- a/packages/nouns-contracts/test/utils.ts +++ b/packages/nouns-contracts/test/utils.ts @@ -531,18 +531,7 @@ function dataToDescriptorInput(data: string[]): { }; } -export const deployGovernorV3WithV3Proxy = async ( - deployer: SignerWithAddress, - tokenAddress: string, - timelockAddress?: string, - forkEscrowAddress?: string, - forkDAODeployerAddress?: string, - vetoerAddress?: string, - votingPeriod?: number, - votingDelay?: number, - proposalThresholdBPs?: number, - dynamicQuorumParams?: DynamicQuorumParams, -): Promise => { +export const deployGovernorV3 = async (deployer: SignerWithAddress): Promise => { const NounsDAOV3Proposals = await ( await ethers.getContractFactory('NounsDAOV3Proposals', deployer) ).deploy(); @@ -559,7 +548,7 @@ export const deployGovernorV3WithV3Proxy = async ( await ethers.getContractFactory('NounsDAOV3DynamicQuorum', deployer) ).deploy(); - const v3LogicContract = await new NounsDaoLogicV3Factory( + return await new NounsDaoLogicV3Factory( { 'contracts/governance/NounsDAOV3Proposals.sol:NounsDAOV3Proposals': NounsDAOV3Proposals.address, @@ -571,7 +560,21 @@ export const deployGovernorV3WithV3Proxy = async ( }, deployer, ).deploy(); +}; +export const deployGovernorV3WithV3Proxy = async ( + deployer: SignerWithAddress, + tokenAddress: string, + timelockAddress?: string, + forkEscrowAddress?: string, + forkDAODeployerAddress?: string, + vetoerAddress?: string, + votingPeriod?: number, + votingDelay?: number, + proposalThresholdBPs?: number, + dynamicQuorumParams?: DynamicQuorumParams, +): Promise => { + const v3LogicContract = await deployGovernorV3(deployer); const predictedProxyAddress = ethers.utils.getContractAddress({ from: deployer.address, nonce: (await deployer.getTransactionCount()) + 1, From 0c725c170c39b568019182e2ce85b6ee4f2dcd0e Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:11:49 -0500 Subject: [PATCH 07/28] point propose test to V3 and move to foundry --- .../foundry/NounsDAOLogicV3/Propose.t.sol | 57 ++++++++++++ .../governance/NounsDAO/V2/propose.test.ts | 88 ------------------- 2 files changed, 57 insertions(+), 88 deletions(-) create mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Propose.t.sol delete mode 100644 packages/nouns-contracts/test/governance/NounsDAO/V2/propose.test.ts diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Propose.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Propose.t.sol new file mode 100644 index 0000000000..e019ea9401 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Propose.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Test.sol'; +import { NounsDAOLogicV3BaseTest } from './NounsDAOLogicV3BaseTest.sol'; + +contract ProposeTest is NounsDAOLogicV3BaseTest { + address proposer = makeAddr('proposer'); + + function setUp() public override { + super.setUp(); + + vm.prank(address(dao.timelock())); + dao._setProposalThresholdBPS(1_000); + + for (uint256 i = 0; i < 10; i++) { + mintTo(proposer); + } + } + + function testEmits_ProposalCreatedWithRequirements() public { + address[] memory targets = new address[](1); + targets[0] = makeAddr('target'); + uint256[] memory values = new uint256[](1); + values[0] = 42; + string[] memory signatures = new string[](1); + signatures[0] = 'some signature'; + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = ''; + + uint256 updatablePeriodEndBlock = block.number + dao.proposalUpdatablePeriodInBlocks(); + uint256 startBlock = updatablePeriodEndBlock + dao.votingDelay(); + uint256 endBlock = startBlock + dao.votingPeriod(); + + vm.expectEmit(true, true, true, true); + + emit ProposalCreatedWithRequirements( + 1, + proposer, + new address[](0), + targets, + values, + signatures, + calldatas, + startBlock, + endBlock, + updatablePeriodEndBlock, + 1, // prop threshold + dao.minQuorumVotes(), + 'some description' + ); + + vm.prank(proposer); + + dao.propose(targets, values, signatures, calldatas, 'some description'); + } +} diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/propose.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/propose.test.ts deleted file mode 100644 index 2c10d6d30d..0000000000 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/propose.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; -import { - NounsDAOLogicV2, - NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, -} from '../../../../typechain'; -import { - address, - blockNumber, - deployGovernorV2WithV2Proxy, - deployNounsToken, - encodeParameters, - getSigners, - populateDescriptorV2, - setTotalSupply, - TestSigners, -} from '../../../utils'; - -chai.use(solidity); -const { expect } = chai; - -let token: NounsToken; -let deployer: SignerWithAddress; -let gov: NounsDAOLogicV2; -let signers: TestSigners; - -const votingDelay = 5; -const votingPeriod = 5760; -const proposalThresholdBPs = 1000; // 10% -const MIN_QUORUM_VOTES_BPS = 2000; // 20% -const MAX_QUORUM_VOTES_BPS = 4000; // 40% - -describe('NounsDAOV2#propose', async () => { - before(async () => { - signers = await getSigners(); - deployer = signers.deployer; - token = await deployNounsToken(signers.deployer); - - await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), - ); - - await setTotalSupply(token, 10); - gov = await deployGovernorV2WithV2Proxy( - deployer, - token.address, - deployer.address, - deployer.address, - votingPeriod, - votingDelay, - proposalThresholdBPs, - { - minQuorumVotesBPS: MIN_QUORUM_VOTES_BPS, - maxQuorumVotesBPS: MAX_QUORUM_VOTES_BPS, - quorumCoefficient: 0, - }, - ); - }); - - it('emits ProposalCreatedWithRequirements', async () => { - const targets = [address(0)]; - const values = ['0']; - const signatures = ['getBalanceOf(address)']; - const callDatas = [encodeParameters(['address'], [address(0)])]; - - const blockNum = await blockNumber(); - - await expect( - gov.connect(deployer).propose(targets, values, signatures, callDatas, 'do nothing'), - ) - .to.emit(gov, 'ProposalCreatedWithRequirements') - .withArgs( - 1, - deployer.address, - targets, - values, - signatures, - callDatas, - votingDelay + blockNum + 1, - votingPeriod + votingDelay + blockNum + 1, - 1, - 2, - 'do nothing', - ); - }); -}); From fb552ece38c47a2c8ecaa5358dc12b5fb47ffcf8 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:19:10 -0500 Subject: [PATCH 08/28] point proxy test to V3 --- .../V2/{proxyV2.test.ts => proxy.test.ts} | 20 +++++++++---------- packages/nouns-contracts/test/utils.ts | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) rename packages/nouns-contracts/test/governance/NounsDAO/V2/{proxyV2.test.ts => proxy.test.ts} (79%) diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/proxyV2.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/proxy.test.ts similarity index 79% rename from packages/nouns-contracts/test/governance/NounsDAO/V2/proxyV2.test.ts rename to packages/nouns-contracts/test/governance/NounsDAO/V2/proxy.test.ts index 9a08db8da8..ba0c40d567 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/proxyV2.test.ts +++ b/packages/nouns-contracts/test/governance/NounsDAO/V2/proxy.test.ts @@ -6,15 +6,15 @@ import { TestSigners, setTotalSupply, blockNumber, - deployGovernorV2WithV2Proxy, populateDescriptorV2, + deployGovernorV3WithV3Proxy, } from '../../../utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, NounsDescriptorV2__factory as NounsDescriptorV2Factory, - NounsDAOLogicV2, + NounsDAOLogicV3, } from '../../../../typechain'; import { MAX_QUORUM_VOTES_BPS, MIN_QUORUM_VOTES_BPS } from '../../../constants'; @@ -24,8 +24,7 @@ const { expect } = chai; let token: NounsToken; let deployer: SignerWithAddress; let signers: TestSigners; - -let govV2: NounsDAOLogicV2; +let gov: NounsDAOLogicV3; async function setup() { token = await deployNounsToken(signers.deployer); @@ -37,7 +36,7 @@ async function setup() { await setTotalSupply(token, 100); } -describe('NounsDAOProxyV2', () => { +describe('NounsDAOProxyV3', () => { before(async () => { signers = await getSigners(); deployer = signers.deployer; @@ -46,12 +45,13 @@ describe('NounsDAOProxyV2', () => { }); it('Deploys successfully', async () => { - govV2 = await deployGovernorV2WithV2Proxy( + gov = await deployGovernorV3WithV3Proxy( deployer, token.address, deployer.address, deployer.address, - 5760, + deployer.address, + 7200, 1, 1, { @@ -63,12 +63,12 @@ describe('NounsDAOProxyV2', () => { }); it('Sets some basic parameters as expected', async () => { - expect(await govV2.votingPeriod()).to.equal(5760); - expect(await govV2.timelock()).to.equal(deployer.address); + expect(await gov.votingPeriod()).to.equal(7200); + expect(await gov.timelock()).to.equal(deployer.address); }); it('Sets quorum params as expected', async () => { - const params = await govV2.getDynamicQuorumParamsAt(await blockNumber()); + const params = await gov.getDynamicQuorumParamsAt(await blockNumber()); expect(params.quorumCoefficient).to.equal(3); }); }); diff --git a/packages/nouns-contracts/test/utils.ts b/packages/nouns-contracts/test/utils.ts index 9c9d3daf48..637c7c94a7 100644 --- a/packages/nouns-contracts/test/utils.ts +++ b/packages/nouns-contracts/test/utils.ts @@ -566,7 +566,6 @@ export const deployGovernorV3WithV3Proxy = async ( deployer: SignerWithAddress, tokenAddress: string, timelockAddress?: string, - forkEscrowAddress?: string, forkDAODeployerAddress?: string, vetoerAddress?: string, votingPeriod?: number, From 606e3193e52a128fa993a15944853c58a4e8a2b7 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:31:37 -0500 Subject: [PATCH 09/28] point quorumConfig test to V3 --- .../governance/NounsDAO/V2/quorumConfig.test.ts | 15 ++++++++------- packages/nouns-contracts/test/utils.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/quorumConfig.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/quorumConfig.test.ts index fce366871d..f76deb2031 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/quorumConfig.test.ts +++ b/packages/nouns-contracts/test/governance/NounsDAO/V2/quorumConfig.test.ts @@ -9,14 +9,14 @@ import { deployGovernorV1, blockNumber, advanceBlocks, - deployGovernorV2, populateDescriptorV2, + deployGovernorV3AndSetImpl, } from '../../../utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, NounsDescriptorV2__factory as NounsDescriptorV2Factory, - NounsDAOLogicV2, + NounsDAOLogicV3, } from '../../../../typechain'; import { parseUnits } from 'ethers/lib/utils'; import { DynamicQuorumParams } from '../../../types'; @@ -29,12 +29,12 @@ let token: NounsToken; let deployer: SignerWithAddress; let account0: SignerWithAddress; let signers: TestSigners; -let gov: NounsDAOLogicV2; +let gov: NounsDAOLogicV3; let snapshotId: number; const V1_QUORUM_BPS = 201; -async function setupWithV2() { +async function setup() { token = await deployNounsToken(signers.deployer); await populateDescriptorV2( @@ -48,16 +48,17 @@ async function setupWithV2() { token.address, V1_QUORUM_BPS, ); - gov = await deployGovernorV2(deployer, govProxyAddress); + + gov = await deployGovernorV3AndSetImpl(deployer, govProxyAddress); } -describe('NounsDAOV2#_setDynamicQuorumParams', () => { +describe('NounsDAO#_setDynamicQuorumParams', () => { before(async () => { signers = await getSigners(); deployer = signers.deployer; account0 = signers.account0; - await setupWithV2(); + await setup(); }); beforeEach(async () => { diff --git a/packages/nouns-contracts/test/utils.ts b/packages/nouns-contracts/test/utils.ts index 637c7c94a7..77beb5914e 100644 --- a/packages/nouns-contracts/test/utils.ts +++ b/packages/nouns-contracts/test/utils.ts @@ -562,6 +562,18 @@ export const deployGovernorV3 = async (deployer: SignerWithAddress): Promise => { + const v3LogicContract = await deployGovernorV3(deployer); + + const proxy = NounsDaoProxyFactory.connect(proxyAddress, deployer); + await proxy._setImplementation(v3LogicContract.address); + + return NounsDaoLogicV3Factory.connect(proxyAddress, deployer); +}; + export const deployGovernorV3WithV3Proxy = async ( deployer: SignerWithAddress, tokenAddress: string, From 34e7a1547a6e3649ac3ba5b6f5db417dfbb1f4e2 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:32:40 -0500 Subject: [PATCH 10/28] remove tests for V1 > V2 upgrade --- .../NounsDAO/V2/upgradeToV2.test.ts | 137 ------------------ .../NounsDAO/V2/votingDelayBugfix.test.ts | 106 -------------- 2 files changed, 243 deletions(-) delete mode 100644 packages/nouns-contracts/test/governance/NounsDAO/V2/upgradeToV2.test.ts delete mode 100644 packages/nouns-contracts/test/governance/NounsDAO/V2/votingDelayBugfix.test.ts diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/upgradeToV2.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/upgradeToV2.test.ts deleted file mode 100644 index 03e6570c4e..0000000000 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/upgradeToV2.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; -import { - deployNounsToken, - getSigners, - TestSigners, - setTotalSupply, - advanceBlocks, - propStateToString, - deployGovernorV1, - deployGovernorV2, - propose, - blockNumber, - populateDescriptorV2, -} from '../../../utils'; -import { mineBlock } from '../../../utils'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { - NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, - NounsDAOLogicV1, - NounsDAOLogicV1__factory as NounsDaoLogicV1Factory, - NounsDAOLogicV2, - NounsDAOLogicV2__factory as NounsDaoLogicV2Factory, -} from '../../../../typechain'; -import { MAX_QUORUM_VOTES_BPS, MIN_QUORUM_VOTES_BPS } from '../../../constants'; - -chai.use(solidity); -const { expect } = chai; - -const V1_QUORUM_BPS = 100; - -let token: NounsToken; -let deployer: SignerWithAddress; -let account0: SignerWithAddress; -let account1: SignerWithAddress; -let account2: SignerWithAddress; -let signers: TestSigners; - -let govProxyAddress: string; -let govV1: NounsDAOLogicV1; -let govV2: NounsDAOLogicV2; - -async function setupWithV1() { - token = await deployNounsToken(signers.deployer); - - await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), - ); - - await setTotalSupply(token, 100); - - ({ address: govProxyAddress } = await deployGovernorV1(deployer, token.address, V1_QUORUM_BPS)); -} - -describe('NounsDAO upgrade to V2', () => { - before(async () => { - signers = await getSigners(); - deployer = signers.deployer; - account0 = signers.account0; - account1 = signers.account1; - account2 = signers.account2; - - await setupWithV1(); - - govV1 = NounsDaoLogicV1Factory.connect(govProxyAddress, deployer); - govV2 = NounsDaoLogicV2Factory.connect(govProxyAddress, deployer); - }); - - it('Simulate some proposals in V1', async () => { - await token.connect(deployer).transferFrom(deployer.address, account0.address, 0); - await token.connect(deployer).transferFrom(deployer.address, account1.address, 1); - - // Prop 1 - await propose(govV1, account0); - await mineBlock(); - await govV1.connect(account0).castVote(1, 1); - await advanceBlocks(2000); - - // Prop 2 - await propose(govV1, account1); - await advanceBlocks(2); - - // Prop 3 - await propose(govV1, account0); - await advanceBlocks(2); - }); - - it('and upgrade to V2', async () => { - await deployGovernorV2(deployer, govProxyAddress); - }); - - it('and V2 returns default quorum params with V1 value', async () => { - const quorumParams = await govV2.getDynamicQuorumParamsAt(await blockNumber()); - - expect(quorumParams.minQuorumVotesBPS).to.equal(V1_QUORUM_BPS); - expect(quorumParams.maxQuorumVotesBPS).to.equal(V1_QUORUM_BPS); - expect(quorumParams.quorumCoefficient).to.equal(0); - }); - - it('and V2 config set', async () => { - await govV2._setDynamicQuorumParams(MIN_QUORUM_VOTES_BPS, MAX_QUORUM_VOTES_BPS, 0); - - const quorumParams = await govV2.getDynamicQuorumParamsAt(await blockNumber()); - - expect(quorumParams.minQuorumVotesBPS).to.equal(MIN_QUORUM_VOTES_BPS); - expect(quorumParams.maxQuorumVotesBPS).to.equal(MAX_QUORUM_VOTES_BPS); - }); - - it('and V1 proposalCount stayed the same, meaning the storage slot below the rename is good', async () => { - expect(await govV2.proposalCount()).to.equal(3); - }); - - it('and V1 Props have the same quorumVotes', async () => { - expect(await govV2.quorumVotes(3)).to.equal(1); - }); - - it('and V2 props have a different quorum', async () => { - await token.connect(deployer).transferFrom(deployer.address, account2.address, 3); - const propId = await propose(govV2, account2); - - expect(await govV2.quorumVotes(propId)).to.equal(2); - }); - - it('and V1 and V2 props reach their end state as expected', async () => { - await govV2.connect(account0).castVote(3, 1); - // Prop 4 will fail because it's using the new and higher quorum - await govV2.connect(account1).castVote(4, 1); - - await advanceBlocks(2000); - - expect(propStateToString(await govV2.state(1)), '1').to.equal('Succeeded'); - expect(propStateToString(await govV2.state(2)), '2').to.equal('Defeated'); - expect(propStateToString(await govV2.state(3)), '3').to.equal('Succeeded'); - expect(propStateToString(await govV2.state(4)), '4').to.equal('Defeated'); - }); -}); diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/votingDelayBugfix.test.ts b/packages/nouns-contracts/test/governance/NounsDAO/V2/votingDelayBugfix.test.ts deleted file mode 100644 index 37add81d1c..0000000000 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/votingDelayBugfix.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; -import { - deployNounsToken, - getSigners, - TestSigners, - setTotalSupply, - advanceBlocks, - deployGovernorV1, - deployGovernorV2AndSetQuorumParams, - propose, - populateDescriptorV2, -} from '../../../utils'; -import { mineBlock } from '../../../utils'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { - NounsToken, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, - NounsDAOLogicV1, - NounsDAOLogicV1__factory as NounsDaoLogicV1Factory, - NounsDAOLogicV2, - NounsDAOLogicV2__factory as NounsDaoLogicV2Factory, -} from '../../../../typechain'; - -chai.use(solidity); -const { expect } = chai; - -const V1_QUORUM_BPS = 100; - -let token: NounsToken; -let deployer: SignerWithAddress; -let account0: SignerWithAddress; -let account1: SignerWithAddress; -let signers: TestSigners; - -let govProxyAddress: string; -let govV1: NounsDAOLogicV1; -let govV2: NounsDAOLogicV2; - -async function setupWithV1() { - token = await deployNounsToken(signers.deployer); - - await populateDescriptorV2( - NounsDescriptorV2Factory.connect(await token.descriptor(), signers.deployer), - ); - - await setTotalSupply(token, 100); - - ({ address: govProxyAddress } = await deployGovernorV1(deployer, token.address, V1_QUORUM_BPS)); -} - -async function createPropEditVotingDelayFlow( - gov: NounsDAOLogicV1 | NounsDAOLogicV2, - user: SignerWithAddress, - tokenId: number, -) { - await gov._setVotingDelay(1); - await advanceBlocks(10); - await token.connect(deployer).transferFrom(deployer.address, user.address, tokenId); - - await propose(gov, user); - await mineBlock(); - - await gov._setVotingDelay(10); - return await gov.latestProposalIds(user.address); -} - -describe('NounsDAOV2 votingDelay bugfix', () => { - before(async () => { - signers = await getSigners(); - deployer = signers.deployer; - account0 = signers.account0; - account1 = signers.account1; - - await setupWithV1(); - - govV1 = NounsDaoLogicV1Factory.connect(govProxyAddress, deployer); - govV2 = NounsDaoLogicV2Factory.connect(govProxyAddress, deployer); - }); - - it('Simulate the bug in V1', async () => { - const propId = await createPropEditVotingDelayFlow(govV1, account0, 0); - let prop = await govV1.proposals(propId); - expect(prop.forVotes).to.equal(0); - - await govV1.connect(account0).castVote(propId, 1); - - prop = await govV1.proposals(propId); - expect(prop.forVotes).to.equal(0); - }); - - it('and upgrade to V2', async () => { - await deployGovernorV2AndSetQuorumParams(deployer, govProxyAddress); - }); - - it('and V2 fixes the bug', async () => { - const propId = await createPropEditVotingDelayFlow(govV2, account1, 1); - let prop = await govV2.proposals(propId); - expect(prop.forVotes).to.equal(0); - - await govV2.connect(account1).castVote(propId, 1); - - prop = await govV2.proposals(propId); - expect(prop.forVotes).to.equal(1); - }); -}); From b73ba895e2ee0a640b90e530da368044922a8d2b Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:34:18 -0500 Subject: [PATCH 11/28] remove dao V1 e2e test it's available in the V1 tag --- packages/nouns-contracts/test/end2end.test.ts | 289 ------------------ 1 file changed, 289 deletions(-) delete mode 100644 packages/nouns-contracts/test/end2end.test.ts diff --git a/packages/nouns-contracts/test/end2end.test.ts b/packages/nouns-contracts/test/end2end.test.ts deleted file mode 100644 index 7385e69a44..0000000000 --- a/packages/nouns-contracts/test/end2end.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import chai from 'chai'; -import { ethers, upgrades } from 'hardhat'; -import { BigNumber as EthersBN } from 'ethers'; -import { solidity } from 'ethereum-waffle'; - -import { - WETH, - NounsToken, - NounsAuctionHouse, - NounsAuctionHouse__factory as NounsAuctionHouseFactory, - NounsDescriptorV2, - NounsDescriptorV2__factory as NounsDescriptorV2Factory, - NounsDAOProxy__factory as NounsDaoProxyFactory, - NounsDAOLogicV1, - NounsDAOLogicV1__factory as NounsDaoLogicV1Factory, - NounsDAOExecutor, - NounsDAOExecutor__factory as NounsDaoExecutorFactory, -} from '../typechain'; - -import { - deployNounsToken, - deployWeth, - populateDescriptorV2, - address, - encodeParameters, - advanceBlocks, - blockTimestamp, - setNextBlockTimestamp, -} from './utils'; - -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; - -chai.use(solidity); -const { expect } = chai; - -let nounsToken: NounsToken; -let nounsAuctionHouse: NounsAuctionHouse; -let descriptor: NounsDescriptorV2; -let weth: WETH; -let gov: NounsDAOLogicV1; -let timelock: NounsDAOExecutor; - -let deployer: SignerWithAddress; -let wethDeployer: SignerWithAddress; -let bidderA: SignerWithAddress; -let noundersDAO: SignerWithAddress; - -// Governance Config -const TIME_LOCK_DELAY = 172_800; // 2 days -const PROPOSAL_THRESHOLD_BPS = 500; // 5% -const QUORUM_VOTES_BPS = 1_000; // 10% -const VOTING_PERIOD = 5_760; // About 24 hours with 15s blocks -const VOTING_DELAY = 1; // 1 block - -// Proposal Config -const targets: string[] = []; -const values: string[] = []; -const signatures: string[] = []; -const callDatas: string[] = []; - -let proposalId: EthersBN; - -// Auction House Config -const TIME_BUFFER = 15 * 60; -const RESERVE_PRICE = 2; -const MIN_INCREMENT_BID_PERCENTAGE = 5; -const DURATION = 60 * 60 * 24; - -async function deploy() { - [deployer, bidderA, wethDeployer, noundersDAO] = await ethers.getSigners(); - - // Deployed by another account to simulate real network - - weth = await deployWeth(wethDeployer); - - // nonce 2: Deploy AuctionHouse - // nonce 3: Deploy nftDescriptorLibraryFactory - // nonce 4: Deploy NounsDescriptor - // nonce 5: Deploy NounsSeeder - // nonce 6: Deploy NounsToken - // nonce 0: Deploy NounsDAOExecutor - // nonce 1: Deploy NounsDAOLogicV1 - // nonce 7: Deploy NounsDAOProxy - // nonce ++: populate Descriptor - // nonce ++: set ownable contracts owner to timelock - - // 1. DEPLOY Nouns token - nounsToken = await deployNounsToken( - deployer, - noundersDAO.address, - deployer.address, // do not know minter/auction house yet - ); - - // 2a. DEPLOY AuctionHouse - const auctionHouseFactory = await ethers.getContractFactory('NounsAuctionHouse', deployer); - const nounsAuctionHouseProxy = await upgrades.deployProxy(auctionHouseFactory, [ - nounsToken.address, - weth.address, - TIME_BUFFER, - RESERVE_PRICE, - MIN_INCREMENT_BID_PERCENTAGE, - DURATION, - ]); - - // 2b. CAST proxy as AuctionHouse - nounsAuctionHouse = NounsAuctionHouseFactory.connect(nounsAuctionHouseProxy.address, deployer); - - // 3. SET MINTER - await nounsToken.setMinter(nounsAuctionHouse.address); - - // 4. POPULATE body parts - descriptor = NounsDescriptorV2Factory.connect(await nounsToken.descriptor(), deployer); - - await populateDescriptorV2(descriptor); - - // 5a. CALCULATE Gov Delegate, takes place after 2 transactions - const calculatedGovDelegatorAddress = ethers.utils.getContractAddress({ - from: deployer.address, - nonce: (await deployer.getTransactionCount()) + 2, - }); - - // 5b. DEPLOY NounsDAOExecutor with pre-computed Delegator address - timelock = await new NounsDaoExecutorFactory(deployer).deploy( - calculatedGovDelegatorAddress, - TIME_LOCK_DELAY, - ); - - // 6. DEPLOY Delegate - const govDelegate = await new NounsDaoLogicV1Factory(deployer).deploy(); - - // 7a. DEPLOY Delegator - const nounsDAOProxy = await new NounsDaoProxyFactory(deployer).deploy( - timelock.address, - nounsToken.address, - noundersDAO.address, // NoundersDAO is vetoer - timelock.address, - govDelegate.address, - VOTING_PERIOD, - VOTING_DELAY, - PROPOSAL_THRESHOLD_BPS, - QUORUM_VOTES_BPS, - ); - - expect(calculatedGovDelegatorAddress).to.equal(nounsDAOProxy.address); - - // 7b. CAST Delegator as Delegate - gov = NounsDaoLogicV1Factory.connect(nounsDAOProxy.address, deployer); - - // 8. SET Nouns owner to NounsDAOExecutor - await nounsToken.transferOwnership(timelock.address); - // 9. SET Descriptor owner to NounsDAOExecutor - await descriptor.transferOwnership(timelock.address); - - // 10. UNPAUSE auction and kick off first mint - await nounsAuctionHouse.unpause(); - - // 11. SET Auction House owner to NounsDAOExecutor - await nounsAuctionHouse.transferOwnership(timelock.address); -} - -describe('End to End test with deployment, auction, proposing, voting, executing', async () => { - before(deploy); - - it('sets all starting params correctly', async () => { - expect(await nounsToken.owner()).to.equal(timelock.address); - expect(await descriptor.owner()).to.equal(timelock.address); - expect(await nounsAuctionHouse.owner()).to.equal(timelock.address); - - expect(await nounsToken.minter()).to.equal(nounsAuctionHouse.address); - expect(await nounsToken.noundersDAO()).to.equal(noundersDAO.address); - - expect(await gov.admin()).to.equal(timelock.address); - expect(await timelock.admin()).to.equal(gov.address); - expect(await gov.timelock()).to.equal(timelock.address); - - expect(await gov.vetoer()).to.equal(noundersDAO.address); - - expect(await nounsToken.totalSupply()).to.equal(EthersBN.from('2')); - - expect(await nounsToken.ownerOf(0)).to.equal(noundersDAO.address); - expect(await nounsToken.ownerOf(1)).to.equal(nounsAuctionHouse.address); - - expect((await nounsAuctionHouse.auction()).nounId).to.equal(EthersBN.from('1')); - }); - - it('allows bidding, settling, and transferring ETH correctly', async () => { - await nounsAuctionHouse.connect(bidderA).createBid(1, { value: RESERVE_PRICE }); - await setNextBlockTimestamp(Number(await blockTimestamp('latest')) + DURATION); - await nounsAuctionHouse.settleCurrentAndCreateNewAuction(); - - expect(await nounsToken.ownerOf(1)).to.equal(bidderA.address); - expect(await ethers.provider.getBalance(timelock.address)).to.equal(RESERVE_PRICE); - }); - - it('allows proposing, voting, queuing', async () => { - const description = 'Set nounsToken minter to address(1) and transfer treasury to address(2)'; - - // Action 1. Execute nounsToken.setMinter(address(1)) - targets.push(nounsToken.address); - values.push('0'); - signatures.push('setMinter(address)'); - callDatas.push(encodeParameters(['address'], [address(1)])); - - // Action 2. Execute transfer RESERVE_PRICE to address(2) - targets.push(address(2)); - values.push(String(RESERVE_PRICE)); - signatures.push(''); - callDatas.push('0x'); - - await gov.connect(bidderA).propose(targets, values, signatures, callDatas, description); - - proposalId = await gov.latestProposalIds(bidderA.address); - - // Wait for VOTING_DELAY - await advanceBlocks(VOTING_DELAY + 1); - - // cast vote for proposal - await gov.connect(bidderA).castVote(proposalId, 1); - - await advanceBlocks(VOTING_PERIOD); - - await gov.connect(bidderA).queue(proposalId); - - // Queued state - expect(await gov.state(proposalId)).to.equal(5); - }); - - it('executes proposal transactions correctly', async () => { - const { eta } = await gov.proposals(proposalId); - await setNextBlockTimestamp(eta.toNumber(), false); - await gov.execute(proposalId); - - // Successfully executed Action 1 - expect(await nounsToken.minter()).to.equal(address(1)); - - // Successfully executed Action 2 - expect(await ethers.provider.getBalance(address(2))).to.equal(RESERVE_PRICE); - }); - - it('does not allow NounsDAO to accept funds', async () => { - let error1; - - // NounsDAO does not accept value without calldata - try { - await bidderA.sendTransaction({ - to: gov.address, - value: 10, - }); - } catch (e) { - error1 = e; - } - - expect(error1); - - let error2; - - // NounsDAO does not accept value with calldata - try { - await bidderA.sendTransaction({ - data: '0xb6b55f250000000000000000000000000000000000000000000000000000000000000001', - to: gov.address, - value: 10, - }); - } catch (e) { - error2 = e; - } - - expect(error2); - }); - - it('allows NounsDAOExecutor to receive funds', async () => { - // test receive() - await bidderA.sendTransaction({ - to: timelock.address, - value: 10, - }); - - expect(await ethers.provider.getBalance(timelock.address)).to.equal(10); - - // test fallback() calls deposit(uint) which is not implemented - await bidderA.sendTransaction({ - data: '0xb6b55f250000000000000000000000000000000000000000000000000000000000000001', - to: timelock.address, - value: 10, - }); - - expect(await ethers.provider.getBalance(timelock.address)).to.equal(20); - }); -}); From 48d057d9baf8f91dba5c2166097447a1d0674d31 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:34:40 -0500 Subject: [PATCH 12/28] remove old descriptor V1 test --- .../nouns-contracts/test/descriptor.test.ts | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 packages/nouns-contracts/test/descriptor.test.ts diff --git a/packages/nouns-contracts/test/descriptor.test.ts b/packages/nouns-contracts/test/descriptor.test.ts deleted file mode 100644 index ace3d90b44..0000000000 --- a/packages/nouns-contracts/test/descriptor.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import chai from 'chai'; -import { solidity } from 'ethereum-waffle'; -import { NounsDescriptor } from '../typechain'; -import ImageData from '../files/image-data-v1.json'; -import { LongestPart } from './types'; -import { deployNounsDescriptor, populateDescriptor } from './utils'; -import { ethers } from 'hardhat'; -import { appendFileSync } from 'fs'; - -chai.use(solidity); -const { expect } = chai; - -describe('NounsDescriptor', () => { - let nounsDescriptor: NounsDescriptor; - let snapshotId: number; - - const part: LongestPart = { - length: 0, - index: 0, - }; - const longest: Record = { - bodies: part, - accessories: part, - heads: part, - glasses: part, - }; - - before(async () => { - nounsDescriptor = await deployNounsDescriptor(); - - for (const [l, layer] of Object.entries(ImageData.images)) { - for (const [i, item] of layer.entries()) { - if (item.data.length > longest[l].length) { - longest[l] = { - length: item.data.length, - index: i, - }; - } - } - } - - await populateDescriptor(nounsDescriptor); - }); - - beforeEach(async () => { - snapshotId = await ethers.provider.send('evm_snapshot', []); - }); - - afterEach(async () => { - await ethers.provider.send('evm_revert', [snapshotId]); - }); - - it('should generate valid token uri metadata when data uris are disabled', async () => { - const BASE_URI = 'https://api.nouns.wtf/metadata/'; - - await nounsDescriptor.setBaseURI(BASE_URI); - await nounsDescriptor.toggleDataURIEnabled(); - - const tokenUri = await nounsDescriptor.tokenURI(0, { - background: 0, - body: longest.bodies.index, - accessory: longest.accessories.index, - head: longest.heads.index, - glasses: longest.glasses.index, - }); - expect(tokenUri).to.equal(`${BASE_URI}0`); - }); - - // Unskip this test to validate the encoding of all parts. It ensures that no parts revert when building the token URI. - // This test also outputs a parts.html file, which can be visually inspected. - // Note that this test takes a long time to run. You must increase the mocha timeout to a large number. - it.skip('should generate valid token uri metadata for all supported parts when data uris are enabled', async () => { - console.log('Running... this may take a little while...'); - - const { bgcolors, images } = ImageData; - const { bodies, accessories, heads, glasses } = images; - const max = Math.max(bodies.length, accessories.length, heads.length, glasses.length); - for (let i = 0; i < max; i++) { - const tokenUri = await nounsDescriptor.tokenURI(i, { - background: Math.min(i, bgcolors.length - 1), - body: Math.min(i, bodies.length - 1), - accessory: Math.min(i, accessories.length - 1), - head: Math.min(i, heads.length - 1), - glasses: Math.min(i, glasses.length - 1), - }); - const { name, description, image } = JSON.parse( - Buffer.from(tokenUri.replace('data:application/json;base64,', ''), 'base64').toString( - 'ascii', - ), - ); - expect(name).to.equal(`Noun ${i}`); - expect(description).to.equal(`Noun ${i} is a member of the Nouns DAO`); - expect(image).to.not.be.undefined; - - appendFileSync( - 'parts.html', - Buffer.from(image.split(';base64,').pop(), 'base64').toString('ascii'), - ); - - if (i && i % Math.round(max / 10) === 0) { - console.log(`${Math.round((i / max) * 100)}% complete`); - } - } - }); -}); From ddaf63b11d71af33e65878bc007c0d386c9aded6 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:38:06 -0500 Subject: [PATCH 13/28] remove DAO V2 test folder we don't want gov tests for older versions anymore --- .../test/governance/{NounsDAO/V2 => }/castVote.test.ts | 6 +++--- .../test/governance/{NounsDAO/V2 => }/dynamicQuorum.test.ts | 4 ++-- .../test/governance/{NounsDAO/V2 => }/proxy.test.ts | 6 +++--- .../test/governance/{NounsDAO/V2 => }/quorumConfig.test.ts | 6 +++--- .../test/governance/{NounsDAO/V2 => }/voteRefund.test.ts | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) rename packages/nouns-contracts/test/governance/{NounsDAO/V2 => }/castVote.test.ts (97%) rename packages/nouns-contracts/test/governance/{NounsDAO/V2 => }/dynamicQuorum.test.ts (95%) rename packages/nouns-contracts/test/governance/{NounsDAO/V2 => }/proxy.test.ts (92%) rename packages/nouns-contracts/test/governance/{NounsDAO/V2 => }/quorumConfig.test.ts (98%) rename packages/nouns-contracts/test/governance/{NounsDAO/V2 => }/voteRefund.test.ts (99%) diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/castVote.test.ts b/packages/nouns-contracts/test/governance/castVote.test.ts similarity index 97% rename from packages/nouns-contracts/test/governance/NounsDAO/V2/castVote.test.ts rename to packages/nouns-contracts/test/governance/castVote.test.ts index 1dfcf1591a..e89a7eb22e 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/castVote.test.ts +++ b/packages/nouns-contracts/test/governance/castVote.test.ts @@ -14,15 +14,15 @@ import { propose, deployGovernorV3WithV3Proxy, populateDescriptorV2, -} from '../../../utils'; +} from '../utils'; -import { mineBlock } from '../../../utils'; +import { mineBlock } from '../utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, NounsDescriptorV2__factory as NounsDescriptorV2Factory, NounsDAOLogicV3, -} from '../../../../typechain'; +} from '../../typechain'; chai.use(solidity); const { expect } = chai; diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/dynamicQuorum.test.ts b/packages/nouns-contracts/test/governance/dynamicQuorum.test.ts similarity index 95% rename from packages/nouns-contracts/test/governance/NounsDAO/V2/dynamicQuorum.test.ts rename to packages/nouns-contracts/test/governance/dynamicQuorum.test.ts index b6c2a76212..eaa10a8239 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/dynamicQuorum.test.ts +++ b/packages/nouns-contracts/test/governance/dynamicQuorum.test.ts @@ -2,8 +2,8 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import chai from 'chai'; import { solidity } from 'ethereum-waffle'; import { parseUnits } from 'ethers/lib/utils'; -import { NounsDAOLogicV3 } from '../../../../typechain'; -import { deployGovernorV3, getSigners, TestSigners } from '../../../utils'; +import { NounsDAOLogicV3 } from '../../typechain'; +import { deployGovernorV3, getSigners, TestSigners } from '../utils'; chai.use(solidity); const { expect } = chai; diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/proxy.test.ts b/packages/nouns-contracts/test/governance/proxy.test.ts similarity index 92% rename from packages/nouns-contracts/test/governance/NounsDAO/V2/proxy.test.ts rename to packages/nouns-contracts/test/governance/proxy.test.ts index ba0c40d567..f96af20308 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/proxy.test.ts +++ b/packages/nouns-contracts/test/governance/proxy.test.ts @@ -8,15 +8,15 @@ import { blockNumber, populateDescriptorV2, deployGovernorV3WithV3Proxy, -} from '../../../utils'; +} from '../utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, NounsDescriptorV2__factory as NounsDescriptorV2Factory, NounsDAOLogicV3, -} from '../../../../typechain'; -import { MAX_QUORUM_VOTES_BPS, MIN_QUORUM_VOTES_BPS } from '../../../constants'; +} from '../../typechain'; +import { MAX_QUORUM_VOTES_BPS, MIN_QUORUM_VOTES_BPS } from '../constants'; chai.use(solidity); const { expect } = chai; diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/quorumConfig.test.ts b/packages/nouns-contracts/test/governance/quorumConfig.test.ts similarity index 98% rename from packages/nouns-contracts/test/governance/NounsDAO/V2/quorumConfig.test.ts rename to packages/nouns-contracts/test/governance/quorumConfig.test.ts index f76deb2031..9e8581d778 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/quorumConfig.test.ts +++ b/packages/nouns-contracts/test/governance/quorumConfig.test.ts @@ -11,15 +11,15 @@ import { advanceBlocks, populateDescriptorV2, deployGovernorV3AndSetImpl, -} from '../../../utils'; +} from '../utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { NounsToken, NounsDescriptorV2__factory as NounsDescriptorV2Factory, NounsDAOLogicV3, -} from '../../../../typechain'; +} from '../../typechain'; import { parseUnits } from 'ethers/lib/utils'; -import { DynamicQuorumParams } from '../../../types'; +import { DynamicQuorumParams } from '../types'; chai.use(solidity); const { expect } = chai; diff --git a/packages/nouns-contracts/test/governance/NounsDAO/V2/voteRefund.test.ts b/packages/nouns-contracts/test/governance/voteRefund.test.ts similarity index 99% rename from packages/nouns-contracts/test/governance/NounsDAO/V2/voteRefund.test.ts rename to packages/nouns-contracts/test/governance/voteRefund.test.ts index 03908d0b24..66f3adcaf5 100644 --- a/packages/nouns-contracts/test/governance/NounsDAO/V2/voteRefund.test.ts +++ b/packages/nouns-contracts/test/governance/voteRefund.test.ts @@ -9,8 +9,8 @@ import { NounsDescriptorV2__factory, NounsToken, Voter__factory, -} from '../../../../typechain'; -import { MaliciousVoter__factory } from '../../../../typechain/factories/contracts/test/MaliciousVoter__factory'; +} from '../../typechain'; +import { MaliciousVoter__factory } from '../../typechain/factories/contracts/test/MaliciousVoter__factory'; import { address, advanceBlocks, @@ -21,7 +21,7 @@ import { populateDescriptorV2, setNextBlockBaseFee, TestSigners, -} from '../../../utils'; +} from '../utils'; chai.use(solidity); const { expect } = chai; From 5464880329e5243b1fb05cfc69633fe82fd9cbdb Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 8 Nov 2023 16:55:00 -0500 Subject: [PATCH 14/28] remove old V1 > V2 upgrade test --- .../test/foundry/NounsDAOUpgradeToV2.t.sol | 277 ------------------ 1 file changed, 277 deletions(-) delete mode 100644 packages/nouns-contracts/test/foundry/NounsDAOUpgradeToV2.t.sol diff --git a/packages/nouns-contracts/test/foundry/NounsDAOUpgradeToV2.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOUpgradeToV2.t.sol deleted file mode 100644 index 028060e46f..0000000000 --- a/packages/nouns-contracts/test/foundry/NounsDAOUpgradeToV2.t.sol +++ /dev/null @@ -1,277 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.15; - -import 'forge-std/Test.sol'; -import { NounsDAOLogicSharedBaseTest } from './helpers/NounsDAOLogicSharedBase.t.sol'; -import { NounsDAOLogicV1 } from '../../contracts/governance/NounsDAOLogicV1.sol'; -import { NounsDAOLogicV2 } from '../../contracts/governance/NounsDAOLogicV2.sol'; -import { NounsDAOProxy } from '../../contracts/governance/NounsDAOProxy.sol'; -import { NounsToken } from '../../contracts/NounsToken.sol'; -import { NounsDAOExecutor } from '../../contracts/governance/NounsDAOExecutor.sol'; -import { NounsDAOStorageV1, NounsDAOStorageV2 } from '../../contracts/governance/NounsDAOInterfaces.sol'; - -contract NounsDAOUpgradeToV2 is NounsDAOLogicSharedBaseTest { - uint16 public constant MIN_QUORUM_BPS = 1000; - uint16 public constant MAX_QUORUM_BPS = 4000; - uint32 public constant COEFFICIENT = 1.5e6; - - address voter; - - function setUp() public override { - super.setUp(); - voter = utils.getNextUserAddress(); - } - - function daoVersion() internal pure override returns (uint256) { - return 1; - } - - function deployDAOProxy( - address timelock, - address nounsToken, - address vetoer - ) internal override returns (NounsDAOLogicV1) { - NounsDAOLogicV1 daoLogic = new NounsDAOLogicV1(); - - return - NounsDAOLogicV1( - payable( - new NounsDAOProxy( - timelock, - nounsToken, - vetoer, - address(timelock), - address(daoLogic), - votingPeriod, - votingDelay, - proposalThresholdBPS, - 1000 - ) - ) - ); - } - - function testV1Sanity_proposalFailsBelowQuorum() public { - mint(proposer, 20); - mint(voter, 1); - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - - // quorum should be 2 because total supply is 21 and quorum BPs is 10% - vote(voter, proposalId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Defeated); - } - - function testV1Sanity_proposalSucceedsWithForVotesMoreThanQuorum() public { - // quorum should be 2 because total supply is 21 and quorum BPs is 10% - // this puts voter at 3 tokens, above quorum - mint(proposer, 20); - mint(voter, 2); - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - - vote(voter, proposalId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Succeeded); - } - - function testUpgradeToV2() public { - NounsDAOLogicV2 daoLogicV2 = new NounsDAOLogicV2(); - mint(proposer, 2); - mint(voter, 2); - - proposeAndExecuteUpgradeToV2AndSetDQParams(address(daoLogicV2)); - address implementationPostUpgrade = NounsDAOProxy(payable(address(daoProxy))).implementation(); - - assertEq(implementationPostUpgrade, address(daoLogicV2)); - - NounsDAOLogicV2 daoV2 = NounsDAOLogicV2(payable(address(daoProxy))); - NounsDAOStorageV2.DynamicQuorumParams memory dqParams = daoV2.getDynamicQuorumParamsAt(block.number); - assertEq(dqParams.minQuorumVotesBPS, MIN_QUORUM_BPS); - assertEq(dqParams.maxQuorumVotesBPS, MAX_QUORUM_BPS); - assertEq(dqParams.quorumCoefficient, COEFFICIENT); - } - - function testUpgradeToV2_withV1PropInFlightWithAgainstVotesStillPasses() public { - NounsDAOLogicV2 daoLogicV2 = new NounsDAOLogicV2(); - mint(proposer, 2); - address forVoter = utils.getNextUserAddress(); - address againstVoter = utils.getNextUserAddress(); - mint(forVoter, 3); - mint(againstVoter, 2); - - uint256 v1PropId = propose(forVoter, address(0x1234), 100, '', ''); - - uint256 v2UpgradeProposalId = proposeUpgradeToV2AndSetDQParams(address(daoLogicV2)); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vote(forVoter, v2UpgradeProposalId, 1); - - vote(forVoter, v1PropId, 1); - vote(againstVoter, v1PropId, 0); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - - daoProxy.queue(v2UpgradeProposalId); - vm.warp(block.timestamp + timelock.delay() + 1); - daoProxy.execute(v2UpgradeProposalId); - - assertTrue(daoProxy.state(v1PropId) == NounsDAOStorageV1.ProposalState.Succeeded); - } - - function testUpgradeToV2_newV2Prop_failsBecauseOfDQ() public { - NounsDAOLogicV2 daoLogicV2 = new NounsDAOLogicV2(); - mint(proposer, 2); - mint(voter, 2); - address forVoter = utils.getNextUserAddress(); - address againstVoter = utils.getNextUserAddress(); - mint(forVoter, 3); - mint(againstVoter, 2); - - proposeAndExecuteUpgradeToV2AndSetDQParams(address(daoLogicV2)); - - uint256 newPropId = propose(forVoter, address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vote(forVoter, newPropId, 1); - vote(againstVoter, newPropId, 0); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - - assertTrue(daoProxy.state(newPropId) == NounsDAOStorageV1.ProposalState.Defeated); - } - - function testUpgradeToV2_newV2Prop_succeedsWithEnoughForVotes() public { - NounsDAOLogicV2 daoLogicV2 = new NounsDAOLogicV2(); - mint(proposer, 2); - mint(voter, 2); - address forVoter = utils.getNextUserAddress(); - address againstVoter = utils.getNextUserAddress(); - mint(forVoter, 4); - mint(againstVoter, 2); - - proposeAndExecuteUpgradeToV2AndSetDQParams(address(daoLogicV2)); - - uint256 newPropId = propose(forVoter, address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vote(forVoter, newPropId, 1); - vote(againstVoter, newPropId, 0); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - - assertTrue(daoProxy.state(newPropId) == NounsDAOStorageV1.ProposalState.Succeeded); - } - - function testUpgradeToV2_dqParamsChange_newPropMustPassNewDQ() public { - NounsDAOLogicV2 daoLogicV2 = new NounsDAOLogicV2(); - mint(proposer, 2); - mint(voter, 2); - proposeAndExecuteUpgradeToV2AndSetDQParams(address(daoLogicV2)); - proposeAndExecuteSetDQParams(MIN_QUORUM_BPS, MAX_QUORUM_BPS, 2.5e6); - - address forVoter = utils.getNextUserAddress(); - address againstVoter = utils.getNextUserAddress(); - mint(forVoter, 2); - mint(againstVoter, 1); - - uint256 newPropId = propose(forVoter, address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vote(forVoter, newPropId, 1); - vote(againstVoter, newPropId, 0); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - - assertTrue(daoProxy.state(newPropId) == NounsDAOStorageV1.ProposalState.Defeated); - } - - function testUpgradeToV2_dqParamsChange_doesNotAffectExistingProposal() public { - NounsDAOLogicV2 daoLogicV2 = new NounsDAOLogicV2(); - mint(proposer, 2); - mint(voter, 2); - proposeAndExecuteUpgradeToV2AndSetDQParams(address(daoLogicV2)); - - address forVoter = utils.getNextUserAddress(); - address againstVoter = utils.getNextUserAddress(); - mint(forVoter, 2); - mint(againstVoter, 1); - - uint256 dqParamsPropId = proposeSetDQParams(MIN_QUORUM_BPS, MAX_QUORUM_BPS, 2.5e6); - uint256 newPropId = propose(forVoter, address(0x1234), 100, '', ''); - - vm.roll(block.number + daoProxy.votingDelay() + 1); - vote(forVoter, newPropId, 1); - vote(againstVoter, newPropId, 0); - vote(forVoter, dqParamsPropId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - - daoProxy.queue(dqParamsPropId); - vm.warp(block.timestamp + timelock.delay() + 1); - daoProxy.execute(dqParamsPropId); - vm.roll(block.number + 1); - - assertTrue(daoProxy.state(newPropId) == NounsDAOStorageV1.ProposalState.Succeeded); - } - - function proposeAndExecuteUpgradeToV2AndSetDQParams(address daoLogicV2) internal returns (uint256 proposalId) { - proposalId = proposeUpgradeToV2AndSetDQParams(daoLogicV2); - executeProposal(proposalId); - } - - function proposeUpgradeToV2AndSetDQParams(address daoLogicV2) internal returns (uint256 proposalId) { - address[] memory targets = new address[](2); - targets[0] = address(daoProxy); - targets[1] = address(daoProxy); - - uint256[] memory values = new uint256[](2); - values[0] = 0; - values[1] = 0; - - string[] memory signatures = new string[](2); - signatures[0] = '_setImplementation(address)'; - signatures[1] = '_setDynamicQuorumParams(uint16,uint16,uint32)'; - - bytes[] memory calldatas = new bytes[](2); - calldatas[0] = abi.encode(daoLogicV2); - calldatas[1] = abi.encode(MIN_QUORUM_BPS, MAX_QUORUM_BPS, COEFFICIENT); - - vm.prank(proposer); - proposalId = daoProxy.propose(targets, values, signatures, calldatas, 'upgrade to DAO V2'); - } - - function proposeAndExecuteSetDQParams( - uint16 minQuorumBPs, - uint16 maxQuorumBPs, - uint32 coefficient - ) internal returns (uint256 proposalId) { - proposalId = proposeSetDQParams(minQuorumBPs, maxQuorumBPs, coefficient); - executeProposal(proposalId); - } - - function proposeSetDQParams( - uint16 minQuorumBPs, - uint16 maxQuorumBPs, - uint32 coefficient - ) internal returns (uint256 proposalId) { - address[] memory targets = new address[](1); - targets[0] = address(daoProxy); - - uint256[] memory values = new uint256[](1); - values[0] = 0; - - string[] memory signatures = new string[](1); - signatures[0] = '_setDynamicQuorumParams(uint16,uint16,uint32)'; - - bytes[] memory calldatas = new bytes[](1); - calldatas[0] = abi.encode(minQuorumBPs, maxQuorumBPs, coefficient); - - vm.prank(proposer); - proposalId = daoProxy.propose(targets, values, signatures, calldatas, 'upgrade to DAO V2'); - } - - function executeProposal(uint256 proposalId) internal { - vm.roll(block.number + daoProxy.votingDelay() + 1); - vote(voter, proposalId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - daoProxy.queue(proposalId); - vm.warp(block.timestamp + timelock.delay() + 1); - daoProxy.execute(proposalId); - vm.roll(block.number + 1); - } -} From 989ab7858abf2f9bf540b7f8fb8658696b7eb722 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 13:59:38 -0500 Subject: [PATCH 15/28] remove dao V1 tests and point V2 tests to V3 --- ....t.sol => NounsDAOLogicStateAndVeto.t.sol} | 193 +++++------------- 1 file changed, 53 insertions(+), 140 deletions(-) rename packages/nouns-contracts/test/foundry/{NounsDAOLogicV1V2Shared.t.sol => NounsDAOLogicStateAndVeto.t.sol} (79%) diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV1V2Shared.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol similarity index 79% rename from packages/nouns-contracts/test/foundry/NounsDAOLogicV1V2Shared.t.sol rename to packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol index 84155ff681..4ee6c17833 100644 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV1V2Shared.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol @@ -15,7 +15,7 @@ import { IProxyRegistry } from '../../contracts/external/opensea/IProxyRegistry. import { NounsDAOExecutor } from '../../contracts/governance/NounsDAOExecutor.sol'; import { NounsDAOLogicSharedBaseTest } from './helpers/NounsDAOLogicSharedBase.t.sol'; -abstract contract NounsDAOLogicV1V2StateTest is NounsDAOLogicSharedBaseTest { +abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { function setUp() public override { super.setUp(); @@ -180,7 +180,7 @@ abstract contract NounsDAOLogicV1V2StateTest is NounsDAOLogicSharedBaseTest { } } -contract NounsDAOLogicV1ForkStateTest is NounsDAOLogicV1V2StateTest { +contract NounsDAOLogicV1ForkStateTest is NounsDAOLogicStateBaseTest { function daoVersion() internal pure override returns (uint256) { return 1; } @@ -194,73 +194,7 @@ contract NounsDAOLogicV1ForkStateTest is NounsDAOLogicV1V2StateTest { } } -contract NounsDAOLogicV1StateTest is NounsDAOLogicV1V2StateTest { - function daoVersion() internal pure override returns (uint256) { - return 1; - } - - function deployDAOProxy( - address timelock, - address nounsToken, - address vetoer - ) internal override returns (NounsDAOLogicV1) { - NounsDAOLogicV1 daoLogic = new NounsDAOLogicV1(); - - return - NounsDAOLogicV1( - payable( - new NounsDAOProxy( - timelock, - nounsToken, - vetoer, - admin, - address(daoLogic), - votingPeriod, - votingDelay, - proposalThresholdBPS, - 1000 - ) - ) - ); - } -} - -contract NounsDAOLogicV2StateTest is NounsDAOLogicV1V2StateTest { - function daoVersion() internal pure override returns (uint256) { - return 2; - } - - function deployDAOProxy( - address timelock, - address nounsToken, - address vetoer - ) internal override returns (NounsDAOLogicV1) { - NounsDAOLogicV2 daoLogic = new NounsDAOLogicV2(); - - return - NounsDAOLogicV1( - payable( - new NounsDAOProxyV2( - timelock, - nounsToken, - vetoer, - admin, - address(daoLogic), - votingPeriod, - votingDelay, - proposalThresholdBPS, - NounsDAOStorageV2.DynamicQuorumParams({ - minQuorumVotesBPS: 200, - maxQuorumVotesBPS: 2000, - quorumCoefficient: 10000 - }) - ) - ) - ); - } -} - -contract NounsDAOLogicV3StateTest is NounsDAOLogicV1V2StateTest { +contract NounsDAOLogicV3StateTest is NounsDAOLogicStateBaseTest { function deployDAOProxy( address timelock, address nounsToken, @@ -274,7 +208,7 @@ contract NounsDAOLogicV3StateTest is NounsDAOLogicV1V2StateTest { } } -abstract contract NounsDAOLogicV1V2VetoingTest is NounsDAOLogicSharedBaseTest { +abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { function setUp() public override { super.setUp(); @@ -329,6 +263,8 @@ abstract contract NounsDAOLogicV1V2VetoingTest is NounsDAOLogicSharedBaseTest { function test_veto_worksForPropStatePending() public { uint256 proposalId = propose(address(0x1234), 100, '', ''); + // Need to roll one block because in V3 on the proposal creation block the state is Updatable + vm.roll(block.number + 1); assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Pending); vm.prank(vetoer); @@ -453,56 +389,21 @@ abstract contract NounsDAOLogicV1V2VetoingTest is NounsDAOLogicSharedBaseTest { } } -contract NounsDAOLogicV1VetoingTest is NounsDAOLogicV1V2VetoingTest { - function test_setVetoer_revertsForNonVetoer() public { - address newVetoer = utils.getNextUserAddress(); +contract NounsDAOLogicV3VetoingTest is NounsDAOLogicVetoingBaseTest { + event NewPendingVetoer(address oldPendingVetoer, address newPendingVetoer); + event NewVetoer(address oldVetoer, address newVetoer); - vm.expectRevert('NounsDAO::_setVetoer: vetoer only'); - daoProxy._setVetoer(newVetoer); - } + function test_veto_worksForPropStateUpdatable() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + NounsDAOLogicV3 daoAsV3 = NounsDAOLogicV3(payable(address(daoProxy))); - function test_setVetoer_worksForVetoer() public { - address newVetoer = utils.getNextUserAddress(); + assertTrue(daoAsV3.state(proposalId) == NounsDAOStorageV3.ProposalState.Updatable); vm.prank(vetoer); - daoProxy._setVetoer(newVetoer); - - assertEq(daoProxy.vetoer(), newVetoer); - } + daoAsV3.veto(proposalId); - function daoVersion() internal pure override returns (uint256) { - return 1; - } - - function deployDAOProxy( - address timelock, - address nounsToken, - address vetoer - ) internal override returns (NounsDAOLogicV1) { - NounsDAOLogicV1 daoLogic = new NounsDAOLogicV1(); - - return - NounsDAOLogicV1( - payable( - new NounsDAOProxy( - timelock, - nounsToken, - vetoer, - admin, - address(daoLogic), - votingPeriod, - votingDelay, - proposalThresholdBPS, - 1000 - ) - ) - ); + assertTrue(daoAsV3.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } -} - -contract NounsDAOLogicV2VetoingTest is NounsDAOLogicV1V2VetoingTest { - event NewPendingVetoer(address oldPendingVetoer, address newPendingVetoer); - event NewVetoer(address oldVetoer, address newVetoer); function test_setPendingVetoer_failsIfNotCurrentVetoer() public { vm.expectRevert(NounsDAOLogicV2.VetoerOnly.selector); @@ -572,36 +473,48 @@ contract NounsDAOLogicV2VetoingTest is NounsDAOLogicV1V2VetoingTest { assertEq(daoProxyAsV2().pendingVetoer(), address(0)); } - function daoVersion() internal pure override returns (uint256) { - return 2; - } - function deployDAOProxy( address timelock, address nounsToken, address vetoer ) internal override returns (NounsDAOLogicV1) { - NounsDAOLogicV2 daoLogic = new NounsDAOLogicV2(); - - return - NounsDAOLogicV1( - payable( - new NounsDAOProxyV2( - timelock, - nounsToken, - vetoer, - admin, - address(daoLogic), - votingPeriod, - votingDelay, - proposalThresholdBPS, - NounsDAOStorageV2.DynamicQuorumParams({ - minQuorumVotesBPS: 200, - maxQuorumVotesBPS: 2000, - quorumCoefficient: 10000 - }) - ) - ) - ); + return _createDAOV3Proxy(timelock, nounsToken, vetoer); + } + + function daoVersion() internal pure override returns (uint256) { + return 3; } + + // function daoVersion() internal pure override returns (uint256) { + // return 2; + // } + + // function deployDAOProxy( + // address timelock, + // address nounsToken, + // address vetoer + // ) internal override returns (NounsDAOLogicV1) { + // NounsDAOLogicV2 daoLogic = new NounsDAOLogicV2(); + + // return + // NounsDAOLogicV1( + // payable( + // new NounsDAOProxyV2( + // timelock, + // nounsToken, + // vetoer, + // admin, + // address(daoLogic), + // votingPeriod, + // votingDelay, + // proposalThresholdBPS, + // NounsDAOStorageV2.DynamicQuorumParams({ + // minQuorumVotesBPS: 200, + // maxQuorumVotesBPS: 2000, + // quorumCoefficient: 10000 + // }) + // ) + // ) + // ); + // } } From 2906230f77c583c46a419fd1cd2a197ce2479544 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 14:30:03 -0500 Subject: [PATCH 16/28] remove dao V1 from scripts --- .../script/DeployDAOV3DataContractsBase.s.sol | 11 +++++---- .../script/ProposeDAOV3UpgradeMainnet.s.sol | 16 +++++++++---- .../script/ProposeDAOV3UpgradeTestnet.s.sol | 23 ++++++++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/nouns-contracts/script/DeployDAOV3DataContractsBase.s.sol b/packages/nouns-contracts/script/DeployDAOV3DataContractsBase.s.sol index f56d03f8fa..a360e5b5bb 100644 --- a/packages/nouns-contracts/script/DeployDAOV3DataContractsBase.s.sol +++ b/packages/nouns-contracts/script/DeployDAOV3DataContractsBase.s.sol @@ -2,18 +2,21 @@ pragma solidity ^0.8.15; import 'forge-std/Script.sol'; -import { NounsDAOLogicV1 } from '../contracts/governance/NounsDAOLogicV1.sol'; import { NounsDAOData } from '../contracts/governance/data/NounsDAOData.sol'; import { NounsDAODataProxy } from '../contracts/governance/data/NounsDAODataProxy.sol'; +interface NounsDAO { + function nouns() external view returns (address); +} + contract DeployDAOV3DataContractsBase is Script { uint256 public constant CREATE_CANDIDATE_COST = 0.01 ether; - NounsDAOLogicV1 public immutable daoProxy; + NounsDAO public immutable daoProxy; address public immutable timelockV2Proxy; constructor(address _daoProxy, address _timelockV2Proxy) { - daoProxy = NounsDAOLogicV1(payable(_daoProxy)); + daoProxy = NounsDAO(_daoProxy); timelockV2Proxy = _timelockV2Proxy; } @@ -22,7 +25,7 @@ contract DeployDAOV3DataContractsBase is Script { vm.startBroadcast(deployerKey); - NounsDAOData dataLogic = new NounsDAOData(address(daoProxy.nouns()), address(daoProxy)); + NounsDAOData dataLogic = new NounsDAOData(daoProxy.nouns(), address(daoProxy)); bytes memory initCallData = abi.encodeWithSignature( 'initialize(address,uint256,uint256,address)', diff --git a/packages/nouns-contracts/script/ProposeDAOV3UpgradeMainnet.s.sol b/packages/nouns-contracts/script/ProposeDAOV3UpgradeMainnet.s.sol index 70ee27b14f..0cd4a28e06 100644 --- a/packages/nouns-contracts/script/ProposeDAOV3UpgradeMainnet.s.sol +++ b/packages/nouns-contracts/script/ProposeDAOV3UpgradeMainnet.s.sol @@ -2,14 +2,22 @@ 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'; +interface NounsDAO { + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); +} + contract ProposeDAOV3UpgradeMainnet is Script { - NounsDAOLogicV1 public constant NOUNS_DAO_PROXY_MAINNET = - NounsDAOLogicV1(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d); + NounsDAO public constant NOUNS_DAO_PROXY_MAINNET = NounsDAO(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d); address public constant NOUNS_TIMELOCK_V1_MAINNET = 0x0BC3807Ec262cB779b38D65b38158acC3bfedE10; uint256 public constant ETH_TO_SEND_TO_NEW_TIMELOCK = 2500 ether; @@ -60,7 +68,7 @@ contract ProposeDAOV3UpgradeMainnet is Script { } function propose( - NounsDAOLogicV1 daoProxy, + NounsDAO daoProxy, address daoV3Implementation, address timelockV2, uint256 ethToSendToNewTimelock, diff --git a/packages/nouns-contracts/script/ProposeDAOV3UpgradeTestnet.s.sol b/packages/nouns-contracts/script/ProposeDAOV3UpgradeTestnet.s.sol index e8948c9afb..13d7fb45e8 100644 --- a/packages/nouns-contracts/script/ProposeDAOV3UpgradeTestnet.s.sol +++ b/packages/nouns-contracts/script/ProposeDAOV3UpgradeTestnet.s.sol @@ -2,22 +2,31 @@ 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'; +interface NounsDAO { + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); +} + abstract contract ProposeDAOV3UpgradeTestnet is Script { uint256 public constant ETH_TO_SEND_TO_NEW_TIMELOCK = 0.001 ether; uint256 public constant FORK_PERIOD = 1 hours; uint256 public constant FORK_THRESHOLD_BPS = 2000; - NounsDAOLogicV1 public immutable daoProxyContract; + NounsDAO public immutable daoProxyContract; address public immutable timelockV1; address public immutable auctionHouseProxy; address public immutable stETH; constructor( - NounsDAOLogicV1 daoProxy_, + NounsDAO daoProxy_, address timelockV1_, address auctionHouseProxy_, address stETH_ @@ -60,7 +69,7 @@ abstract contract ProposeDAOV3UpgradeTestnet is Script { } function propose( - NounsDAOLogicV1 daoProxy, + NounsDAO daoProxy, address daoV3Implementation, address timelockV2, uint256 ethToSendToNewTimelock, @@ -139,8 +148,7 @@ abstract contract ProposeDAOV3UpgradeTestnet is Script { } contract ProposeDAOV3UpgradeGoerli is ProposeDAOV3UpgradeTestnet { - NounsDAOLogicV1 public constant NOUNS_DAO_PROXY_GOERLI = - NounsDAOLogicV1(0x9e6D4B42b8Dc567AC4aeCAB369Eb9a3156dF095C); + NounsDAO public constant NOUNS_DAO_PROXY_GOERLI = NounsDAO(0x9e6D4B42b8Dc567AC4aeCAB369Eb9a3156dF095C); address public constant NOUNS_TIMELOCK_V1_GOERLI = 0xADa0F1A73D1df49477fa41C7F8476F9eA5aB115f; address public constant AUCTION_HOUSE_PROXY_GOERLI = 0x17e8512851Db9F04164Aa54A6e62f368acCF9D0c; address public constant STETH_GOERLI = 0x1643E812aE58766192Cf7D2Cf9567dF2C37e9B7F; @@ -156,8 +164,7 @@ contract ProposeDAOV3UpgradeGoerli is ProposeDAOV3UpgradeTestnet { } contract ProposeDAOV3UpgradeSepolia is ProposeDAOV3UpgradeTestnet { - NounsDAOLogicV1 public constant NOUNS_DAO_PROXY_SEPOLIA = - NounsDAOLogicV1(0x35d2670d7C8931AACdd37C89Ddcb0638c3c44A57); + NounsDAO public constant NOUNS_DAO_PROXY_SEPOLIA = NounsDAO(0x35d2670d7C8931AACdd37C89Ddcb0638c3c44A57); address public constant NOUNS_TIMELOCK_V1_SEPOLIA = 0x332db58b51393f3a6b28d4DD8964234967e1aD33; address public constant AUCTION_HOUSE_PROXY_SEPOLIA = 0x488609b7113FCf3B761A05956300d605E8f6BcAf; address public constant STETH_SEPOLIA = 0xf16e3ab44cC450fCbe5E890322Ee715f3f7eAC29; // ERC20Mock From dceba182160f304bd44ebdddc9e9743e9bd221ed Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 14:43:12 -0500 Subject: [PATCH 17/28] remove unused test contract --- .../contracts/test/NounsDAOImmutable.sol | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 packages/nouns-contracts/contracts/test/NounsDAOImmutable.sol diff --git a/packages/nouns-contracts/contracts/test/NounsDAOImmutable.sol b/packages/nouns-contracts/contracts/test/NounsDAOImmutable.sol deleted file mode 100644 index d0a9491d64..0000000000 --- a/packages/nouns-contracts/contracts/test/NounsDAOImmutable.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.19; - -import '../governance/NounsDAOLogicV1.sol'; - -contract NounsDAOImmutable is NounsDAOLogicV1 { - constructor( - address timelock_, - address nouns_, - address admin_, - address vetoer_, - uint256 votingPeriod_, - uint256 votingDelay_, - uint256 proposalThresholdBPS_, - uint256 quorumVotesBPS_ - ) { - admin = msg.sender; - initialize(timelock_, nouns_, vetoer_, votingPeriod_, votingDelay_, proposalThresholdBPS_, quorumVotesBPS_); - - admin = admin_; - } - - function initialize( - address timelock_, - address nouns_, - address vetoer_, - uint256 votingPeriod_, - uint256 votingDelay_, - uint256 proposalThresholdBPS_, - uint256 quorumVotesBPS_ - ) public override { - require(msg.sender == admin, 'NounsDAO::initialize: admin only'); - require(address(timelock) == address(0), 'NounsDAO::initialize: can only initialize once'); - - timelock = INounsDAOExecutor(timelock_); - nouns = NounsTokenLike(nouns_); - vetoer = vetoer_; - votingPeriod = votingPeriod_; - votingDelay = votingDelay_; - proposalThresholdBPS = proposalThresholdBPS_; - quorumVotesBPS = quorumVotesBPS_; - } -} From 70d243a507c64edfa2a7163c215def485e5884e1 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 14:43:31 -0500 Subject: [PATCH 18/28] remove DAO V1 from a script import --- .../script/DeployDAOV3NewContractsBase.s.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/nouns-contracts/script/DeployDAOV3NewContractsBase.s.sol b/packages/nouns-contracts/script/DeployDAOV3NewContractsBase.s.sol index 2fcd1d380a..738428a1e3 100644 --- a/packages/nouns-contracts/script/DeployDAOV3NewContractsBase.s.sol +++ b/packages/nouns-contracts/script/DeployDAOV3NewContractsBase.s.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.15; import 'forge-std/Script.sol'; import { NounsDAOExecutorV2 } from '../contracts/governance/NounsDAOExecutorV2.sol'; import { NounsDAOExecutorV2Test } from '../contracts/test/NounsDAOExecutorHarness.sol'; -import { NounsDAOLogicV1 } from '../contracts/governance/NounsDAOLogicV1.sol'; import { NounsDAOLogicV3 } from '../contracts/governance/NounsDAOLogicV3.sol'; import { NounsDAOExecutorProxy } from '../contracts/governance/NounsDAOExecutorProxy.sol'; import { INounsDAOExecutor } from '../contracts/governance/NounsDAOInterfaces.sol'; @@ -15,6 +14,10 @@ import { NounsDAOLogicV1Fork } from '../contracts/governance/fork/newdao/governa import { ForkDAODeployer } from '../contracts/governance/fork/ForkDAODeployer.sol'; import { ERC20Transferer } from '../contracts/utils/ERC20Transferer.sol'; +interface NounsDAO { + function nouns() external view returns (address); +} + contract DeployDAOV3NewContractsBase is Script { uint256 public constant DELAYED_GOV_DURATION = 30 days; uint256 public immutable forkDAOVotingPeriod; @@ -22,7 +25,7 @@ contract DeployDAOV3NewContractsBase is Script { uint256 public constant FORK_DAO_PROPOSAL_THRESHOLD_BPS = 25; // 0.25% uint256 public constant FORK_DAO_QUORUM_VOTES_BPS = 1000; // 10% - NounsDAOLogicV1 public immutable daoProxy; + NounsDAO public immutable daoProxy; INounsDAOExecutor public immutable timelockV1; bool public immutable deployTimelockV2Harness; // should be true only for testnets @@ -33,7 +36,7 @@ contract DeployDAOV3NewContractsBase is Script { uint256 _forkDAOVotingPeriod, uint256 _forkDAOVotingDelay ) { - daoProxy = NounsDAOLogicV1(payable(_daoProxy)); + daoProxy = NounsDAO(_daoProxy); timelockV1 = INounsDAOExecutor(_timelockV1); deployTimelockV2Harness = _deployTimelockV2Harness; forkDAOVotingPeriod = _forkDAOVotingPeriod; From 325aa254c8509460a61b4d41cec5da7a9b6fe0a9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 14:46:06 -0500 Subject: [PATCH 19/28] remove DAO V1 import from tests --- .../foundry/NounsDAOLogicStateAndVeto.t.sol | 103 ++++++------------ .../governance/InflationHandling.t.sol | 8 +- .../NounsDAOLogicV3GasSnapshot.t.sol | 12 +- .../test/foundry/helpers/DeployUtils.sol | 16 +-- .../test/foundry/helpers/DeployUtilsV3.sol | 8 +- .../test/foundry/helpers/INounsDAOShared.sol | 54 +++++++++ .../helpers/NounsDAOLogicSharedBase.t.sol | 14 +-- 7 files changed, 118 insertions(+), 97 deletions(-) create mode 100644 packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol index 4ee6c17833..6a7d155a9b 100644 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.15; import 'forge-std/Test.sol'; -import { NounsDAOLogicV1 } from '../../contracts/governance/NounsDAOLogicV1.sol'; +import { INounsDAOShared } from './helpers/INounsDAOShared.sol'; import { NounsDAOLogicV2 } from '../../contracts/governance/NounsDAOLogicV2.sol'; import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; import { NounsDAOProxy } from '../../contracts/governance/NounsDAOProxy.sol'; import { NounsDAOProxyV2 } from '../../contracts/governance/NounsDAOProxyV2.sol'; -import { NounsDAOStorageV1, NounsDAOStorageV2, NounsDAOStorageV3 } from '../../contracts/governance/NounsDAOInterfaces.sol'; +import { NounsDAOStorageV2, NounsDAOStorageV3 } from '../../contracts/governance/NounsDAOInterfaces.sol'; import { NounsDescriptorV2 } from '../../contracts/NounsDescriptorV2.sol'; import { NounsToken } from '../../contracts/NounsToken.sol'; import { NounsSeeder } from '../../contracts/NounsSeeder.sol'; @@ -35,7 +35,7 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { uint256 state = uint256(NounsDAOLogicV3(payable(address(daoProxy))).state(proposalId)); if (daoVersion() < 3) { - assertEq(state, uint256(NounsDAOStorageV1.ProposalState.Pending)); + assertEq(state, uint256(NounsDAOStorageV3.ProposalState.Pending)); } else { assertEq(state, uint256(NounsDAOStorageV3.ProposalState.Updatable)); } @@ -44,7 +44,7 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { function testActiveGivenProposalPastVotingDelay() public { uint256 proposalId = propose(address(0x1234), 100, '', ''); vm.roll(block.number + daoProxy.votingDelay() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Active); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Active); } function testCanceledGivenCanceledProposal() public { @@ -52,14 +52,14 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { vm.prank(proposer); daoProxy.cancel(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Canceled); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Canceled); } function testDefeatedByRunningOutOfTime() public { uint256 proposalId = propose(address(0x1234), 100, '', ''); vm.roll(block.number + daoProxy.votingDelay() + daoProxy.votingPeriod() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Defeated); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Defeated); } function testDefeatedByVotingAgainst() public { @@ -74,7 +74,7 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { vote(againstVoter, proposalId, 0); endVotingPeriod(); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Defeated); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Defeated); } function testSucceeded() public { @@ -89,14 +89,14 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { vote(againstVoter, proposalId, 0); endVotingPeriod(); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Succeeded); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Succeeded); } function testQueueRevertsGivenDefeatedProposal() public { uint256 proposalId = propose(address(0x1234), 100, '', ''); vm.roll(block.number + daoProxy.votingDelay() + daoProxy.votingPeriod() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Defeated); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Defeated); vm.expectRevert('NounsDAO::queue: proposal can only be queued if it is succeeded'); daoProxy.queue(proposalId); @@ -107,7 +107,7 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { vm.prank(proposer); daoProxy.cancel(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Canceled); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Canceled); vm.expectRevert('NounsDAO::queue: proposal can only be queued if it is succeeded'); daoProxy.queue(proposalId); @@ -128,7 +128,7 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { // anyone can queue daoProxy.queue(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Queued); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Queued); } function testExpired() public { @@ -145,7 +145,7 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { daoProxy.queue(proposalId); vm.warp(block.timestamp + timelock.delay() + timelock.GRACE_PERIOD() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Expired); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Expired); } function testExecutedOnlyAfterQueued() public { @@ -173,10 +173,10 @@ abstract contract NounsDAOLogicStateBaseTest is NounsDAOLogicSharedBaseTest { vm.deal(address(timelock), 100); daoProxy.execute(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Executed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Executed); vm.warp(block.timestamp + timelock.delay() + timelock.GRACE_PERIOD() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Executed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Executed); } } @@ -189,8 +189,8 @@ contract NounsDAOLogicV1ForkStateTest is NounsDAOLogicStateBaseTest { address, address, address - ) internal override returns (NounsDAOLogicV1) { - return deployForkDAOProxy(); + ) internal override returns (INounsDAOShared) { + return INounsDAOShared(address(deployForkDAOProxy())); } } @@ -199,7 +199,7 @@ contract NounsDAOLogicV3StateTest is NounsDAOLogicStateBaseTest { address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { return _createDAOV3Proxy(timelock, nounsToken, vetoer); } @@ -265,35 +265,35 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { uint256 proposalId = propose(address(0x1234), 100, '', ''); // Need to roll one block because in V3 on the proposal creation block the state is Updatable vm.roll(block.number + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Pending); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Pending); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } function test_veto_worksForPropStateActive() public { uint256 proposalId = propose(address(0x1234), 100, '', ''); vm.roll(block.number + daoProxy.votingDelay() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Active); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Active); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } function test_veto_worksForPropStateCanceled() public { uint256 proposalId = propose(address(0x1234), 100, '', ''); vm.prank(proposer); daoProxy.cancel(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Canceled); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Canceled); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } function test_veto_worksForPropStateDefeated() public { @@ -302,12 +302,12 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { vm.prank(proposer); daoProxy.castVote(proposalId, 0); vm.roll(block.number + daoProxy.votingPeriod() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Defeated); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Defeated); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } function test_veto_worksForPropStateSucceeded() public { @@ -316,12 +316,12 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { vm.prank(proposer); daoProxy.castVote(proposalId, 1); vm.roll(block.number + daoProxy.votingPeriod() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Succeeded); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Succeeded); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } function test_veto_worksForPropStateQueued() public { @@ -331,12 +331,12 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { daoProxy.castVote(proposalId, 1); vm.roll(block.number + daoProxy.votingPeriod() + 1); daoProxy.queue(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Queued); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Queued); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } function test_veto_worksForPropStateExpired() public { @@ -347,12 +347,12 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { vm.roll(block.number + daoProxy.votingPeriod() + 1); daoProxy.queue(proposalId); vm.warp(block.timestamp + timelock.delay() + timelock.GRACE_PERIOD() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Expired); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Expired); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } function test_veto_revertsForPropStateExecuted() public { @@ -365,7 +365,7 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { daoProxy.queue(proposalId); vm.warp(block.timestamp + timelock.delay() + 1); daoProxy.execute(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Executed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Executed); vm.prank(vetoer); if (daoVersion() == 1) { @@ -380,12 +380,12 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { uint256 proposalId = propose(address(0x1234), 100, '', ''); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); vm.prank(vetoer); daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV1.ProposalState.Vetoed); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } } @@ -477,44 +477,11 @@ contract NounsDAOLogicV3VetoingTest is NounsDAOLogicVetoingBaseTest { address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { return _createDAOV3Proxy(timelock, nounsToken, vetoer); } function daoVersion() internal pure override returns (uint256) { return 3; } - - // function daoVersion() internal pure override returns (uint256) { - // return 2; - // } - - // function deployDAOProxy( - // address timelock, - // address nounsToken, - // address vetoer - // ) internal override returns (NounsDAOLogicV1) { - // NounsDAOLogicV2 daoLogic = new NounsDAOLogicV2(); - - // return - // NounsDAOLogicV1( - // payable( - // new NounsDAOProxyV2( - // timelock, - // nounsToken, - // vetoer, - // admin, - // address(daoLogic), - // votingPeriod, - // votingDelay, - // proposalThresholdBPS, - // NounsDAOStorageV2.DynamicQuorumParams({ - // minQuorumVotesBPS: 200, - // maxQuorumVotesBPS: 2000, - // quorumCoefficient: 10000 - // }) - // ) - // ) - // ); - // } } diff --git a/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol b/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol index 60208b7007..0a99499924 100644 --- a/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.15; import 'forge-std/Test.sol'; +import { INounsDAOShared } from '../helpers/INounsDAOShared.sol'; import { NounsDAOLogicSharedBaseTest } from '../helpers/NounsDAOLogicSharedBase.t.sol'; -import { NounsDAOLogicV1 } from '../../../contracts/governance/NounsDAOLogicV1.sol'; import { NounsDAOLogicV2 } from '../../../contracts/governance/NounsDAOLogicV2.sol'; import { NounsDAOProxyV2 } from '../../../contracts/governance/NounsDAOProxyV2.sol'; import { NounsDAOStorageV1, NounsDAOStorageV2 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; @@ -25,12 +25,12 @@ abstract contract NounsDAOLogicV2InflationHandlingTest is NounsDAOLogicSharedBas address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { NounsDAOLogicV2 daoLogic = new NounsDAOLogicV2(); return - NounsDAOLogicV1( - payable( + INounsDAOShared( + address( new NounsDAOProxyV2( timelock, nounsToken, diff --git a/packages/nouns-contracts/test/foundry/governance/NounsDAOLogicV3GasSnapshot.t.sol b/packages/nouns-contracts/test/foundry/governance/NounsDAOLogicV3GasSnapshot.t.sol index 31096ed606..fd756d67ee 100644 --- a/packages/nouns-contracts/test/foundry/governance/NounsDAOLogicV3GasSnapshot.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/NounsDAOLogicV3GasSnapshot.t.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.15; import 'forge-std/Test.sol'; import { NounsDAOLogicSharedBaseTest } from '../helpers/NounsDAOLogicSharedBase.t.sol'; +import { INounsDAOShared } from '../helpers/INounsDAOShared.sol'; import { DeployUtilsV3 } from '../helpers/DeployUtilsV3.sol'; -import { NounsDAOLogicV1 } from '../../../contracts/governance/NounsDAOLogicV1.sol'; import { NounsDAOLogicV2 } from '../../../contracts/governance/NounsDAOLogicV2.sol'; import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; import { NounsDAOProxyV2 } from '../../../contracts/governance/NounsDAOProxyV2.sol'; @@ -155,7 +155,7 @@ contract NounsDAOLogic_GasSnapshot_V3_propose is DeployUtilsV3, NounsDAOLogic_Ga address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { return _createDAOV3Proxy(timelock, nounsToken, vetoer); } } @@ -165,7 +165,7 @@ contract NounsDAOLogic_GasSnapshot_V2_propose is DeployUtilsV3, NounsDAOLogic_Ga address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { return _createDAOV2Proxy(timelock, nounsToken, vetoer); } } @@ -175,7 +175,7 @@ contract NounsDAOLogic_GasSnapshot_V3_vote is DeployUtilsV3, NounsDAOLogic_GasSn address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { return _createDAOV3Proxy(timelock, nounsToken, vetoer); } } @@ -185,7 +185,7 @@ contract NounsDAOLogic_GasSnapshot_V2_vote is DeployUtilsV3, NounsDAOLogic_GasSn address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { return _createDAOV2Proxy(timelock, nounsToken, vetoer); } } @@ -198,7 +198,7 @@ contract NounsDAOLogic_GasSnapshot_V3_voteDuringObjectionPeriod is address timelock, address nounsToken, address vetoer - ) internal override returns (NounsDAOLogicV1) { + ) internal override returns (INounsDAOShared) { return _createDAOV3Proxy(timelock, nounsToken, vetoer); } } diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol index ad73761ed5..b5aff8a781 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtils.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.19; import 'forge-std/Test.sol'; +import { INounsDAOShared } from './INounsDAOShared.sol'; import { DescriptorHelpers } from './DescriptorHelpers.sol'; import { NounsDescriptorV2 } from '../../../contracts/NounsDescriptorV2.sol'; import { SVGRenderer } from '../../../contracts/SVGRenderer.sol'; import { NounsArt } from '../../../contracts/NounsArt.sol'; import { NounsDAOExecutor } from '../../../contracts/governance/NounsDAOExecutor.sol'; -import { NounsDAOLogicV1 } from '../../../contracts/governance/NounsDAOLogicV1.sol'; import { NounsDAOLogicV2 } from '../../../contracts/governance/NounsDAOLogicV2.sol'; import { IProxyRegistry } from '../../../contracts/external/opensea/IProxyRegistry.sol'; import { NounsDescriptor } from '../../../contracts/NounsDescriptor.sol'; @@ -74,7 +74,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { address(nounsToken), vetoer, address(timelock), - address(new NounsDAOLogicV1()), + address(new NounsDAOLogicV2()), VOTING_PERIOD, VOTING_DELAY, PROPOSAL_THRESHOLD, @@ -97,10 +97,10 @@ abstract contract DeployUtils is Test, DescriptorHelpers { address timelock, address nounsToken, address vetoer - ) internal returns (NounsDAOLogicV1) { + ) internal returns (INounsDAOShared) { return - NounsDAOLogicV1( - payable( + INounsDAOShared( + address( new NounsDAOProxyV2( timelock, nounsToken, @@ -120,7 +120,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { ); } - function deployDAOV2() internal returns (NounsDAOLogicV1) { + function deployDAOV2() internal returns (NounsDAOLogicV2) { NounsDAOExecutor timelock = new NounsDAOExecutor(address(1), TIMELOCK_DELAY); NounsAuctionHouse auctionLogic = new NounsAuctionHouse(); @@ -140,7 +140,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { new NounsSeeder(), IProxyRegistry(address(0)) ); - NounsDAOLogicV1 daoProxy = _createDAOV2Proxy(address(timelock), address(nounsToken), makeAddr('vetoer')); + INounsDAOShared daoProxy = _createDAOV2Proxy(address(timelock), address(nounsToken), makeAddr('vetoer')); vm.prank(address(timelock)); timelock.setPendingAdmin(address(daoProxy)); @@ -149,7 +149,7 @@ abstract contract DeployUtils is Test, DescriptorHelpers { nounsToken.transferOwnership(address(timelock)); - return daoProxy; + return NounsDAOLogicV2(payable(address(daoProxy))); } function get1967Implementation(address proxy) internal returns (address) { diff --git a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol index 0c2176d682..9218b83713 100644 --- a/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol +++ b/packages/nouns-contracts/test/foundry/helpers/DeployUtilsV3.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import 'forge-std/Test.sol'; import { DeployUtils } from './DeployUtils.sol'; -import { NounsDAOLogicV1 } from '../../../contracts/governance/NounsDAOLogicV1.sol'; +import { INounsDAOShared } from './INounsDAOShared.sol'; import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; import { NounsDAOProxyV3 } from '../../../contracts/governance/NounsDAOProxyV3.sol'; import { NounsDAOForkEscrow } from '../../../contracts/governance/fork/NounsDAOForkEscrow.sol'; @@ -26,11 +26,11 @@ abstract contract DeployUtilsV3 is DeployUtils { address timelock, address nounsToken, address vetoer - ) internal returns (NounsDAOLogicV1 dao) { + ) internal returns (INounsDAOShared dao) { uint256 nonce = vm.getNonce(address(this)); address predictedForkEscrowAddress = computeCreateAddress(address(this), nonce + 2); - dao = NounsDAOLogicV1( - payable( + dao = INounsDAOShared( + address( new NounsDAOProxyV3( timelock, nounsToken, diff --git a/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol new file mode 100644 index 0000000000..9f55d03c92 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import { NounsDAOStorageV3 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; + +interface INounsDAOShared { + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); + + function queue(uint256 proposalId) external; + + function execute(uint256 proposalId) external; + + function cancel(uint256 proposalId) external; + + function castVote(uint256 proposalId, uint8 support) external; + + function castVoteWithReason( + uint256 proposalId, + uint8 support, + string memory reason + ) external; + + function veto(uint256 proposalId) external; + + function state(uint256 proposalId) external view returns (NounsDAOStorageV3.ProposalState); + + function timelock() external view returns (address); + + function votingDelay() external view returns (uint256); + + function votingPeriod() external view returns (uint256); + + function proposalThresholdBPS() external view returns (uint256); + + function proposalThreshold() external view returns (uint256); + + function vetoer() external view returns (address); + + function _setVotingPeriod(uint256 votingPeriod_) external; + + function _setVotingDelay(uint256 votingDelay_) external; + + function _setProposalThresholdBPS(uint256 proposalThresholdBPS_) external; + + function _setQuorumVotesBPS(uint256 quorumVotesBPS_) external; + + function _burnVetoPower() external; +} diff --git a/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol b/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol index 26803346ca..f790b4b799 100644 --- a/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol +++ b/packages/nouns-contracts/test/foundry/helpers/NounsDAOLogicSharedBase.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.15; import 'forge-std/Test.sol'; -import { NounsDAOLogicV1 } from '../../../contracts/governance/NounsDAOLogicV1.sol'; +import { INounsDAOShared } from './INounsDAOShared.sol'; import { NounsDAOLogicV2 } from '../../../contracts/governance/NounsDAOLogicV2.sol'; import { NounsDAOProxy } from '../../../contracts/governance/NounsDAOProxy.sol'; import { NounsDAOProxyV2 } from '../../../contracts/governance/NounsDAOProxyV2.sol'; @@ -16,7 +16,7 @@ import { INounsTokenForkLike } from '../../../contracts/governance/fork/newdao/g import { Utils } from './Utils.sol'; abstract contract NounsDAOLogicSharedBaseTest is Test, DeployUtilsFork { - NounsDAOLogicV1 daoProxy; + INounsDAOShared daoProxy; NounsToken nounsToken; NounsDAOExecutor timelock = new NounsDAOExecutor(address(1), TIMELOCK_DELAY); address vetoer = address(0x3); @@ -47,7 +47,7 @@ abstract contract NounsDAOLogicSharedBaseTest is Test, DeployUtilsFork { address timelock, address nounsToken, address vetoer - ) internal virtual returns (NounsDAOLogicV1); + ) internal virtual returns (INounsDAOShared); function daoVersion() internal virtual returns (uint256) { return 0; // override to specify version @@ -112,15 +112,15 @@ abstract contract NounsDAOLogicSharedBaseTest is Test, DeployUtilsFork { return NounsDAOLogicV2(payable(address(daoProxy))); } - function deployForkDAOProxy() internal returns (NounsDAOLogicV1) { + function deployForkDAOProxy() internal returns (INounsDAOShared) { (address treasuryAddress, address tokenAddress, address daoAddress) = _deployForkDAO(); timelock = NounsDAOExecutor(payable(treasuryAddress)); nounsToken = NounsToken(tokenAddress); minter = nounsToken.minter(); - NounsDAOLogicV1 dao = NounsDAOLogicV1(daoAddress); + INounsDAOShared dao = INounsDAOShared(daoAddress); - vm.startPrank(address(dao.timelock())); + vm.startPrank(dao.timelock()); dao._setVotingPeriod(votingPeriod); dao._setVotingDelay(votingDelay); dao._setProposalThresholdBPS(proposalThresholdBPS); @@ -129,6 +129,6 @@ abstract contract NounsDAOLogicSharedBaseTest is Test, DeployUtilsFork { vm.warp(INounsTokenForkLike(tokenAddress).forkingPeriodEndTimestamp()); - return NounsDAOLogicV1(daoAddress); + return INounsDAOShared(daoAddress); } } From 0eaa244b481ed15a5b6b17d15592629cb6d48257 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 14:46:22 -0500 Subject: [PATCH 20/28] remove upgrade tests we don't need --- .../DescriptorUpgradeViaProposal.t.sol | 63 --- .../NounsDAOLogicV3/UpgradeToDAOV3.t.sol | 407 ------------------ .../UpgradeToDAOV3ForkMainnetTest.t.sol | 340 --------------- 3 files changed, 810 deletions(-) delete mode 100644 packages/nouns-contracts/test/foundry/DescriptorUpgradeViaProposal.t.sol delete mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3.t.sol delete mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3ForkMainnetTest.t.sol diff --git a/packages/nouns-contracts/test/foundry/DescriptorUpgradeViaProposal.t.sol b/packages/nouns-contracts/test/foundry/DescriptorUpgradeViaProposal.t.sol deleted file mode 100644 index 3ca9e0a55a..0000000000 --- a/packages/nouns-contracts/test/foundry/DescriptorUpgradeViaProposal.t.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.19; - -import 'forge-std/Test.sol'; -import { DeployUtils } from './helpers/DeployUtils.sol'; -import { NounsToken } from '../../contracts/NounsToken.sol'; -import { NounsDescriptorV2 } from '../../contracts/NounsDescriptorV2.sol'; -import { NounsDAOLogicV1 } from '../../contracts/governance/NounsDAOLogicV1.sol'; - -contract DescriptorUpgradeViaProposalTest is Test, DeployUtils { - NounsToken nounsToken; - NounsDAOLogicV1 dao; - address minter = address(2); - address tokenHolder = address(1337); - - function setUp() public { - address noundersDAO = address(42); - (address tokenAddress, address daoAddress) = _deployTokenAndDAOAndPopulateDescriptor( - noundersDAO, - noundersDAO, - minter - ); - nounsToken = NounsToken(tokenAddress); - dao = NounsDAOLogicV1(daoAddress); - - vm.startPrank(minter); - nounsToken.mint(); - nounsToken.transferFrom(minter, tokenHolder, 1); - vm.stopPrank(); - } - - function testUpgradeToV2ViaProposal() public { - NounsDescriptorV2 descriptorV2 = _deployAndPopulateV2(); - - address[] memory targets = new address[](1); - targets[0] = address(nounsToken); - uint256[] memory values = new uint256[](1); - values[0] = 0; - string[] memory signatures = new string[](1); - signatures[0] = 'setDescriptor(address)'; - bytes[] memory calldatas = new bytes[](1); - calldatas[0] = abi.encode(address(descriptorV2)); - - uint256 blockNumber = block.number + 1; - vm.roll(blockNumber); - - vm.startPrank(tokenHolder); - dao.propose(targets, values, signatures, calldatas, 'upgrade descriptor'); - blockNumber += VOTING_DELAY + 1; - vm.roll(blockNumber); - dao.castVote(1, 1); - vm.stopPrank(); - - blockNumber += VOTING_PERIOD + 1; - vm.roll(blockNumber); - dao.queue(1); - - vm.warp(block.timestamp + TIMELOCK_DELAY + 1); - dao.execute(1); - - assertEq(address(nounsToken.descriptor()), address(descriptorV2)); - } -} diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3.t.sol deleted file mode 100644 index 073e1fa914..0000000000 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3.t.sol +++ /dev/null @@ -1,407 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.15; - -import 'forge-std/Test.sol'; -import { NounsDAOLogicV3BaseTest } from './NounsDAOLogicV3BaseTest.sol'; -import { NounsDAOLogicV1 } from '../../../contracts/governance/NounsDAOLogicV1.sol'; -import { NounsDAOLogicV2 } from '../../../contracts/governance/NounsDAOLogicV2.sol'; -import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; -import { DeployUtils } from '../helpers/DeployUtils.sol'; -import { NounsDAOExecutorV2 } from '../../../contracts/governance/NounsDAOExecutorV2.sol'; -import { NounsDAOExecutorProxy } from '../../../contracts/governance/NounsDAOExecutorProxy.sol'; -import { INounsDAOExecutor, NounsDAOStorageV2, NounsDAOStorageV3 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; -import { NounsDAOForkEscrow } from '../../../contracts/governance/fork/NounsDAOForkEscrow.sol'; -import { ForkDAODeployer } from '../../../contracts/governance/fork/ForkDAODeployer.sol'; -import { ERC20Mock } from '../helpers/ERC20Mock.sol'; - -contract UpgradeToDAOV3Test is DeployUtils { - NounsDAOLogicV1 daoProxy; - address proposer = makeAddr('proposer'); - address proposer2 = makeAddr('proposer2'); - INounsDAOExecutor timelockV1; - ERC20Mock stETH = new ERC20Mock(); - - address[] targets; - uint256[] values; - string[] signatures; - bytes[] calldatas; - - event ProposalCreatedOnTimelockV1(uint256 id); - - function setUp() public virtual { - daoProxy = deployDAOV2(); - timelockV1 = daoProxy.timelock(); - - vm.startPrank(daoProxy.nouns().minter()); - daoProxy.nouns().mint(); - daoProxy.nouns().mint(); - daoProxy.nouns().transferFrom(daoProxy.nouns().minter(), proposer, 1); - daoProxy.nouns().transferFrom(daoProxy.nouns().minter(), proposer2, 2); - vm.stopPrank(); - vm.roll(block.number + 1); - - vm.deal(address(daoProxy.timelock()), 1000 ether); - } - - function test_upgradeToDAOV3() public { - address[] memory erc20TokensToIncludeInFork = new address[](1); - erc20TokensToIncludeInFork[0] = address(stETH); - ( - NounsDAOForkEscrow forkEscrow, - ForkDAODeployer forkDeployer, - NounsDAOLogicV3 daoV3Implementation, - NounsDAOExecutorV2 timelockV2 - ) = deployNewContracts(); - uint256 proposalId = proposeUpgradeToDAOV3( - address(daoV3Implementation), - address(timelockV2), - address(daoProxy.timelock()), - 500 ether, - forkEscrow, - forkDeployer, - erc20TokensToIncludeInFork - ); - - rollAndCastVote(proposer, proposalId, 1); - - queueAndExecute(proposalId); - - NounsDAOLogicV3 daoProxyAsV3 = NounsDAOLogicV3(payable(address(daoProxy))); - - assertEq(daoProxy.implementation(), address(daoV3Implementation)); - assertEq(daoProxyAsV3.timelockV1(), address(timelockV1)); - assertEq(address(daoProxy.timelock()), address(timelockV2)); - - // check fork params - assertEq(address(daoProxyAsV3.forkEscrow()), address(forkEscrow)); - assertEq(address(daoProxyAsV3.forkDAODeployer()), address(forkDeployer)); - assertEq(daoProxyAsV3.forkPeriod(), 7 days); - assertEq(daoProxyAsV3.forkThresholdBPS(), 2_000); - - address[] memory erc20sInFork = daoProxyAsV3.erc20TokensToIncludeInFork(); - assertEq(erc20sInFork.length, 1); - assertEq(erc20sInFork[0], address(stETH)); - - // check funds were transferred - assertEq(address(daoProxyAsV3.timelock()).balance, 500 ether); - assertEq(address(daoProxyAsV3.timelockV1()).balance, 500 ether); - } - - function test_proposalToSendETHWorksBeforeUpgrade() public { - uint256 proposalId = proposeToSendETH(proposer2, proposer2, 100 ether); - - rollAndCastVote(proposer, proposalId, 1); - - queueAndExecute(proposalId); - - assertEq(proposer2.balance, 100 ether); - } - - function test_proposalQueuedBeforeUpgrade_executeRevertsButExecuteOnV1Works() public { - uint256 proposalId = deployContractsAndProposeUpgradeToDAOV3(address(daoProxy.timelock()), 500 ether); - - uint256 proposalId2 = proposeToSendETH(proposer2, proposer2, 100 ether); - - rollAndCastVote(proposer, proposalId, 1); - - vm.prank(proposer2); - daoProxy.castVote(proposalId2, 1); - - vm.roll(block.number + daoProxy.votingPeriod() + 1); - daoProxy.queue(proposalId); - daoProxy.queue(proposalId2); - - vm.warp(block.timestamp + daoProxy.timelock().delay()); - daoProxy.execute(proposalId); - - vm.expectRevert("NounsDAOExecutor::executeTransaction: Transaction hasn't been queued."); - daoProxy.execute(proposalId2); - - NounsDAOLogicV3(payable(address(daoProxy))).executeOnTimelockV1(proposalId2); - assertEq(proposer2.balance, 100 ether); - } - - function test_proposalWasQueuedAfterUpgrade() public { - uint256 proposalId = deployContractsAndProposeUpgradeToDAOV3(address(daoProxy.timelock()), 500 ether); - - uint256 proposalId2 = proposeToSendETH(proposer2, proposer2, 100 ether); - - rollAndCastVote(proposer, proposalId, 1); - - vm.prank(proposer2); - daoProxy.castVote(proposalId2, 1); - - queueAndExecute(proposalId); - - daoProxy.queue(proposalId2); - vm.warp(block.timestamp + daoProxy.timelock().delay()); - daoProxy.execute(proposalId2); - - assertEq(proposer2.balance, 100 ether); - } - - function test_proposalAfterUpgrade() public { - upgradeToV3(); - - uint256 proposalId = proposeToSendETH(proposer2, proposer2, 100 ether); - - // check executeOnTimelockV1 is false - NounsDAOLogicV3 daoV3 = NounsDAOLogicV3(payable(address(daoProxy))); - NounsDAOStorageV3.ProposalCondensed memory proposal = daoV3.proposalsV3(proposalId); - assertFalse(proposal.executeOnTimelockV1); - - rollAndCastVote(proposer, proposalId, 1); - - queueAndExecute(proposalId); - - assertEq(proposer2.balance, 100 ether); - } - - function test_proposeOnTimelockV1() public { - upgradeToV3(); - - targets = [proposer]; - values = [400 ether]; - signatures = ['']; - calldatas = [bytes('')]; - vm.expectEmit(true, true, true, true); - emit ProposalCreatedOnTimelockV1(2); - vm.prank(proposer); - NounsDAOLogicV3 daoV3 = NounsDAOLogicV3(payable(address(daoProxy))); - uint256 proposalId = daoV3.proposeOnTimelockV1(targets, values, signatures, calldatas, 'send eth'); - - NounsDAOStorageV3.ProposalCondensed memory proposal = daoV3.proposalsV3(proposalId); - assertTrue(proposal.executeOnTimelockV1); - - rollAndCastVote(proposer, proposalId, 1); - queueAndExecute(proposalId); - - assertEq(proposer.balance, 400 ether); - assertEq(address(timelockV1).balance, 100 ether); - assertEq(address(daoProxy.timelock()).balance, 500 ether); - } - - function test_timelockV2IsUpgradable() public { - upgradeToV3(); - - targets = [address(daoProxy.timelock())]; - values = [0]; - signatures = ['upgradeTo(address)']; - address newTimelock = address(new NewTimelockMock()); - calldatas = [abi.encode(newTimelock)]; - vm.prank(proposer); - uint256 proposalId = daoProxy.propose(targets, values, signatures, calldatas, 'upgrade to 1234'); - - rollAndCastVote(proposer, proposalId, 1); - queueAndExecute(proposalId); - - assertEq(get1967Implementation(address(daoProxy.timelock())), address(newTimelock)); - assertEq(NewTimelockMock(payable(address(daoProxy.timelock()))).banner(), 'NewTimelockMock'); - } - - function test_daoCanBeUpgradedAfterUpgradeToV3() public { - upgradeToV3(); - - targets = [address(daoProxy)]; - values = [0]; - signatures = ['_setImplementation(address)']; - calldatas = [abi.encode(address(1234))]; - vm.prank(proposer); - uint256 proposalId = daoProxy.propose(targets, values, signatures, calldatas, 'upgrade to 1234'); - - rollAndCastVote(proposer, proposalId, 1); - queueAndExecute(proposalId); - - assertEq(daoProxy.implementation(), address(1234)); - } - - using stdStorage for StdStorage; - - function test_proposalCreatedInV2HasSameFieldsInV3() public { - vm.roll(block.number + 256); - - uint256 proposalId = proposeToSendETH(proposer2, proposer2, 100 ether); - rollAndCastVote(proposer, proposalId, 1); - queueAndExecute(proposalId); - - NounsDAOStorageV2.ProposalCondensed memory propV2 = NounsDAOLogicV2(payable(address(daoProxy))).proposals(1); - - upgradeToV3(); - - NounsDAOStorageV2.ProposalCondensed memory propV3 = NounsDAOLogicV3(payable(address(daoProxy))).proposals(1); - - assertEq(propV2.id, propV3.id); - assertEq(propV2.proposer, propV3.proposer); - assertEq(propV2.proposalThreshold, propV3.proposalThreshold); - assertEq(propV2.quorumVotes, propV3.quorumVotes); - assertEq(propV2.eta, propV3.eta); - assertEq(propV2.startBlock, propV3.startBlock); - assertEq(propV2.endBlock, propV3.endBlock); - assertEq(propV2.forVotes, propV3.forVotes); - assertEq(propV2.againstVotes, propV3.againstVotes); - assertEq(propV2.abstainVotes, propV3.abstainVotes); - assertEq(propV2.canceled, propV3.canceled); - assertEq(propV2.vetoed, propV3.vetoed); - assertEq(propV2.executed, propV3.executed); - assertEq(propV2.totalSupply, propV3.totalSupply); - assertEq(propV2.creationBlock, propV3.creationBlock); - } - - function upgradeToV3() internal returns (uint256 proposalId) { - proposalId = deployContractsAndProposeUpgradeToDAOV3(address(daoProxy.timelock()), 500 ether); - rollAndCastVote(proposer, proposalId, 1); - queueAndExecute(proposalId); - } - - function queueAndExecute(uint256 proposalId) internal { - vm.roll(block.number + daoProxy.votingPeriod() + 1); - daoProxy.queue(proposalId); - - vm.warp(block.timestamp + daoProxy.timelock().delay()); - daoProxy.execute(proposalId); - } - - function rollAndCastVote( - address voter, - uint256 proposalId, - uint8 support - ) internal { - vm.roll(block.number + daoProxy.votingDelay() + 1); - vm.prank(voter); - daoProxy.castVote(proposalId, support); - } - - function proposeToSendETH( - address proposer_, - address to, - uint256 amount - ) internal returns (uint256 proposalId) { - targets = [to]; - values = [amount]; - signatures = ['']; - calldatas = [bytes('')]; - - vm.prank(proposer_); - proposalId = daoProxy.propose(targets, values, signatures, calldatas, 'send eth'); - } - - function deployAndInitTimelockV2() internal returns (NounsDAOExecutorV2 timelockV2, address timelockV2Impl) { - timelockV2Impl = address(new NounsDAOExecutorV2()); - - bytes memory initCallData = abi.encodeWithSignature( - 'initialize(address,uint256)', - address(daoProxy), - timelockV1.delay() - ); - - timelockV2 = NounsDAOExecutorV2(payable(address(new NounsDAOExecutorProxy(timelockV2Impl, initCallData)))); - - assertEq(timelockV2.delay(), timelockV1.delay()); - assertEq(get1967Implementation(address(timelockV2)), timelockV2Impl); - - return (timelockV2, timelockV2Impl); - } - - function deployNewContracts() - internal - returns ( - NounsDAOForkEscrow forkEscrow, - ForkDAODeployer forkDeployer, - NounsDAOLogicV3 daoV3Impl, - NounsDAOExecutorV2 timelockV2 - ) - { - forkEscrow = new NounsDAOForkEscrow(address(daoProxy), address(daoProxy.nouns())); - forkDeployer = new ForkDAODeployer( - address(0), // tokenImpl_, - address(0), // auctionImpl_, - address(0), // governorImpl_, - address(0), // treasuryImpl_, - 30 days, - FORK_DAO_VOTING_PERIOD, - FORK_DAO_VOTING_DELAY, - FORK_DAO_PROPOSAL_THRESHOLD_BPS, - FORK_DAO_QUORUM_VOTES_BPS - ); - daoV3Impl = new NounsDAOLogicV3(); - (timelockV2, ) = deployAndInitTimelockV2(); - } - - function deployContractsAndProposeUpgradeToDAOV3(address timelockV1_, uint256 ethToSendToNewTimelock) - internal - returns (uint256 proposalId) - { - ( - NounsDAOForkEscrow forkEscrow, - ForkDAODeployer forkDeployer, - NounsDAOLogicV3 daoV3Impl, - NounsDAOExecutorV2 timelockV2 - ) = deployNewContracts(); - - address[] memory erc20TokensToIncludeInFork = new address[](1); - erc20TokensToIncludeInFork[0] = address(stETH); - proposalId = proposeUpgradeToDAOV3( - address(daoV3Impl), - address(timelockV2), - timelockV1_, - ethToSendToNewTimelock, - forkEscrow, - forkDeployer, - erc20TokensToIncludeInFork - ); - } - - function proposeUpgradeToDAOV3( - address daoV3Implementation, - address timelockV2, - address timelockV1_, - uint256 ethToSendToNewTimelock, - NounsDAOForkEscrow forkEscrow, - ForkDAODeployer forkDeployer, - address[] memory erc20TokensToIncludeInFork - ) internal returns (uint256 proposalId) { - targets = new address[](4); - values = new uint256[](4); - signatures = new string[](4); - calldatas = new bytes[](4); - - uint256 i = 0; - targets[i] = address(timelockV2); - values[i] = ethToSendToNewTimelock; - signatures[i] = ''; - calldatas[i] = ''; - - i++; - targets[i] = address(daoProxy); - values[i] = 0; - signatures[i] = '_setImplementation(address)'; - calldatas[i] = abi.encode(daoV3Implementation); - - i++; - targets[i] = address(daoProxy); - values[i] = 0; - signatures[i] = '_setForkParams(address,address,address[],uint256,uint256)'; - calldatas[i] = abi.encode( - address(forkEscrow), - address(forkDeployer), - erc20TokensToIncludeInFork, - 7 days, - 2_000 - ); - - i++; - targets[i] = address(daoProxy); - values[i] = 0; - signatures[i] = '_setTimelocksAndAdmin(address,address,address)'; - calldatas[i] = abi.encode(timelockV2, timelockV1_, timelockV2); - - vm.prank(proposer); - proposalId = daoProxy.propose(targets, values, signatures, calldatas, 'upgrade to v3'); - } -} - -contract NewTimelockMock is NounsDAOExecutorV2 { - function banner() public pure returns (string memory) { - return 'NewTimelockMock'; - } -} diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3ForkMainnetTest.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3ForkMainnetTest.t.sol deleted file mode 100644 index fa9e4fe85a..0000000000 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/UpgradeToDAOV3ForkMainnetTest.t.sol +++ /dev/null @@ -1,340 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.15; - -import 'forge-std/Test.sol'; -import { ProposeDAOV3UpgradeMainnet } from '../../../script/ProposeDAOV3UpgradeMainnet.s.sol'; -import { DeployDAOV3NewContractsMainnet } from '../../../script/DeployDAOV3NewContractsMainnet.s.sol'; -import { ProposeTimelockMigrationCleanupMainnet } from '../../../script/ProposeTimelockMigrationCleanupMainnet.s.sol'; -import { ProposeENSReverseLookupConfigMainnet } from '../../../script/ProposeENSReverseLookupConfigMainnet.s.sol'; -import { NounsDAOLogicV1 } from '../../../contracts/governance/NounsDAOLogicV1.sol'; -import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; -import { NounsDAOProxy } from '../../../contracts/governance/NounsDAOProxy.sol'; -import { NounsToken } from '../../../contracts/NounsToken.sol'; -import { NounsDAOExecutorV2 } from '../../../contracts/governance/NounsDAOExecutorV2.sol'; -import { INounsDAOExecutor, INounsDAOForkEscrow, IForkDAODeployer } from '../../../contracts/governance/NounsDAOInterfaces.sol'; -import { NounsDAOForkEscrow } from '../../../contracts/governance/fork/NounsDAOForkEscrow.sol'; -import { ForkDAODeployer } from '../../../contracts/governance/fork/ForkDAODeployer.sol'; -import { NounsDAOLogicV1Fork } from '../../../contracts/governance/fork/newdao/governance/NounsDAOLogicV1Fork.sol'; -import { Strings } from '@openzeppelin/contracts/utils/Strings.sol'; -import { ERC20Transferer } from '../../../contracts/utils/ERC20Transferer.sol'; -import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import { NounsAuctionHouse } from '../../../contracts/NounsAuctionHouse.sol'; -import { ERC721Enumerable } from '../../../contracts/base/ERC721Enumerable.sol'; -import { NounsTokenFork } from '../../../contracts/governance/fork/newdao/token/NounsTokenFork.sol'; -import { NounsDAOLogicV1Fork } from '../../../contracts/governance/fork/newdao/governance/NounsDAOLogicV1Fork.sol'; -import { ENSNamehash } from '../lib/ENSNamehash.sol'; -import '../lib/ENSInterfaces.sol'; -import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol'; -import { IERC721 } from '@openzeppelin/contracts/token/ERC721/IERC721.sol'; - -interface IHasName { - function NAME() external pure returns (string memory); -} - -interface IOwnable { - function owner() external view returns (address); -} - -contract UpgradeToDAOV3ForkMainnetTest is Test { - address public constant NOUNDERS = 0x2573C60a6D127755aA2DC85e342F7da2378a0Cc5; - NounsToken public nouns = NounsToken(0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03); - uint256 proposalId; - address proposerAddr = vm.addr(0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb); - NounsDAOLogicV1 public constant NOUNS_DAO_PROXY_MAINNET = - NounsDAOLogicV1(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d); - INounsDAOExecutor public constant NOUNS_TIMELOCK_V1_MAINNET = - INounsDAOExecutor(0x0BC3807Ec262cB779b38D65b38158acC3bfedE10); - address public constant AUCTION_HOUSE_PROXY_ADMIN_MAINNET = 0xC1C119932d78aB9080862C5fcb964029f086401e; - address public constant DESCRIPTOR_MAINNET = 0x6229c811D04501523C6058bfAAc29c91bb586268; - address public constant LILNOUNS_MAINNET = 0x4b10701Bfd7BFEdc47d50562b76b436fbB5BdB3B; - address whaleAddr = 0xf6B6F07862A02C85628B3A9688beae07fEA9C863; - address public constant STETH_MAINNET = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; - address public constant WSTETH_MAINNET = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - address public constant RETH_MAINNET = 0xae78736Cd615f374D3085123A210448E74Fc6393; - address public constant USDC_MAINNET = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address public constant TOKEN_BUYER_MAINNET = 0x4f2aCdc74f6941390d9b1804faBc3E780388cfe5; - address public constant PAYER_MAINNET = 0xd97Bcd9f47cEe35c0a9ec1dc40C1269afc9E8E1D; - address public constant AUCTION_HOUSE_PROXY_MAINNET = 0x830BD73E4184ceF73443C15111a1DF14e495C706; - uint256 public constant USDC_BALANCE = 300_000 * 1e6; - - uint256 initialETHInTreasury; - uint256 expectedTreasuryV2ETHBalanceAfterFirstProposal; - uint256 stETHBalance; - uint256 stETHBuffer; - NounsDAOExecutorV2 timelockV2; - NounsDAOLogicV3 daoV3; - - uint256[] tokenIds; - address[] targets; - uint256[] values; - string[] signatures; - bytes[] calldatas; - - function setUp() public { - // at block 17766661 a recent proposal to convert another 10K ETH into stETH was executed - vm.createSelectFork(vm.envString('RPC_MAINNET'), 17766662); - - deal(USDC_MAINNET, address(NOUNS_TIMELOCK_V1_MAINNET), USDC_BALANCE); - - ProposeDAOV3UpgradeMainnet upgradePropScript = new ProposeDAOV3UpgradeMainnet(); - stETHBalance = IERC20(STETH_MAINNET).balanceOf(address(NOUNS_TIMELOCK_V1_MAINNET)); - initialETHInTreasury = address(NOUNS_TIMELOCK_V1_MAINNET).balance; - expectedTreasuryV2ETHBalanceAfterFirstProposal = upgradePropScript.ETH_TO_SEND_TO_NEW_TIMELOCK(); - stETHBuffer = upgradePropScript.STETH_BUFFER(); - - // give ourselves voting power - vm.prank(NOUNDERS); - nouns.delegate(proposerAddr); - - vm.roll(block.number + 1); - - // deploy contracts - - vm.setEnv('DEPLOYER_PRIVATE_KEY', '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); - - ( - NounsDAOForkEscrow forkEscrow, - ForkDAODeployer forkDeployer, - NounsDAOLogicV3 daoV3Impl, - NounsDAOExecutorV2 timelockV2_, - ERC20Transferer erc20Transferer_ - ) = new DeployDAOV3NewContractsMainnet().run(); - - timelockV2 = timelockV2_; - - // propose upgrade - - vm.setEnv('PROPOSER_KEY', '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'); - - vm.setEnv('DAO_V3_IMPL', Strings.toHexString(uint160(address(daoV3Impl)), 20)); - vm.setEnv('TIMELOCK_V2', Strings.toHexString(uint160(address(timelockV2)), 20)); - vm.setEnv('FORK_ESCROW', Strings.toHexString(uint160(address(forkEscrow)), 20)); - vm.setEnv('FORK_DEPLOYER', Strings.toHexString(uint160(address(forkDeployer)), 20)); - vm.setEnv('ERC20_TRANSFERER', Strings.toHexString(uint160(address(erc20Transferer_)), 20)); - vm.setEnv('PROPOSAL_DESCRIPTION_FILE', 'test/foundry/NounsDAOLogicV3/proposal-description.txt'); - - proposalId = upgradePropScript.run(); - - // simulate vote & proposal execution - voteAndExecuteProposal(); - - daoV3 = NounsDAOLogicV3(payable(address(NOUNS_DAO_PROXY_MAINNET))); - } - - function voteAndExecuteProposal() internal { - vm.roll(block.number + 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_V1_MAINNET.delay()); - NOUNS_DAO_PROXY_MAINNET.execute(proposalId); - } - - function test_transfersETHToNewTimelock() public { - assertEq( - address(daoV3.timelockV1()).balance, - initialETHInTreasury - expectedTreasuryV2ETHBalanceAfterFirstProposal - ); - assertEq(address(daoV3.timelock()).balance, expectedTreasuryV2ETHBalanceAfterFirstProposal); - } - - function test_timelockV2adminIsDAO() public { - assertEq(timelockV2.admin(), address(NOUNS_DAO_PROXY_MAINNET)); - } - - function test_timelockV2delayIsCopiedFromTimelockV1() public { - assertEq(timelockV2.delay(), NOUNS_TIMELOCK_V1_MAINNET.delay()); - } - - function test_forkEscrowConstructorParamsAreCorrect() public { - INounsDAOForkEscrow forkEscrow = daoV3.forkEscrow(); - assertEq(address(forkEscrow.dao()), address(NOUNS_DAO_PROXY_MAINNET)); - assertEq(address(forkEscrow.nounsToken()), address(nouns)); - } - - function test_forkDeployerSetsImplementationContracts() public { - IForkDAODeployer forkDeployer = daoV3.forkDAODeployer(); - assertEq(IHasName(forkDeployer.tokenImpl()).NAME(), 'NounsTokenFork'); - assertEq(IHasName(forkDeployer.auctionImpl()).NAME(), 'NounsAuctionHouseFork'); - assertEq(NounsDAOLogicV1Fork(forkDeployer.governorImpl()).name(), 'Nouns DAO'); - assertEq(IHasName(forkDeployer.treasuryImpl()).NAME(), 'NounsDAOExecutorV2'); - } - - function test_forkParams() public { - address[] memory erc20TokensToIncludeInFork = daoV3.erc20TokensToIncludeInFork(); - assertEq(erc20TokensToIncludeInFork.length, 4); - assertEq(erc20TokensToIncludeInFork[0], STETH_MAINNET); - assertEq(erc20TokensToIncludeInFork[1], WSTETH_MAINNET); - assertEq(erc20TokensToIncludeInFork[2], RETH_MAINNET); - assertEq(erc20TokensToIncludeInFork[3], USDC_MAINNET); - - assertEq(daoV3.forkPeriod(), 7 days); - assertEq(daoV3.forkThresholdBPS(), 2000); - } - - function test_setsTimelocksAndAdmin() public { - assertEq(address(daoV3.timelock()), address(timelockV2)); - assertEq(address(daoV3.timelockV1()), address(NOUNS_TIMELOCK_V1_MAINNET)); - assertEq(NounsDAOProxy(payable(address(daoV3))).admin(), address(timelockV2)); - } - - function test_DAOV3Params() public { - assertEq(daoV3.lastMinuteWindowInBlocks(), 0); - assertEq(daoV3.objectionPeriodDurationInBlocks(), 0); - assertEq(daoV3.proposalUpdatablePeriodInBlocks(), 0); - - assertEq(daoV3.voteSnapshotBlockSwitchProposalId(), 348); - } - - function test_transfersAllstETHExceptTheBuffer() public { - assertEq(IERC20(STETH_MAINNET).balanceOf(address(NOUNS_TIMELOCK_V1_MAINNET)), stETHBuffer + 1); - assertEq(IERC20(STETH_MAINNET).balanceOf(address(timelockV2)), stETHBalance - stETHBuffer - 1); - } - - function test_AuctionHouseProxyAndAdmin_changedOwner() public { - assertEq(IOwnable(AUCTION_HOUSE_PROXY_MAINNET).owner(), address(timelockV2)); - assertEq(Ownable(AUCTION_HOUSE_PROXY_ADMIN_MAINNET).owner(), address(timelockV2)); - } - - function test_descriptor_changedOwner() public { - assertEq(Ownable(DESCRIPTOR_MAINNET).owner(), address(timelockV2)); - } - - function test_AuctionHouseRevenueGoesToNewTimelock() public { - assertEq(address(daoV3.timelock()).balance, expectedTreasuryV2ETHBalanceAfterFirstProposal); - - (, uint256 amount, , uint256 endTime, , ) = NounsAuctionHouse(AUCTION_HOUSE_PROXY_MAINNET).auction(); - vm.warp(endTime + 1); - NounsAuctionHouse(AUCTION_HOUSE_PROXY_MAINNET).settleCurrentAndCreateNewAuction(); - - assertEq(address(daoV3.timelock()).balance, expectedTreasuryV2ETHBalanceAfterFirstProposal + amount); - } - - function test_forkScenarioAfterUpgrade() public { - uint256[] memory whaleTokens = _getAllNounsOf(whaleAddr); - _escrowAllNouns(whaleAddr); - _escrowAllNouns(NOUNDERS); - _escrowAllNouns(0x5606B493c51316A9e65c9b2A00BbF7Ff92515A3E); - _escrowAllNouns(0xd1d1D4e36117aB794ec5d4c78cBD3a8904E691D0); - _escrowAllNouns(0x7dE92ca2D0768cDbA376Aac853234D4EEd8d8B5C); - _escrowAllNouns(0xFa4FC4ec2F81A4897743C5b4f45907c02ce06199); - - (address forkTreasury, address forkToken) = daoV3.executeFork(); - - vm.startPrank(whaleAddr); - NounsTokenFork(forkToken).claimFromEscrow(whaleTokens); - vm.roll(block.number + 1); - - NounsDAOLogicV1Fork forkDao = NounsDAOLogicV1Fork(NounsDAOExecutorV2(payable(forkTreasury)).admin()); - - targets = [makeAddr('wallet')]; - values = [50 ether]; - signatures = ['']; - calldatas = [bytes('')]; - - vm.expectRevert(NounsDAOLogicV1Fork.GovernanceBlockedDuringForkingPeriod.selector); - forkDao.propose(targets, values, signatures, calldatas, 'new prop'); - - vm.warp(block.timestamp + 7 days); - vm.expectRevert(NounsDAOLogicV1Fork.WaitingForTokensToClaimOrExpiration.selector); - forkDao.propose(targets, values, signatures, calldatas, 'new prop'); - - vm.warp(forkDao.delayedGovernanceExpirationTimestamp() + 1); - forkDao.propose(targets, values, signatures, calldatas, 'new prop'); - - vm.roll(block.number + forkDao.votingDelay() + 1); - forkDao.castVote(1, 1); - - vm.roll(block.number + forkDao.votingPeriod()); - forkDao.queue(1); - - vm.warp(block.timestamp + 2 days); - forkDao.execute(1); - - assertEq(makeAddr('wallet').balance, 50 ether); - - // check new forked DAO has correct params - assertEq(forkDao.votingDelay(), 36000); - assertEq(forkDao.votingPeriod(), 36000); - assertEq(forkDao.proposalThresholdBPS(), 25); - assertEq(forkDao.quorumVotesBPS(), 1000); - } - - function test_timelockV1CleanupProposal() public { - uint256 expectedV2Balance = address(timelockV2).balance + address(NOUNS_TIMELOCK_V1_MAINNET).balance; - uint256 rETHExpectedBalance = IERC20(RETH_MAINNET).balanceOf(address(NOUNS_TIMELOCK_V1_MAINNET)); - uint256 wstETHExpectedBalance = IERC20(WSTETH_MAINNET).balanceOf(address(NOUNS_TIMELOCK_V1_MAINNET)); - uint256 usdcExpectedBalance = IERC20(USDC_MAINNET).balanceOf(address(NOUNS_TIMELOCK_V1_MAINNET)); - - proposalId = new ProposeTimelockMigrationCleanupMainnet().run(); - voteAndExecuteProposal(); - - assertEq(nouns.owner(), address(timelockV2)); - assertEq(address(NOUNS_TIMELOCK_V1_MAINNET).balance, 0); - assertEq(address(timelockV2).balance, expectedV2Balance); - assertTrue(IERC721(LILNOUNS_MAINNET).isApprovedForAll(address(NOUNS_TIMELOCK_V1_MAINNET), address(timelockV2))); - assertEq(nouns.balanceOf(address(NOUNS_TIMELOCK_V1_MAINNET)), 0); - assertEq(IOwnable(TOKEN_BUYER_MAINNET).owner(), address(timelockV2)); - assertEq(IOwnable(PAYER_MAINNET).owner(), address(timelockV2)); - assertEq(IERC20(STETH_MAINNET).balanceOf(address(timelockV2)), stETHBalance - 1); - assertEq(IERC20(WSTETH_MAINNET).balanceOf(address(timelockV2)), wstETHExpectedBalance); - assertEq(IERC20(RETH_MAINNET).balanceOf(address(timelockV2)), rETHExpectedBalance); - assertEq(IERC20(USDC_MAINNET).balanceOf(address(timelockV2)), usdcExpectedBalance); - } - - function test_ensChange_nounsDotETHResolvesBothWaysWithTimelockV2() public { - ENS ens = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e); - // 0xdc972a4db1aa8630a234db4202794eae94ad0e7a9e201e13667ac92aa887a02a - bytes32 node = ENSNamehash.namehash('nouns.eth'); - Resolver resolver = Resolver(ens.resolver(node)); - - // showing nouns.eth resolves to timelockv1 - assertEq(resolver.addr(node), address(NOUNS_TIMELOCK_V1_MAINNET)); - - // this is a critical step that will need to happen outside DAO proposals - // 0x88f9E324801320A3fC22C8d045A98Ad32a490d8E; - vm.prank(ens.owner(node)); - resolver.setAddr(node, address(timelockV2)); - - // showing nouns.eth resolves to timelockv2 after the setAddr change - assertEq(resolver.addr(node), address(timelockV2)); - - // Now tackling reverse lookup - - // the proposal calls (reverse.ens.eth).setName('nouns.eth') from timelock V2 - proposalId = new ProposeENSReverseLookupConfigMainnet().run(); - voteAndExecuteProposal(); - - // reverse.ens.eth - ReverseRegistrar reverse = ReverseRegistrar(0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb); - bytes32 resolvedReverseNode = reverse.node(address(timelockV2)); // 0xb983f3b9362fbdfcdb9012cf09dce9ae0c0a377c167b14fdf5b3bd94a4dfdf81 - - // showing that timelockV2's address resolves to nouns.eth - assertEq(reverse.defaultResolver().name(resolvedReverseNode), 'nouns.eth'); - } - - function _escrowAllNouns(address owner) internal { - vm.startPrank(owner); - daoV3.nouns().setApprovalForAll(address(daoV3), true); - daoV3.escrowToFork(_getAllNounsOf(owner), new uint256[](0), ''); - vm.stopPrank(); - } - - function _getAllNounsOf(address owner) internal view returns (uint256[] memory) { - ERC721Enumerable nouns_ = ERC721Enumerable(address(daoV3.nouns())); - uint256 numTokens = nouns_.balanceOf(owner); - - uint256[] memory tokens = new uint256[](numTokens); - - for (uint256 i; i < numTokens; i++) { - tokens[i] = nouns_.tokenOfOwnerByIndex(owner, i); - } - - return tokens; - } -} From 7369e4711c29a6dd4196fa2dddcfb909a95ab705 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 14:53:46 -0500 Subject: [PATCH 21/28] point more tests to dao V3 cancel tests were already duplicated withdraw test now migrated --- .../test/foundry/NounsDAOLogicV2.t.sol | 150 ------------------ .../foundry/NounsDAOLogicV3/Withdraw.t.sol | 31 ++++ 2 files changed, 31 insertions(+), 150 deletions(-) delete mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV2.t.sol create mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Withdraw.t.sol diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV2.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV2.t.sol deleted file mode 100644 index eb17e0fdb8..0000000000 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV2.t.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.15; - -import 'forge-std/Test.sol'; -import { NounsDAOLogicV2 } from '../../contracts/governance/NounsDAOLogicV2.sol'; -import { NounsDAOProxyV2 } from '../../contracts/governance/NounsDAOProxyV2.sol'; -import { NounsDAOStorageV2, NounsDAOStorageV1Adjusted } from '../../contracts/governance/NounsDAOInterfaces.sol'; -import { NounsDescriptorV2 } from '../../contracts/NounsDescriptorV2.sol'; -import { DeployUtils } from './helpers/DeployUtils.sol'; -import { NounsToken } from '../../contracts/NounsToken.sol'; -import { NounsSeeder } from '../../contracts/NounsSeeder.sol'; -import { IProxyRegistry } from '../../contracts/external/opensea/IProxyRegistry.sol'; -import { NounsDAOExecutor } from '../../contracts/governance/NounsDAOExecutor.sol'; - -contract NounsDAOLogicV2Test is Test, DeployUtils { - NounsDAOLogicV2 daoLogic; - NounsDAOLogicV2 daoProxy; - NounsToken nounsToken; - NounsDAOExecutor timelock = new NounsDAOExecutor(address(1), TIMELOCK_DELAY); - address vetoer = address(0x3); - address admin = address(0x4); - address noundersDAO = address(0x5); - address minter = address(0x6); - address proposer = address(0x7); - uint256 votingPeriod = 6000; - uint256 votingDelay = 1; - uint256 proposalThresholdBPS = 200; - - event Withdraw(uint256 amount, bool sent); - - function setUp() public virtual { - daoLogic = new NounsDAOLogicV2(); - - NounsDescriptorV2 descriptor = _deployAndPopulateV2(); - - nounsToken = new NounsToken(noundersDAO, minter, descriptor, new NounsSeeder(), IProxyRegistry(address(0))); - - daoProxy = NounsDAOLogicV2( - payable( - new NounsDAOProxyV2( - address(timelock), - address(nounsToken), - vetoer, - admin, - address(daoLogic), - votingPeriod, - votingDelay, - proposalThresholdBPS, - NounsDAOStorageV2.DynamicQuorumParams({ - minQuorumVotesBPS: 200, - maxQuorumVotesBPS: 2000, - quorumCoefficient: 10000 - }) - ) - ) - ); - - vm.prank(address(timelock)); - timelock.setPendingAdmin(address(daoProxy)); - vm.prank(address(daoProxy)); - timelock.acceptAdmin(); - } - - function propose( - address target, - uint256 value, - string memory signature, - bytes memory data - ) internal returns (uint256 proposalId) { - vm.prank(proposer); - 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 = daoProxy.propose(targets, values, signatures, calldatas, 'my proposal'); - } -} - -contract CancelProposalTest is NounsDAOLogicV2Test { - uint256 proposalId; - - function setUp() public override { - super.setUp(); - - vm.prank(minter); - nounsToken.mint(); - - vm.prank(minter); - nounsToken.transferFrom(minter, proposer, 1); - - vm.roll(block.number + 1); - - proposalId = propose(address(0x1234), 100, '', ''); - } - - function testProposerCanCancelProposal() public { - vm.prank(proposer); - daoProxy.cancel(proposalId); - - assertEq(uint256(daoProxy.state(proposalId)), uint256(NounsDAOStorageV1Adjusted.ProposalState.Canceled)); - } - - function testNonProposerCantCancel() public { - vm.expectRevert('NounsDAO::cancel: proposer above threshold'); - daoProxy.cancel(proposalId); - - assertEq(uint256(daoProxy.state(proposalId)), uint256(NounsDAOStorageV1Adjusted.ProposalState.Pending)); - } - - function testAnyoneCanCancelIfProposerVotesBelowThreshold() public { - vm.prank(proposer); - nounsToken.transferFrom(proposer, address(0x9999), 1); - - vm.roll(block.number + 1); - - daoProxy.cancel(proposalId); - - assertEq(uint256(daoProxy.state(proposalId)), uint256(NounsDAOStorageV1Adjusted.ProposalState.Canceled)); - } -} - -contract WithdrawTest is NounsDAOLogicV2Test { - function setUp() public override { - super.setUp(); - } - - function test_withdraw_worksForAdmin() public { - vm.deal(address(daoProxy), 100 ether); - uint256 balanceBefore = admin.balance; - - vm.expectEmit(true, true, true, true); - emit Withdraw(100 ether, true); - - vm.prank(admin); - (uint256 amount, bool sent) = daoProxy._withdraw(); - - assertEq(amount, 100 ether); - assertTrue(sent); - assertEq(admin.balance - balanceBefore, 100 ether); - } - - function test_withdraw_revertsForNonAdmin() public { - vm.expectRevert(NounsDAOLogicV2.AdminOnly.selector); - daoProxy._withdraw(); - } -} diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Withdraw.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Withdraw.t.sol new file mode 100644 index 0000000000..52aa872c1d --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Withdraw.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import 'forge-std/Test.sol'; +import { NounsDAOLogicV3BaseTest } from './NounsDAOLogicV3BaseTest.sol'; +import { NounsDAOStorageV3 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; +import { NounsDAOV3Admin } from '../../../contracts/governance/NounsDAOV3Admin.sol'; + +contract WithdrawTest is NounsDAOLogicV3BaseTest { + event Withdraw(uint256 amount, bool sent); + + function test_withdraw_worksForAdmin() public { + vm.deal(address(dao), 100 ether); + uint256 balanceBefore = address(timelock).balance; + + vm.expectEmit(true, true, true, true); + emit Withdraw(100 ether, true); + + vm.prank(address(timelock)); + (uint256 amount, bool sent) = dao._withdraw(); + + assertEq(amount, 100 ether); + assertTrue(sent); + assertEq(address(timelock).balance - balanceBefore, 100 ether); + } + + function test_withdraw_revertsForNonAdmin() public { + vm.expectRevert(NounsDAOV3Admin.AdminOnly.selector); + dao._withdraw(); + } +} From f9ec85d176081fe38d6e1437b2208c0a51fc54e9 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 15:06:44 -0500 Subject: [PATCH 22/28] cleanup --- .../test/foundry/governance/InflationHandling.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol b/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol index 0a99499924..a99e855780 100644 --- a/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol +++ b/packages/nouns-contracts/test/foundry/governance/InflationHandling.t.sol @@ -82,8 +82,6 @@ contract NounsDAOLogicV2InflationHandling40TotalSupplyTest is NounsDAOLogicV2Inf function testSetsParametersCorrectly() public { assertEq(daoProxy.proposalThresholdBPS(), proposalThresholdBPS_); - // assertEq(daoProxyAsV2().minQuorumVotesBPS(), minQuorumVotesBPS); - assertEq(daoProxyAsV2().getDynamicQuorumParamsAt(block.number).minQuorumVotesBPS, minQuorumVotesBPS); } From 8eca105a4a75944e965b564b9abe704ae444e581 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 15:07:45 -0500 Subject: [PATCH 23/28] point veto to test to V3 --- ...AndVeto.t.sol => NounsDAOLogicState.t.sol} | 10 +- .../test/foundry/NounsDAOLogicV3/Veto.t.sol | 276 ++++++++++++++++++ .../test/foundry/helpers/INounsDAOShared.sol | 6 + 3 files changed, 286 insertions(+), 6 deletions(-) rename packages/nouns-contracts/test/foundry/{NounsDAOLogicStateAndVeto.t.sol => NounsDAOLogicState.t.sol} (99%) create mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Veto.t.sol diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicState.t.sol similarity index 99% rename from packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol rename to packages/nouns-contracts/test/foundry/NounsDAOLogicState.t.sol index 6a7d155a9b..7e2d81e249 100644 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicStateAndVeto.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicState.t.sol @@ -208,7 +208,10 @@ contract NounsDAOLogicV3StateTest is NounsDAOLogicStateBaseTest { } } -abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { +contract NounsDAOLogicV3VetoTest is NounsDAOLogicSharedBaseTest { + event NewPendingVetoer(address oldPendingVetoer, address newPendingVetoer); + event NewVetoer(address oldVetoer, address newVetoer); + function setUp() public override { super.setUp(); @@ -387,11 +390,6 @@ abstract contract NounsDAOLogicVetoingBaseTest is NounsDAOLogicSharedBaseTest { assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); } -} - -contract NounsDAOLogicV3VetoingTest is NounsDAOLogicVetoingBaseTest { - event NewPendingVetoer(address oldPendingVetoer, address newPendingVetoer); - event NewVetoer(address oldVetoer, address newVetoer); function test_veto_worksForPropStateUpdatable() public { uint256 proposalId = propose(address(0x1234), 100, '', ''); diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Veto.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Veto.t.sol new file mode 100644 index 0000000000..0161fe2e6b --- /dev/null +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/Veto.t.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; +import { NounsDAOLogicV3 } from '../../../contracts/governance/NounsDAOLogicV3.sol'; +import { NounsDAOLogicSharedBaseTest } from '../helpers/NounsDAOLogicSharedBase.t.sol'; +import { NounsDAOStorageV3 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; +import { NounsDAOV3Proposals } from '../../../contracts/governance/NounsDAOV3Proposals.sol'; +import { NounsDAOV3Admin } from '../../../contracts/governance/NounsDAOV3Admin.sol'; +import { INounsDAOShared } from '../helpers/INounsDAOShared.sol'; + +contract NounsDAOLogicV3VetoTest is NounsDAOLogicSharedBaseTest { + event NewPendingVetoer(address oldPendingVetoer, address newPendingVetoer); + event NewVetoer(address oldVetoer, address newVetoer); + + function setUp() public override { + super.setUp(); + + mint(proposer, 1); + + vm.roll(block.number + 1); + } + + function testVetoerSetAsExpected() public { + assertEq(daoProxy.vetoer(), vetoer); + } + + function test_burnVetoPower_revertsForNonVetoer() public { + vm.expectRevert('NounsDAO::_burnVetoPower: vetoer only'); + daoProxy._burnVetoPower(); + } + + function test_burnVetoPower_worksForVetoer() public { + assertEq(daoProxy.vetoer(), vetoer); + + vm.prank(vetoer); + daoProxy._burnVetoPower(); + + assertEq(daoProxy.vetoer(), address(0)); + } + + function test_veto_revertsForNonVetoer() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + + vm.expectRevert(NounsDAOV3Proposals.VetoerOnly.selector); + + daoProxy.veto(proposalId); + } + + function test_veto_revertsWhenVetoerIsBurned() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.startPrank(vetoer); + daoProxy._burnVetoPower(); + + vm.expectRevert(NounsDAOV3Proposals.VetoerBurned.selector); + + daoProxy.veto(proposalId); + + vm.stopPrank(); + } + + function test_veto_worksForPropStatePending() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + // Need to roll one block because in V3 on the proposal creation block the state is Updatable + vm.roll(block.number + 1); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Pending); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_worksForPropStateActive() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.roll(block.number + daoProxy.votingDelay() + 1); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Active); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_worksForPropStateCanceled() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.prank(proposer); + daoProxy.cancel(proposalId); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Canceled); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_worksForPropStateDefeated() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.roll(block.number + daoProxy.votingDelay() + 1); + vm.prank(proposer); + daoProxy.castVote(proposalId, 0); + vm.roll(block.number + daoProxy.votingPeriod() + 1); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Defeated); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_worksForPropStateSucceeded() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.roll(block.number + daoProxy.votingDelay() + 1); + vm.prank(proposer); + daoProxy.castVote(proposalId, 1); + vm.roll(block.number + daoProxy.votingPeriod() + 1); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Succeeded); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_worksForPropStateQueued() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.roll(block.number + daoProxy.votingDelay() + 1); + vm.prank(proposer); + daoProxy.castVote(proposalId, 1); + vm.roll(block.number + daoProxy.votingPeriod() + 1); + daoProxy.queue(proposalId); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Queued); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_worksForPropStateExpired() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.roll(block.number + daoProxy.votingDelay() + 1); + vm.prank(proposer); + daoProxy.castVote(proposalId, 1); + vm.roll(block.number + daoProxy.votingPeriod() + 1); + daoProxy.queue(proposalId); + vm.warp(block.timestamp + timelock.delay() + timelock.GRACE_PERIOD() + 1); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Expired); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_revertsForPropStateExecuted() public { + vm.deal(address(timelock), 100); + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.roll(block.number + daoProxy.votingDelay() + 1); + vm.prank(proposer); + daoProxy.castVote(proposalId, 1); + vm.roll(block.number + daoProxy.votingPeriod() + 1); + daoProxy.queue(proposalId); + vm.warp(block.timestamp + timelock.delay() + 1); + daoProxy.execute(proposalId); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Executed); + + vm.expectRevert(NounsDAOV3Proposals.CantVetoExecutedProposal.selector); + vm.prank(vetoer); + daoProxy.veto(proposalId); + } + + function test_veto_worksForPropStateVetoed() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + vm.prank(vetoer); + daoProxy.veto(proposalId); + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + + vm.prank(vetoer); + daoProxy.veto(proposalId); + + assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_veto_worksForPropStateUpdatable() public { + uint256 proposalId = propose(address(0x1234), 100, '', ''); + NounsDAOLogicV3 daoAsV3 = NounsDAOLogicV3(payable(address(daoProxy))); + + assertTrue(daoAsV3.state(proposalId) == NounsDAOStorageV3.ProposalState.Updatable); + + vm.prank(vetoer); + daoAsV3.veto(proposalId); + + assertTrue(daoAsV3.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); + } + + function test_setPendingVetoer_failsIfNotCurrentVetoer() public { + vm.expectRevert(NounsDAOV3Proposals.VetoerOnly.selector); + daoProxy._setPendingVetoer(address(0x1234)); + } + + function test_setPendingVetoer_updatePendingVetoer() public { + assertEq(daoProxy.pendingVetoer(), address(0)); + + address pendingVetoer = address(0x3333); + + vm.prank(vetoer); + vm.expectEmit(true, true, true, true); + emit NewPendingVetoer(address(0), pendingVetoer); + daoProxy._setPendingVetoer(pendingVetoer); + + assertEq(daoProxy.pendingVetoer(), pendingVetoer); + } + + function test_onlyPendingVetoerCanAcceptNewVetoer() public { + address pendingVetoer = address(0x3333); + + vm.prank(vetoer); + daoProxy._setPendingVetoer(pendingVetoer); + + vm.expectRevert(NounsDAOV3Admin.PendingVetoerOnly.selector); + daoProxy._acceptVetoer(); + + vm.prank(pendingVetoer); + vm.expectEmit(true, true, true, true); + emit NewVetoer(vetoer, pendingVetoer); + daoProxy._acceptVetoer(); + + assertEq(daoProxy.vetoer(), pendingVetoer); + assertEq(daoProxy.pendingVetoer(), address(0x0)); + } + + function test_burnVetoPower_failsIfNotVetoer() public { + vm.expectRevert('NounsDAO::_burnVetoPower: vetoer only'); + daoProxy._burnVetoPower(); + } + + function test_burnVetoPower_setsVetoerToZero() public { + vm.prank(vetoer); + vm.expectEmit(true, true, true, true); + emit NewVetoer(vetoer, address(0)); + daoProxy._burnVetoPower(); + + assertEq(daoProxy.vetoer(), address(0)); + } + + function test_burnVetoPower_setsPendingVetoerToZero() public { + address pendingVetoer = address(0x3333); + + vm.prank(vetoer); + daoProxy._setPendingVetoer(pendingVetoer); + + vm.prank(vetoer); + vm.expectEmit(true, true, true, true); + emit NewPendingVetoer(pendingVetoer, address(0)); + daoProxy._burnVetoPower(); + + vm.prank(pendingVetoer); + vm.expectRevert(NounsDAOV3Admin.PendingVetoerOnly.selector); + daoProxy._acceptVetoer(); + + assertEq(daoProxy.pendingVetoer(), address(0)); + } + + function deployDAOProxy( + address timelock, + address nounsToken, + address vetoer + ) internal override returns (INounsDAOShared) { + return _createDAOV3Proxy(timelock, nounsToken, vetoer); + } + + function daoVersion() internal pure override returns (uint256) { + return 3; + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol index 9f55d03c92..7bc60660dc 100644 --- a/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol +++ b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol @@ -51,4 +51,10 @@ interface INounsDAOShared { function _setQuorumVotesBPS(uint256 quorumVotesBPS_) external; function _burnVetoPower() external; + + function _setPendingVetoer(address pendingVetoer_) external; + + function pendingVetoer() external view returns (address); + + function _acceptVetoer() external; } From 1007958d5411606935dbc8fb2b859ba464c57768 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Fri, 10 Nov 2023 15:08:04 -0500 Subject: [PATCH 24/28] actually delete the moved veto test --- .../test/foundry/NounsDAOLogicState.t.sol | 276 ------------------ 1 file changed, 276 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicState.t.sol b/packages/nouns-contracts/test/foundry/NounsDAOLogicState.t.sol index 7e2d81e249..2fb60197f2 100644 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicState.t.sol +++ b/packages/nouns-contracts/test/foundry/NounsDAOLogicState.t.sol @@ -207,279 +207,3 @@ contract NounsDAOLogicV3StateTest is NounsDAOLogicStateBaseTest { return 3; } } - -contract NounsDAOLogicV3VetoTest is NounsDAOLogicSharedBaseTest { - event NewPendingVetoer(address oldPendingVetoer, address newPendingVetoer); - event NewVetoer(address oldVetoer, address newVetoer); - - function setUp() public override { - super.setUp(); - - mint(proposer, 1); - - vm.roll(block.number + 1); - } - - function testVetoerSetAsExpected() public { - assertEq(daoProxy.vetoer(), vetoer); - } - - function test_burnVetoPower_revertsForNonVetoer() public { - vm.expectRevert('NounsDAO::_burnVetoPower: vetoer only'); - daoProxy._burnVetoPower(); - } - - function test_burnVetoPower_worksForVetoer() public { - assertEq(daoProxy.vetoer(), vetoer); - - vm.prank(vetoer); - daoProxy._burnVetoPower(); - - assertEq(daoProxy.vetoer(), address(0)); - } - - function test_veto_revertsForNonVetoer() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - - if (daoVersion() == 1) { - vm.expectRevert('NounsDAO::veto: only vetoer'); - } else { - vm.expectRevert(NounsDAOLogicV2.VetoerOnly.selector); - } - daoProxy.veto(proposalId); - } - - function test_veto_revertsWhenVetoerIsBurned() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.startPrank(vetoer); - daoProxy._burnVetoPower(); - - if (daoVersion() == 1) { - vm.expectRevert('NounsDAO::veto: veto power burned'); - } else { - vm.expectRevert(NounsDAOLogicV2.VetoerBurned.selector); - } - daoProxy.veto(proposalId); - - vm.stopPrank(); - } - - function test_veto_worksForPropStatePending() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - // Need to roll one block because in V3 on the proposal creation block the state is Updatable - vm.roll(block.number + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Pending); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_worksForPropStateActive() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Active); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_worksForPropStateCanceled() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.prank(proposer); - daoProxy.cancel(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Canceled); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_worksForPropStateDefeated() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vm.prank(proposer); - daoProxy.castVote(proposalId, 0); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Defeated); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_worksForPropStateSucceeded() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vm.prank(proposer); - daoProxy.castVote(proposalId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Succeeded); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_worksForPropStateQueued() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vm.prank(proposer); - daoProxy.castVote(proposalId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - daoProxy.queue(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Queued); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_worksForPropStateExpired() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vm.prank(proposer); - daoProxy.castVote(proposalId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - daoProxy.queue(proposalId); - vm.warp(block.timestamp + timelock.delay() + timelock.GRACE_PERIOD() + 1); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Expired); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_revertsForPropStateExecuted() public { - vm.deal(address(timelock), 100); - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.roll(block.number + daoProxy.votingDelay() + 1); - vm.prank(proposer); - daoProxy.castVote(proposalId, 1); - vm.roll(block.number + daoProxy.votingPeriod() + 1); - daoProxy.queue(proposalId); - vm.warp(block.timestamp + timelock.delay() + 1); - daoProxy.execute(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Executed); - - vm.prank(vetoer); - if (daoVersion() == 1) { - vm.expectRevert('NounsDAO::veto: cannot veto executed proposal'); - } else { - vm.expectRevert(NounsDAOLogicV2.CantVetoExecutedProposal.selector); - } - daoProxy.veto(proposalId); - } - - function test_veto_worksForPropStateVetoed() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - vm.prank(vetoer); - daoProxy.veto(proposalId); - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - - vm.prank(vetoer); - daoProxy.veto(proposalId); - - assertTrue(daoProxy.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_veto_worksForPropStateUpdatable() public { - uint256 proposalId = propose(address(0x1234), 100, '', ''); - NounsDAOLogicV3 daoAsV3 = NounsDAOLogicV3(payable(address(daoProxy))); - - assertTrue(daoAsV3.state(proposalId) == NounsDAOStorageV3.ProposalState.Updatable); - - vm.prank(vetoer); - daoAsV3.veto(proposalId); - - assertTrue(daoAsV3.state(proposalId) == NounsDAOStorageV3.ProposalState.Vetoed); - } - - function test_setPendingVetoer_failsIfNotCurrentVetoer() public { - vm.expectRevert(NounsDAOLogicV2.VetoerOnly.selector); - daoProxyAsV2()._setPendingVetoer(address(0x1234)); - } - - function test_setPendingVetoer_updatePendingVetoer() public { - assertEq(daoProxyAsV2().pendingVetoer(), address(0)); - - address pendingVetoer = address(0x3333); - - vm.prank(vetoer); - vm.expectEmit(true, true, true, true); - emit NewPendingVetoer(address(0), pendingVetoer); - daoProxyAsV2()._setPendingVetoer(pendingVetoer); - - assertEq(daoProxyAsV2().pendingVetoer(), pendingVetoer); - } - - function test_onlyPendingVetoerCanAcceptNewVetoer() public { - address pendingVetoer = address(0x3333); - - vm.prank(vetoer); - daoProxyAsV2()._setPendingVetoer(pendingVetoer); - - vm.expectRevert(NounsDAOLogicV2.PendingVetoerOnly.selector); - daoProxyAsV2()._acceptVetoer(); - - vm.prank(pendingVetoer); - vm.expectEmit(true, true, true, true); - emit NewVetoer(vetoer, pendingVetoer); - daoProxyAsV2()._acceptVetoer(); - - assertEq(daoProxy.vetoer(), pendingVetoer); - assertEq(daoProxyAsV2().pendingVetoer(), address(0x0)); - } - - function test_burnVetoPower_failsIfNotVetoer() public { - vm.expectRevert('NounsDAO::_burnVetoPower: vetoer only'); - daoProxy._burnVetoPower(); - } - - function test_burnVetoPower_setsVetoerToZero() public { - vm.prank(vetoer); - vm.expectEmit(true, true, true, true); - emit NewVetoer(vetoer, address(0)); - daoProxy._burnVetoPower(); - - assertEq(daoProxy.vetoer(), address(0)); - } - - function test_burnVetoPower_setsPendingVetoerToZero() public { - address pendingVetoer = address(0x3333); - - vm.prank(vetoer); - daoProxyAsV2()._setPendingVetoer(pendingVetoer); - - vm.prank(vetoer); - vm.expectEmit(true, true, true, true); - emit NewPendingVetoer(pendingVetoer, address(0)); - daoProxy._burnVetoPower(); - - vm.prank(pendingVetoer); - vm.expectRevert(NounsDAOLogicV2.PendingVetoerOnly.selector); - daoProxyAsV2()._acceptVetoer(); - - assertEq(daoProxyAsV2().pendingVetoer(), address(0)); - } - - function deployDAOProxy( - address timelock, - address nounsToken, - address vetoer - ) internal override returns (INounsDAOShared) { - return _createDAOV3Proxy(timelock, nounsToken, vetoer); - } - - function daoVersion() internal pure override returns (uint256) { - return 3; - } -} From 7efa1199bf127f6b279a922f9b06d5e88c6f9f0a Mon Sep 17 00:00:00 2001 From: eladmallel Date: Mon, 13 Nov 2023 13:49:56 -0500 Subject: [PATCH 25/28] remove unused file --- .../test/foundry/NounsDAOLogicV3/proposal-description.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/nouns-contracts/test/foundry/NounsDAOLogicV3/proposal-description.txt diff --git a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/proposal-description.txt b/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/proposal-description.txt deleted file mode 100644 index 720d40dd35..0000000000 --- a/packages/nouns-contracts/test/foundry/NounsDAOLogicV3/proposal-description.txt +++ /dev/null @@ -1 +0,0 @@ -upgrade to v3 \ No newline at end of file From 2d11504f067b087684b4adc69925895ae0be0e0a Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 14 Nov 2023 14:06:08 -0500 Subject: [PATCH 26/28] add upgrade fork test --- .../DAOV3p1/DeployDAOV3LogicMainnet.s.sol | 16 ++++ .../ProposeDAOV3p1UpgradeMainnet.s.sol | 40 ++++++++ .../UpgradeToDAOV3p1MainnetFork.t.sol | 93 +++++++++++++++++++ .../DAOUpgradeTo3p1/proposal-description.txt | 1 + .../test/foundry/helpers/INounsDAOShared.sol | 4 + 5 files changed, 154 insertions(+) create mode 100644 packages/nouns-contracts/script/DAOV3p1/DeployDAOV3LogicMainnet.s.sol create mode 100644 packages/nouns-contracts/script/DAOV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol create mode 100644 packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol create mode 100644 packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/proposal-description.txt diff --git a/packages/nouns-contracts/script/DAOV3p1/DeployDAOV3LogicMainnet.s.sol b/packages/nouns-contracts/script/DAOV3p1/DeployDAOV3LogicMainnet.s.sol new file mode 100644 index 0000000000..514d3a0136 --- /dev/null +++ b/packages/nouns-contracts/script/DAOV3p1/DeployDAOV3LogicMainnet.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; +import { NounsDAOLogicV3 } from '../../contracts/governance/NounsDAOLogicV3.sol'; + +contract DeployDAOV3LogicMainnet is Script { + function run() public returns (NounsDAOLogicV3 daoLogic) { + uint256 deployerKey = vm.envUint('DEPLOYER_PRIVATE_KEY'); + vm.startBroadcast(deployerKey); + + daoLogic = new NounsDAOLogicV3(); + + vm.stopBroadcast(); + } +} diff --git a/packages/nouns-contracts/script/DAOV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol b/packages/nouns-contracts/script/DAOV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol new file mode 100644 index 0000000000..aa51ad7fdd --- /dev/null +++ b/packages/nouns-contracts/script/DAOV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import 'forge-std/Script.sol'; + +interface NounsDAO { + function propose( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas, + string memory description + ) external returns (uint256); +} + +contract ProposeDAOV3p1UpgradeMainnet is Script { + NounsDAO public constant NOUNS_DAO_PROXY_MAINNET = NounsDAO(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d); + + function run() public returns (uint256 proposalId) { + uint256 proposerKey = vm.envUint('PROPOSER_KEY'); + address daoV3Implementation = vm.envAddress('DAO_V3_IMPL'); + string memory description = vm.readFile(vm.envString('PROPOSAL_DESCRIPTION_FILE')); + + address[] memory targets = new address[](1); + uint256[] memory values = new uint256[](1); + string[] memory signatures = new string[](1); + bytes[] memory calldatas = new bytes[](1); + + vm.startBroadcast(proposerKey); + + targets[0] = address(NOUNS_DAO_PROXY_MAINNET); + values[0] = 0; + signatures[0] = '_setImplementation(address)'; + calldatas[0] = abi.encode(daoV3Implementation); + + proposalId = NOUNS_DAO_PROXY_MAINNET.propose(targets, values, signatures, calldatas, description); + + vm.stopBroadcast(); + } +} diff --git a/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol b/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol new file mode 100644 index 0000000000..1b148c7c4a --- /dev/null +++ b/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol @@ -0,0 +1,93 @@ +// 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 { DeployDAOV3LogicMainnet } from '../../../script/DAOV3p1/DeployDAOV3LogicMainnet.s.sol'; +import { ProposeDAOV3p1UpgradeMainnet } from '../../../script/DAOV3p1/ProposeDAOV3p1UpgradeMainnet.s.sol'; +import { NounsToken } from '../../../contracts/NounsToken.sol'; +import { INounsDAOShared } from '../helpers/INounsDAOShared.sol'; +import { NounsDAOStorageV3 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; + +contract UpgradeToDAOV3p1MainnetForkTest is Test { + address public constant NOUNDERS = 0x2573C60a6D127755aA2DC85e342F7da2378a0Cc5; + address public constant WHALE = 0x83fCFe8Ba2FEce9578F0BbaFeD4Ebf5E915045B9; + NounsToken public nouns = NounsToken(0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03); + INounsDAOShared public constant NOUNS_DAO_PROXY_MAINNET = + INounsDAOShared(0x6f3E6272A167e8AcCb32072d08E0957F9c79223d); + address public constant CURRENT_DAO_IMPL = 0xdD1492570beb290a2f309541e1fDdcaAA3f00B61; + + address proposerAddr = vm.addr(0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb); + address newLogic; + + function setUp() public { + vm.createSelectFork(vm.envString('RPC_MAINNET'), 18571818); + + // Deploy the latest DAO logic + vm.setEnv('DEPLOYER_PRIVATE_KEY', '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + newLogic = address(new DeployDAOV3LogicMainnet().run()); + + // Get votes + vm.prank(NOUNDERS); + nouns.delegate(proposerAddr); + vm.roll(block.number + 1); + + // Propose the upgrade + vm.setEnv('PROPOSER_KEY', '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'); + vm.setEnv('DAO_V3_IMPL', Strings.toHexString(uint160(newLogic), 20)); + vm.setEnv('PROPOSAL_DESCRIPTION_FILE', 'test/foundry/DAOUpgradeTo3p1/proposal-description.txt'); + uint256 proposalId = new ProposeDAOV3p1UpgradeMainnet().run(); + + // Execute the upgrade + voteAndExecuteProposal(proposalId); + } + + function test_daoUpgradeWorked() public { + assertTrue(CURRENT_DAO_IMPL != NOUNS_DAO_PROXY_MAINNET.implementation()); + assertEq(newLogic, NOUNS_DAO_PROXY_MAINNET.implementation()); + } + + function test_proposalExecutesAfterTheUpgrade() public { + uint256 balanceBefore = WHALE.balance; + + uint256 proposalId = propose(WHALE, 1 ether, '', ''); + voteAndExecuteProposal(proposalId); + + assertEq(balanceBefore + 1 ether, WHALE.balance); + } + + function propose( + address target, + uint256 value, + string memory signature, + bytes memory data + ) internal returns (uint256 proposalId) { + vm.prank(proposerAddr); + 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 = NOUNS_DAO_PROXY_MAINNET.propose(targets, values, signatures, calldatas, 'my proposal'); + } + + function voteAndExecuteProposal(uint256 proposalId) internal { + NounsDAOStorageV3.ProposalCondensed memory propInfo = NOUNS_DAO_PROXY_MAINNET.proposalsV3(proposalId); + + vm.roll(propInfo.startBlock + 1); + vm.prank(proposerAddr); + NOUNS_DAO_PROXY_MAINNET.castVote(proposalId, 1); + vm.prank(WHALE); + NOUNS_DAO_PROXY_MAINNET.castVote(proposalId, 1); + + vm.roll(propInfo.endBlock + 1); + NOUNS_DAO_PROXY_MAINNET.queue(proposalId); + + propInfo = NOUNS_DAO_PROXY_MAINNET.proposalsV3(proposalId); + vm.warp(propInfo.eta + 1); + NOUNS_DAO_PROXY_MAINNET.execute(proposalId); + } +} diff --git a/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/proposal-description.txt b/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/proposal-description.txt new file mode 100644 index 0000000000..1dcc156492 --- /dev/null +++ b/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/proposal-description.txt @@ -0,0 +1 @@ +proposal description placeholder \ No newline at end of file diff --git a/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol index 7bc60660dc..c52f2c32be 100644 --- a/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol +++ b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol @@ -57,4 +57,8 @@ interface INounsDAOShared { function pendingVetoer() external view returns (address); function _acceptVetoer() external; + + function proposalsV3(uint256 proposalId) external view returns (NounsDAOStorageV3.ProposalCondensed memory); + + function implementation() external view returns (address); } From 69bb3e766562e9eb3fd79ca8130c319a2209bb3f Mon Sep 17 00:00:00 2001 From: eladmallel Date: Tue, 14 Nov 2023 14:16:17 -0500 Subject: [PATCH 27/28] fix build --- .github/workflows/contracts.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contracts.yaml b/.github/workflows/contracts.yaml index 268e704a38..61aeae3ac1 100644 --- a/.github/workflows/contracts.yaml +++ b/.github/workflows/contracts.yaml @@ -50,4 +50,4 @@ jobs: - name: Run Forge tests run: | cd packages/nouns-contracts - forge test -vvv --ffi --nmc 'ForkMainnetTest' + forge test -vvv --ffi --nmc 'MainnetForkTest' From 2a943caaf739d391ec16110fd058517d87cc2da7 Mon Sep 17 00:00:00 2001 From: eladmallel Date: Wed, 15 Nov 2023 13:17:51 -0500 Subject: [PATCH 28/28] add refund test before and after the upgrade --- .../UpgradeToDAOV3p1MainnetFork.t.sol | 88 ++++++++++++------- .../test/foundry/helpers/INounsDAOShared.sol | 2 + 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol b/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol index 1b148c7c4a..b158b2e3a8 100644 --- a/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol +++ b/packages/nouns-contracts/test/foundry/DAOUpgradeTo3p1/UpgradeToDAOV3p1MainnetFork.t.sol @@ -9,7 +9,7 @@ import { NounsToken } from '../../../contracts/NounsToken.sol'; import { INounsDAOShared } from '../helpers/INounsDAOShared.sol'; import { NounsDAOStorageV3 } from '../../../contracts/governance/NounsDAOInterfaces.sol'; -contract UpgradeToDAOV3p1MainnetForkTest is Test { +abstract contract UpgradeToDAOV3p1MainnetForkBaseTest is Test { address public constant NOUNDERS = 0x2573C60a6D127755aA2DC85e342F7da2378a0Cc5; address public constant WHALE = 0x83fCFe8Ba2FEce9578F0BbaFeD4Ebf5E915045B9; NounsToken public nouns = NounsToken(0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03); @@ -18,42 +18,20 @@ contract UpgradeToDAOV3p1MainnetForkTest is Test { address public constant CURRENT_DAO_IMPL = 0xdD1492570beb290a2f309541e1fDdcaAA3f00B61; address proposerAddr = vm.addr(0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb); + address origin = makeAddr('origin'); address newLogic; - function setUp() public { + function setUp() public virtual { vm.createSelectFork(vm.envString('RPC_MAINNET'), 18571818); - // Deploy the latest DAO logic - vm.setEnv('DEPLOYER_PRIVATE_KEY', '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); - newLogic = address(new DeployDAOV3LogicMainnet().run()); - // Get votes vm.prank(NOUNDERS); nouns.delegate(proposerAddr); vm.roll(block.number + 1); - // Propose the upgrade - vm.setEnv('PROPOSER_KEY', '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'); - vm.setEnv('DAO_V3_IMPL', Strings.toHexString(uint160(newLogic), 20)); - vm.setEnv('PROPOSAL_DESCRIPTION_FILE', 'test/foundry/DAOUpgradeTo3p1/proposal-description.txt'); - uint256 proposalId = new ProposeDAOV3p1UpgradeMainnet().run(); - - // Execute the upgrade - voteAndExecuteProposal(proposalId); - } - - function test_daoUpgradeWorked() public { - assertTrue(CURRENT_DAO_IMPL != NOUNS_DAO_PROXY_MAINNET.implementation()); - assertEq(newLogic, NOUNS_DAO_PROXY_MAINNET.implementation()); - } - - function test_proposalExecutesAfterTheUpgrade() public { - uint256 balanceBefore = WHALE.balance; - - uint256 proposalId = propose(WHALE, 1 ether, '', ''); - voteAndExecuteProposal(proposalId); - - assertEq(balanceBefore + 1 ether, WHALE.balance); + vm.deal(address(NOUNS_DAO_PROXY_MAINNET), 100 ether); + vm.fee(50 gwei); + vm.txGasPrice(50 gwei); } function propose( @@ -78,10 +56,10 @@ contract UpgradeToDAOV3p1MainnetForkTest is Test { NounsDAOStorageV3.ProposalCondensed memory propInfo = NOUNS_DAO_PROXY_MAINNET.proposalsV3(proposalId); vm.roll(propInfo.startBlock + 1); - vm.prank(proposerAddr); - NOUNS_DAO_PROXY_MAINNET.castVote(proposalId, 1); - vm.prank(WHALE); - NOUNS_DAO_PROXY_MAINNET.castVote(proposalId, 1); + vm.prank(proposerAddr, origin); + NOUNS_DAO_PROXY_MAINNET.castRefundableVote(proposalId, 1); + vm.prank(WHALE, origin); + NOUNS_DAO_PROXY_MAINNET.castRefundableVote(proposalId, 1); vm.roll(propInfo.endBlock + 1); NOUNS_DAO_PROXY_MAINNET.queue(proposalId); @@ -91,3 +69,49 @@ contract UpgradeToDAOV3p1MainnetForkTest is Test { NOUNS_DAO_PROXY_MAINNET.execute(proposalId); } } + +contract RefundBeforeTheUpgradeTo3p1MainnetForkTest is UpgradeToDAOV3p1MainnetForkBaseTest { + function test_refundBeforeUpgrade_doesNotRefundOrigin() public { + uint256 originBalanceBefore = origin.balance; + + uint256 proposalId = propose(WHALE, 1 ether, '', ''); + voteAndExecuteProposal(proposalId); + + assertEq(originBalanceBefore, origin.balance); + } +} + +contract UpgradeToDAOV3p1MainnetForkTest is UpgradeToDAOV3p1MainnetForkBaseTest { + function setUp() public override { + super.setUp(); + + // Deploy the latest DAO logic + vm.setEnv('DEPLOYER_PRIVATE_KEY', '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + newLogic = address(new DeployDAOV3LogicMainnet().run()); + + // Propose the upgrade + vm.setEnv('PROPOSER_KEY', '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'); + vm.setEnv('DAO_V3_IMPL', Strings.toHexString(uint160(newLogic), 20)); + vm.setEnv('PROPOSAL_DESCRIPTION_FILE', 'test/foundry/DAOUpgradeTo3p1/proposal-description.txt'); + uint256 proposalId = new ProposeDAOV3p1UpgradeMainnet().run(); + + // Execute the upgrade + voteAndExecuteProposal(proposalId); + } + + function test_daoUpgradeWorked() public { + assertTrue(CURRENT_DAO_IMPL != NOUNS_DAO_PROXY_MAINNET.implementation()); + assertEq(newLogic, NOUNS_DAO_PROXY_MAINNET.implementation()); + } + + function test_proposalExecutesAfterTheUpgrade_andRefundGoesToOrigin() public { + uint256 recipientBalanceBefore = WHALE.balance; + uint256 originBalanceBefore = origin.balance; + + uint256 proposalId = propose(WHALE, 1 ether, '', ''); + voteAndExecuteProposal(proposalId); + + assertEq(recipientBalanceBefore + 1 ether, WHALE.balance); + assertGt(origin.balance, originBalanceBefore); + } +} diff --git a/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol index c52f2c32be..a7c35dd189 100644 --- a/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol +++ b/packages/nouns-contracts/test/foundry/helpers/INounsDAOShared.sol @@ -20,6 +20,8 @@ interface INounsDAOShared { function castVote(uint256 proposalId, uint8 support) external; + function castRefundableVote(uint256 proposalId, uint8 support) external; + function castVoteWithReason( uint256 proposalId, uint8 support,