UNPKG

@gooddollar/goodprotocol

Version:
475 lines (416 loc) 18 kB
/*** * Mainnet: * FIXES: * - prevent hacked funds burnFrom * - set GOOD rewards to 0 * - prevent untrusted contracts in goodfundmanager * - use bonding curve for actual cDAI balance (prevent the "buy" instead of "transferTo" used in hack to trick reserve into minting UBI from interest) * - set exit contribution to 10% * - disable gdx * - fix reserve calculations of expansion/currentprice * - add requirement of guardians to approve on-chain proposals * - reserve should not trust exchange helper * - resere should not trust fundmanager for its starting balance * * PLAN: * - pause staking * - prevent fusebridge usage * - set GOOD rewards to 0 * - blacklist hacked accounts to prevent burn (transfer already blocked done via tax) * - withdraw funds from fuse * - transfer to MPB bridge * - upgrade reserve * - set new reserve ratio, supply(minus hacked funds) and reserve * - set contribution to 10% * - unpause reserve * - upgrade exchangeHelper * - upgrade goodfundmanager * - upgrade governance * - unpause reserve * - unpause goodfundmanager * - switch fuse distribution to use lz bridge insted of deprecated fuse bridge * * * Fuse: * PLAN: * - prevent old fuse bridge usage * - upgrade governance * **/ import { network, ethers } from "hardhat"; import { reset, time } from "@nomicfoundation/hardhat-network-helpers"; import { defaultsDeep, last } from "lodash"; import prompt from "prompt"; // import mpbDeployments from "@gooddollar/bridge-contracts/release/mpb.json" import { executeViaGuardian, executeViaSafe, verifyProductionSigner } from "../multichain-deploy/helpers"; import ProtocolSettings from "../../releases/deploy-settings.json"; import dao from "../../releases/deployment.json"; import { ExchangeHelper, FuseOldBridgeKill, GoodFundManager, GoodMarketMaker, GoodReserveCDai, IGoodDollar, ReserveRestore } from "../../types"; let { name: networkName } = network; const isSimulation = network.name === "hardhat" || network.name === "fork" || network.name === "localhost"; // hacker and hacked multichain bridge accounts const LOCKED_ACCOUNTS = [ "0xeC577447D314cf1e443e9f4488216651450DBE7c", "0xD17652350Cfd2A37bA2f947C910987a3B1A1c60d", "0x6738fA889fF31F82d9Fe8862ec025dbE318f3Fde" ]; const INITIAL_DAI = ethers.utils.parseEther("100000"); // 100k // reserve funder (goodlabs safe) const funder = "0xF0652a820dd39EC956659E0018Da022132f2f40a"; export const upgradeMainnet = async network => { const isProduction = networkName.includes("production"); let [root, ...signers] = await ethers.getSigners(); if (isProduction) verifyProductionSigner(root); let guardian = root; //simulate produciton on fork if (isSimulation) { networkName = "production-mainnet"; } let release: { [key: string]: any } = dao[networkName]; let protocolSettings = defaultsDeep({}, ProtocolSettings[networkName], ProtocolSettings["default"]); //simulate on fork, make sure safe has enough eth to simulate txs if (isSimulation) { await reset("https://eth.drpc.org"); guardian = await ethers.getImpersonatedSigner(protocolSettings.guardiansSafe); await root.sendTransaction({ value: ethers.utils.parseEther("1"), to: protocolSettings.guardiansSafe }); } const rootBalance = await ethers.provider.getBalance(root.address).then(_ => _.toString()); const guardianBalance = await ethers.provider.getBalance(guardian.address).then(_ => _.toString()); console.log("got signers:", { networkName, root: root.address, guardian: guardian.address, balance: rootBalance, guardianBalance: guardianBalance }); const reserveImpl = await ethers.deployContract("GoodReserveCDai"); const goodFundManagerImpl = await ethers.deployContract("GoodFundManager"); const exchangeHelperImpl = await ethers.deployContract("ExchangeHelper"); const stakersDistImpl = await ethers.deployContract("StakersDistribution"); const govImpl = await ethers.deployContract("CompoundVotingMachine"); const distHelperImplt = await ethers.deployContract("DistributionHelper"); const marketMakerImpl = await ethers.deployContract("GoodMarketMaker"); const upgradeImpl = (await ethers.deployContract("ReserveRestore", [release.NameService])) as ReserveRestore; const gd = (await ethers.getContractAt("IGoodDollar", release.GoodDollar)) as IGoodDollar; // test blacklisting to prevent burn by hacker if (isSimulation) { const locked = await ethers.getImpersonatedSigner(LOCKED_ACCOUNTS[0]); const tx = await gd .connect(locked) .burn("10") .then(_ => _.wait()) .then(_ => _.status) .catch(e => e); console.log("Burn tx before:", tx); const funderSigner = await ethers.getImpersonatedSigner(funder); const dai = await ethers.getContractAt("IGoodDollar", release.DAI); await dai.connect(funderSigner).approve(upgradeImpl.address, ethers.utils.parseEther("200000")); const whale = await ethers.getImpersonatedSigner("0xa359Fc83C48277EedF375a5b6DC9Ec7D093aD3f2"); await dai.connect(whale).transfer(root.address, ethers.utils.parseEther("100000")); const lockedFunds = await Promise.all(LOCKED_ACCOUNTS.map(_ => gd.balanceOf(_))); const totalLocked = lockedFunds.reduce((acc, cur) => acc.add(cur), ethers.constants.Zero); console.log({ totalLocked }); } const startSupply = await gd.totalSupply(); console.log("executing proposals"); const proposalContracts = [ release.StakingContractsV3[0][0], // pause staking release.StakingContractsV3[1][0], // pause staking release.StakersDistribution, //set GOOD rewards to 0 release.GoodReserveCDai, //expansion ratio release.ForeignBridge, // prevent from using release.Identity, // set locked G$ accounts as blacklisted so cant do burn from release.Identity, // set locked G$ accounts as blacklisted so cant do burn from release.Identity, // set locked G$ accounts as blacklisted so cant do burnfrom release.ForeignBridge, // claim bridge tokens to mpb bridge release.GoodReserveCDai, //upgrade reserve release.GoodFundManager, //upgrade fundmanager release.ExchangeHelper, //upgrade exchangehelper release.DistributionHelper, //upgrade disthelper release.StakersDistribution, //upgrade stakers dist release.GoodMarketMaker, //upgrade mm release.CompoundVotingMachine, // upgrade gov release.DistributionHelper, // switch to lz bridge for fuse release.ExchangeHelper, // activate upgrade changes release.Controller, // upgradeImpl.address, release.GoodReserveCDai ]; const proposalEthValues = proposalContracts.map(_ => 0); const proposalFunctionSignatures = [ "pause(bool)", "pause(bool)", "setMonthlyReputationDistribution(uint256)", "setReserveRatioDailyExpansion(uint256,uint256)", "setExecutionDailyLimit(uint256)", // set limit to 0 so old bridge cant be used "addBlacklisted(address)", "addBlacklisted(address)", "addBlacklisted(address)", "claimTokens(address,address)", "upgradeTo(address)", "upgradeTo(address)", "upgradeTo(address)", "upgradeTo(address)", "upgradeTo(address)", "upgradeTo(address)", "upgradeTo(address)", "addOrUpdateRecipient((uint32,uint32,address,uint8))", "setAddresses()", "registerScheme(address,bytes32,bytes4,address)", // give upgrade contract permissions "grantRole(bytes32,address)" ]; const proposalFunctionInputs = [ ethers.utils.defaultAbiCoder.encode(["bool"], [true]), ethers.utils.defaultAbiCoder.encode(["bool"], [true]), ethers.utils.defaultAbiCoder.encode(["uint256"], [0]), ethers.utils.defaultAbiCoder.encode(["uint256", "uint256"], [999711382710978, 1e15]), ethers.utils.defaultAbiCoder.encode(["uint256"], [0]), ethers.utils.defaultAbiCoder.encode(["address"], [LOCKED_ACCOUNTS[0]]), ethers.utils.defaultAbiCoder.encode(["address"], [LOCKED_ACCOUNTS[1]]), ethers.utils.defaultAbiCoder.encode(["address"], [LOCKED_ACCOUNTS[2]]), ethers.utils.defaultAbiCoder.encode(["address", "address"], [release.GoodDollar, release.MpbBridge]), ethers.utils.defaultAbiCoder.encode(["address"], [reserveImpl.address]), ethers.utils.defaultAbiCoder.encode(["address"], [goodFundManagerImpl.address]), ethers.utils.defaultAbiCoder.encode(["address"], [exchangeHelperImpl.address]), ethers.utils.defaultAbiCoder.encode(["address"], [distHelperImplt.address]), ethers.utils.defaultAbiCoder.encode(["address"], [stakersDistImpl.address]), ethers.utils.defaultAbiCoder.encode(["address"], [marketMakerImpl.address]), ethers.utils.defaultAbiCoder.encode(["address"], [govImpl.address]), ethers.utils.defaultAbiCoder.encode( ["uint32", "uint32", "address", "uint8"], [1000, 122, dao["production"].UBIScheme, 1] //10% chainId 122 ubischeme 1-lz bridge ), "0x", //setAddresses ethers.utils.defaultAbiCoder.encode( ["address", "bytes32", "bytes4", "address"], [ upgradeImpl.address, //scheme ethers.constants.HashZero, //paramshash "0x000000f1", //permissions - minimal release.Avatar ] ), ethers.utils.defaultAbiCoder.encode( ["bytes32", "address"], [ "0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a", //pauser role release.Avatar ] ) ]; if (isProduction) { await executeViaSafe( proposalContracts, proposalEthValues, proposalFunctionSignatures, proposalFunctionInputs, protocolSettings.guardiansSafe, "mainnet" ); } else { //simulation or dev envs await executeViaGuardian( proposalContracts, proposalEthValues, proposalFunctionSignatures, proposalFunctionInputs, guardian, networkName ); } if (isSimulation) { await mainnetPostChecks(upgradeImpl); } }; const mainnetPostChecks = async (upgradeImpl: ReserveRestore) => { networkName = "production-mainnet"; let release: { [key: string]: any } = dao[networkName]; let [root, ...signers] = await ethers.getSigners(); const gd = await ethers.getContractAt("IGoodDollar", release.GoodDollar); //execute the reserve initialization (await upgradeImpl.upgrade(funder, INITIAL_DAI)).wait(); const locked = await ethers.getImpersonatedSigner(LOCKED_ACCOUNTS[0]); const tx = await gd .connect(locked) .burn("10", { maxFeePerGas: 30e9, maxPriorityFeePerGas: 1e9, gasLimit: 200000 }) .then(_ => _.wait()) .then(_ => _.status) .catch(e => e); console.log("Burn tx after should fail:", tx); const dai = await ethers.getContractAt("IGoodDollar", release.DAI); const cdai = await ethers.getContractAt("IGoodDollar", release.cDAI); const reserve = (await ethers.getContractAt("GoodReserveCDai", release.GoodReserveCDai)) as GoodReserveCDai; const mm = (await ethers.getContractAt("GoodMarketMaker", release.GoodMarketMaker)) as GoodMarketMaker; const newExpansion = await mm.reserveRatioDailyExpansion(); console.log( "new expansion set:", newExpansion, newExpansion.mul(1e15).div(ethers.utils.parseEther("1000000000")).toNumber() / 1e15 === 0.999711382710978 ); console.log( "discount should be disabled:", await reserve.discountDisabled(), " gdx should be disabled:", await reserve.gdxDisabled() ); const resereState = await mm.reserveTokens(release.cDAI); console.log({ resereState }); const finalSupply = await gd.totalSupply(); const distHelper = await ethers.getContractAt("DistributionHelper", release.DistributionHelper); const result = await distHelper.calcGDToSell(1e9); console.log("how much G$ to sell to cover distribution fees out of 1M:", result.toNumber() / 100); const [cdaiPriceBefore, daiPriceBefore] = await ( await Promise.all([reserve.currentPrice(), reserve.currentPriceDAI()]) ).map(_ => _.toNumber()); console.log({ cdaiPriceBefore, daiPriceBefore }); const dex = (await ethers.getContractAt("ExchangeHelper", release.ExchangeHelper)) as ExchangeHelper; await dai.approve(dex.address, ethers.utils.parseEther("10000")); await dex.buy([release.DAI], ethers.utils.parseEther("10000"), 0, 0, root.address); // check g$ prices const [cdaiPriceAfter, daiPriceAfter] = await ( await Promise.all([reserve.currentPrice(), reserve.currentPriceDAI()]) ).map(_ => _.toNumber()); console.log("prices after buying form reserve with 10k DAI", { cdaiPriceAfter, daiPriceAfter }); await gd.approve(dex.address, await gd.balanceOf(root.address)); await dex.sell([release.DAI], await gd.balanceOf(root.address), 0, 0, root.address); const daiBalanceAfterSell = await dai.balanceOf(root.address); // expect a 10% sell fee console.log("expect 10% sell fee (selling 10K gets only 9K of dai, balance should be ~99K):", { daiBalanceAfterSell }); const cdaiReserveBalance = await cdai.balanceOf(reserve.address); console.log({ cdaiReserveBalance }); const [mpbBalance, fuseBalance] = await Promise.all([ gd.balanceOf(release.MpbBridge), gd.balanceOf(release.ForeignBridge) ]); console.log("fuse bridge should have 0 balance and Mpb should be >6B", { mpbBalance, fuseBalance }); const gfm = (await ethers.getContractAt("GoodFundManager", release.GoodFundManager)) as GoodFundManager; const stakingContracts = await gfm.callStatic.calcSortedContracts(); console.log({ stakingContracts }); const interesTX = await ( await gfm.collectInterest( stakingContracts.map(_ => _[0]), false ) ).wait(); const ubiEvents = last(await reserve.queryFilter(reserve.filters.UBIMinted(), -1)); console.log( "collectinterest gfm events:", interesTX.events?.find(_ => _.event === "FundsTransferred") ); console.log("ubiEvents after collect interest:", ubiEvents); // check expansion after some time await time.increase(365 * 60 * 60 * 24); const gdSupplyBeforeExpansion = await gd.totalSupply(); const reserveStateBeforeYearExpansion = await mm.reserveTokens(release.cDAI); const expansionTX = await (await gfm.collectInterest([], false)).wait(); const ubiExpansionEvents = last(await reserve.queryFilter(reserve.filters.UBIMinted(), -1)); console.log( "gfm events after 1 year expansion:", expansionTX.events?.filter(_ => _.event === "FundsTransferred") ); console.log("ubiEvents after 1 year expansion:", ubiExpansionEvents); const reserveStateAfterYearExpansion = await mm.reserveTokens(release.cDAI); const gdSupplyAfterExpansion = await gd.totalSupply(); console.log({ reserveStateAfterYearExpansion, gdSupplyAfterExpansion, gdSupplyBeforeExpansion, reserveStateBeforeYearExpansion }); //execute the reserve initialization await (await upgradeImpl.donate(funder, INITIAL_DAI)).wait(); const [cdaiPriceAfterDonation, daiPriceAfterDonation] = await ( await Promise.all([reserve.currentPrice(), reserve.currentPriceDAI()]) ).map(_ => _.toNumber()); console.log("price after dai donation:", { cdaiPriceAfterDonation, daiPriceAfterDonation }); const reserveStateAfterDonation = await mm.reserveTokens(release.cDAI); console.log({ reserveStateAfterDonation }); await (await upgradeImpl.end()).wait(); }; export const upgradeFuse = async network => { let [root] = await ethers.getSigners(); const isProduction = networkName.includes("production"); let networkEnv = networkName.split("-")[0]; if (isSimulation) networkEnv = "production"; let release: { [key: string]: any } = dao[networkEnv]; let guardian = root; //simulate on fork, make sure safe has enough eth to simulate txs if (isSimulation) { await reset("https://fuse.liquify.com"); guardian = await ethers.getImpersonatedSigner(release.GuardiansSafe); await root.sendTransaction({ value: ethers.constants.WeiPerEther.mul(3), to: guardian.address }); } const gd = (await ethers.getContractAt("IGoodDollar", release.GoodDollar)) as IGoodDollar; const isMinter = await gd.isMinter(release.HomeBridge); console.log({ networkEnv, guardian: guardian.address, isSimulation, isProduction, isMinter }); const govImpl = await ethers.deployContract("CompoundVotingMachine"); const killBridge = (await ethers.deployContract("FuseOldBridgeKill")) as FuseOldBridgeKill; const proposalContracts = [ release.HomeBridge, // prevent from using by upgrading to empty contract and removing minting rights release.CompoundVotingMachine //upgrade gov ]; const proposalEthValues = proposalContracts.map(_ => 0); const proposalFunctionSignatures = [ "upgradeToAndCall(uint256,address,bytes)", // upgrade and call end "upgradeTo(address)" ]; const proposalFunctionInputs = [ ethers.utils.defaultAbiCoder.encode( ["uint256", "address", "bytes"], [2, killBridge.address, killBridge.interface.encodeFunctionData("end")] ), ethers.utils.defaultAbiCoder.encode(["address"], [govImpl.address]) ]; if (isProduction) { await executeViaSafe( proposalContracts, proposalEthValues, proposalFunctionSignatures, proposalFunctionInputs, release.GuardiansSafe, "fuse" ); } else { await executeViaGuardian( proposalContracts, proposalEthValues, proposalFunctionSignatures, proposalFunctionInputs, guardian, networkEnv ); } if (isSimulation) { const isMinter = await gd.isMinter(release.HomeBridge); console.log("Fuse bridge scheme registration check:", isMinter ? "Failed" : "Success"); } }; export const main = async () => { prompt.start(); const { network } = await prompt.get(["network"]); console.log("running step:", { network }); const chain = last(network.split("-")); switch (chain) { case "mainnet": // await mainnetPostChecks() await upgradeMainnet(network); break; case "fuse": await upgradeFuse(network); break; } }; main().catch(console.log);