diff --git a/Makefile b/Makefile index d764275..b258324 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,5 @@ bash: docker run --rm -it \ -v $$(pwd):$$(pwd) \ --workdir $$(pwd) \ - -p 8545:8545 \ $(TAG) \ bash diff --git a/src/verification/index.ts b/src/verification/index.ts index 9560a8e..a6532ce 100644 --- a/src/verification/index.ts +++ b/src/verification/index.ts @@ -83,7 +83,7 @@ export async function passGovProposal(contracts: ContractBundle, provider: ether 'Gov proposal unit test' ) await proposalResult.wait() - console.log(`[+] Proposed in hash: ${proposalResult.hash}`) + shouldLogProposal && console.log(`[+] Proposed in hash: ${proposalResult.hash}`) const proposalID = await governor.proposalCount() @@ -91,7 +91,7 @@ export async function passGovProposal(contracts: ContractBundle, provider: ether let proposal = await governor.proposals(proposalID) // Delay until voting begins by waiting until the start time plus one second - console.log("[+] Fast forwarding in time...") + shouldLogProposal && console.log("[+] Fast forwarding in time...") await provider.send("evm_mine", [proposal.startTimestamp.toNumber() + 1]) // Vote for the proposal @@ -103,16 +103,16 @@ export async function passGovProposal(contracts: ContractBundle, provider: ether const voteValueYes = 0 const voteResult = await governor.castVote(proposalID, voteValueYes) await voteResult.wait() - console.log(`[+] Voted for proposal in ${voteResult.hash}`) + shouldLogProposal && console.log(`[+] Voted for proposal in ${voteResult.hash}`) // Delay until voting end by waiting until the end time and mining one more block - console.log("[+] Fast forwarding in time...") + shouldLogProposal && console.log("[+] Fast forwarding in time...") await provider.send("evm_mine", [proposal.endTimestamp.toNumber() + 1]); // Queue our proposal const queueResult = await governor.queue(proposalID) await queueResult.wait() - console.log(`[+] Queued for Execution in Hash: ${queueResult.hash}`) + shouldLogProposal && console.log(`[+] Queued for Execution in Hash: ${queueResult.hash}`) const timelock = contracts.TIMELOCK.contract.connect(provider) @@ -125,7 +125,7 @@ export async function passGovProposal(contracts: ContractBundle, provider: ether const executeResult = await governor.execute(proposalID) await executeResult.wait() - console.log(`[+] Executed in hash: ${executeResult.hash}`) + shouldLogProposal && console.log(`[+] Executed in hash: ${executeResult.hash}`) } export async function addProposalToPropData(contract: Contract, fn: string, args: any[], proposalData: any){ diff --git a/test/apollo/mip-44/assertCurrentExpectedState.ts b/test/apollo/mip-44/assertCurrentExpectedState.ts new file mode 100644 index 0000000..f7a510a --- /dev/null +++ b/test/apollo/mip-44/assertCurrentExpectedState.ts @@ -0,0 +1,23 @@ +import {ethers} from "ethers"; +import {ContractBundle} from "@moonwell-fi/moonwell.js"; +import BigNumber from "bignumber.js"; + +export async function assertCurrentExpectedState(contracts: ContractBundle, provider: ethers.providers.JsonRpcProvider){ + console.log("[+] Asserting market configurations are in an expected state") + + const gov = contracts.GOVERNOR.contract.connect(provider) + + const currentUpperCap = new BigNumber((await gov.upperQuorumCap()).toString()).toFixed() + const currentLowerCap = new BigNumber((await gov.lowerQuorumCap()).toString()).toFixed() + + const EXPECTED_UPPER_QUORUM_CAP = 900_000_000 + const EXPECTED_LOWER_QUORUM_CAP = 10_000_000 + + if (currentUpperCap != new BigNumber(EXPECTED_UPPER_QUORUM_CAP).shiftedBy(18).toFixed()){ + throw new Error("Upper quorum cap is not as expected!") + } + + if (currentLowerCap != new BigNumber(EXPECTED_LOWER_QUORUM_CAP).shiftedBy(18).toFixed()){ + throw new Error("Lower quorum cap is not as expected!") + } +} diff --git a/test/apollo/mip-44/assertExpectedEndState.ts b/test/apollo/mip-44/assertExpectedEndState.ts new file mode 100644 index 0000000..2045d52 --- /dev/null +++ b/test/apollo/mip-44/assertExpectedEndState.ts @@ -0,0 +1,134 @@ +import {ethers} from "ethers"; +import {ContractBundle} from "@moonwell-fi/moonwell.js"; +import { + F_MOVR_GRANT +} from "./vars"; +import { + addProposalToPropData, + assertEndingExpectedGovTokenHoldings, + assertMarketRewardState, passGovProposal +} from "../../../src"; +import {assertDexRewarderRewardsPerSec, assertSTKWellEmissionsPerSecond} from "../../../src/verification/assertions"; +import BigNumber from "bignumber.js"; + +export async function assertExpectedEndState(contracts: ContractBundle, provider: ethers.providers.JsonRpcProvider){ + console.log("[+] Asserting protocol is in an expected state AFTER gov proposal passed") + + const gov = contracts.GOVERNOR.contract.connect(provider) + + const currentUpperCap = new BigNumber((await gov.upperQuorumCap()).toString()).toFixed() + const currentLowerCap = new BigNumber((await gov.lowerQuorumCap()).toString()).toFixed() + + const EXPECTED_UPPER_QUORUM_CAP = 40_000_001 + const EXPECTED_LOWER_QUORUM_CAP = 40_000_000 + + if (currentUpperCap != new BigNumber(EXPECTED_UPPER_QUORUM_CAP).shiftedBy(18).toFixed()){ + throw new Error("Upper quorum cap is not as expected!") + } + + if (currentLowerCap != new BigNumber(EXPECTED_LOWER_QUORUM_CAP).shiftedBy(18).toFixed()){ + throw new Error("Lower quorum cap is not as expected!") + } + + + /* + Go make sure that by adjusting the deployer holdings to QUORUM_CAP_UPPER + 1 we can + still pass proposals + */ + + console.log("\n=== Attempting to pass new proposal with just over quorum cap ===\n") + + // Go return the excess gov tokens we have, getting us to just above the upper quorum cap + const QUORUM_CAP = 40_000_001 + + const govToken = contracts.GOV_TOKEN.contract.connect(provider) + const deployer = await provider.getSigner(0) + const currentBalance = new BigNumber((await govToken.balanceOf(await deployer.getAddress())).toString()) + + const delta = currentBalance.shiftedBy(-18).minus(QUORUM_CAP + 1) + + console.log("[+] Current proposer balance", currentBalance.shiftedBy(-18).toFixed()) + console.log("[+] Current delta", delta.toFixed()) + + await govToken.connect(deployer).transfer(F_MOVR_GRANT, delta.shiftedBy(18).toFixed()) + + const updatedBalance = new BigNumber((await govToken.getCurrentVotes(await deployer.getAddress())).toString()) + console.log("[+] New voting power for proposer", updatedBalance.shiftedBy(-18).toFixed(), 'MFAM') + if (!updatedBalance.shiftedBy(-18).isEqualTo(QUORUM_CAP + 1)){ + throw new Error("Updated holdings aren't correct!") + } + + // Go pass a new proposal with something benign to make sure it executes + const proposalData2: any = { + targets: [], + values: [], + signatures: [], + callDatas: [], + } + const govenor = contracts.GOVERNOR.contract.connect(provider) + await addProposalToPropData( + govenor, + 'sweepTokens', + [contracts.GOV_TOKEN.address, await deployer.getAddress()], + proposalData2 + ) + + await passGovProposal(contracts, provider, proposalData2, 0, false) + + console.log('✅ New `getQuorum()` set to ', new BigNumber((await govenor.getQuorum()).toString()).shiftedBy(-18).toFixed()) + + console.log("\n=== Attempting to pass new proposal with just under quorum cap (should fail) ===\n") + + // Dip voter balance to just below quorum and make sure things fail + await govToken.connect(deployer).transfer(F_MOVR_GRANT, new BigNumber(2).shiftedBy(18).toFixed()) + const proposalData3: any = { + targets: [], + values: [], + signatures: [], + callDatas: [], + } + await addProposalToPropData( + govenor, + 'sweepTokens', + [contracts.GOV_TOKEN.address, await deployer.getAddress()], + proposalData3 + ) + + try { + await passGovProposal(contracts, provider, proposalData3, 0, false) + } catch(e) { + if (!e.toString().includes('GovernorApollo::queue: proposal can only be queued if it is succeeded')){ + console.error('The proposal didn\'t fail as expected!') + throw e + } else { + console.log("✅ Proposal errors out as expected with an unsuccessful voting period when a user has just below quorum cap") + } + } + + // Go make sure that previously failed proposals are also not able to suddenly be queued and are still in the + // DEFEATED state + + const STATE_DEFEATED = 3 + const DEFEATED_PROP_NUMBER = 17 + + const failedPropState = await gov.state(DEFEATED_PROP_NUMBER) + if (!new BigNumber(failedPropState.toString()).isEqualTo(STATE_DEFEATED)){ + throw new Error(`Expected prop ${DEFEATED_PROP_NUMBER} to be in the DEFEATED state, but has state == ${failedPropState.toString()}!`) + } + + try { + await gov.connect(provider.getSigner(0)).queue(DEFEATED_PROP_NUMBER) + } catch(e) { + if (!e.toString().includes('GovernorApollo::queue: proposal can only be queued if it is succeeded')){ + console.error('The proposal didn\'t fail as expected!') + throw e + } else { + console.log("✅ Previously failed proposal is incapable of being queued") + } + } + + // Go make sure that adding 2 more MFAM back to the deployer means they can submit again + await govToken.connect(provider.getSigner(F_MOVR_GRANT)).transfer(await deployer.getAddress(), new BigNumber(2).shiftedBy(18).toFixed()) + await passGovProposal(contracts, provider, proposalData2, 0, false) + console.log('✅ New `getQuorum()` set to ', new BigNumber((await govenor.getQuorum()).toString()).shiftedBy(-18).toFixed()) +} \ No newline at end of file diff --git a/test/apollo/mip-44/generateProposalData.ts b/test/apollo/mip-44/generateProposalData.ts new file mode 100644 index 0000000..a9c1ac0 --- /dev/null +++ b/test/apollo/mip-44/generateProposalData.ts @@ -0,0 +1,33 @@ +import {ethers} from "ethers"; +import {ContractBundle} from "@moonwell-fi/moonwell.js"; +import {addProposalToPropData} from "../../../src"; +import BigNumber from "bignumber.js"; + +export async function generateProposalData(contracts: ContractBundle, provider: ethers.providers.JsonRpcProvider){ + const proposalData: any = { + targets: [], + values: [], + signatures: [], + callDatas: [], + } + + console.log("[+] Constructing Proposal...") + + // Lock both caps within 1 vote of eachother + const NEW_UPPER_CAP = new BigNumber(40_000_001).shiftedBy(18) + const NEW_LOWER_CAP = new BigNumber(40_000_000).shiftedBy(18) + + const governor = contracts.GOVERNOR.contract.connect(provider) + + console.log(` ✅ Setting quorum caps to ${NEW_LOWER_CAP.shiftedBy(-18).toFixed()} (lower) / ${NEW_UPPER_CAP.shiftedBy(-18).toFixed()} (upper)`) + // function setQuorumCaps(uint newLowerQuorumCap, uint newUpperQuorumCap) external + await addProposalToPropData(governor, 'setQuorumCaps', + [ + NEW_LOWER_CAP.toFixed(), + NEW_UPPER_CAP.toFixed(), + ], + proposalData + ) + + return proposalData +} \ No newline at end of file diff --git a/test/apollo/mip-44/mip-44-verification.ts b/test/apollo/mip-44/mip-44-verification.ts new file mode 100644 index 0000000..791c816 --- /dev/null +++ b/test/apollo/mip-44/mip-44-verification.ts @@ -0,0 +1,57 @@ +import {ethers} from 'ethers' +import { + addProposalToPropData, + passGovProposal, + setupDeployerAndEnvForGovernance, + sleep, + startGanache +} from "../../../src"; + +import {Contracts} from '@moonwell-fi/moonwell.js' +import {generateProposalData} from "./generateProposalData"; +import {assertCurrentExpectedState} from "./assertCurrentExpectedState"; +import {assertExpectedEndState} from "./assertExpectedEndState"; +import {F_MOVR_GRANT} from "./vars"; + +const FORK_BLOCK = 4_014_666 + +test("mip-44-verification", async () => { + const contracts = Contracts.moonriver + + const forkedChainProcess = await startGanache(contracts, + FORK_BLOCK, + 'https://rpc.api.moonriver.moonbeam.network', + [F_MOVR_GRANT] + ) + + console.log("Waiting 5 seconds for chain to bootstrap...") + await sleep(5) + + try { + const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545') + + await setupDeployerAndEnvForGovernance( + contracts, + provider, + F_MOVR_GRANT, + FORK_BLOCK, + 5_000_000 // Quorum adjusts up just short of this amount when the proposal is submitted + ) + + await assertCurrentExpectedState(contracts, provider) + + // Generate new proposal data + const proposalData = await generateProposalData(contracts, provider) + + // Pass the proposal + await passGovProposal(contracts, provider, proposalData) + + // Assert that our end state is as desired + await assertExpectedEndState(contracts, provider) + } finally { + // Kill our child chain. + console.log("Shutting down Ganache chain. PID", forkedChainProcess.pid!) + process.kill(-forkedChainProcess.pid!) + console.log("Ganache chain stopped.") + } +}); diff --git a/test/apollo/mip-44/vars.ts b/test/apollo/mip-44/vars.ts new file mode 100644 index 0000000..7147ed5 --- /dev/null +++ b/test/apollo/mip-44/vars.ts @@ -0,0 +1 @@ +export * from '../base-vars'