@gooddollar/goodprotocol
Version:
GoodDollar Protocol
475 lines (416 loc) • 18 kB
text/typescript
/***
* 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);