diff --git a/releases/deploy-settings.json b/releases/deploy-settings.json index 63ae0436..5edd5e39 100644 --- a/releases/deploy-settings.json +++ b/releases/deploy-settings.json @@ -36,6 +36,14 @@ "ubi": { "maxInactiveDays": 14, "minActiveUsers": 1000 + }, + "gooddaohouses": { + "admin": null, + "committee": null, + "citizensMinimumStake": "1000", + "alignmentMinimumStake": "10000", + "flowSplitter": null, + "flowSplitterPoolId": 0 } }, "develop": { @@ -267,6 +275,10 @@ "governance": { "claimersGOODMonthly": "1000000000000000000000000", "stakersGOODMonthly": "1000000000000000000000000" + }, + "gooddaohouses": { + "flowSplitter": "0x0e9ddfd2Ffdb0BA1aC3340D865193A7b6d4Ea147", + "flowSplitterPoolId": 11 } }, "alfajores": { diff --git a/releases/deployment.json b/releases/deployment.json index 1fbee156..d117ffec 100644 --- a/releases/deployment.json +++ b/releases/deployment.json @@ -482,7 +482,11 @@ "MentoExchangeProvider": "0x558eC7E55855FAC9403De3ADB3aa1e588234A92C", "MentoExpansionController": "0x0F6bEc7ba1e7f5D98c4Dc04c2F1F219d6B650963", "MentoReserve": "0x1cbDc8C2F57C3988cbE1B7bD2a323AaDb17379a7", - "MentoBroker": "0xE60cf1cb6a56131CE135c604D0BD67e84B57CA3C" + "MentoBroker": "0xE60cf1cb6a56131CE135c604D0BD67e84B57CA3C", + "GoodDaoHouses": "0x4Bc3Cdc036f21b68E034C0f1d90775fc3D725735", + "GoodDaoHousesFlowSplitter": "0x0e9ddfd2Ffdb0BA1aC3340D865193A7b6d4Ea147", + "GoodDaoHousesPoolId": "11", + "GoodDaoHousesPool": "0x805D843BFcf6680440420cEC33a4adB6f9a4eB89" }, "staging-celo": { "network": "staging-celo", diff --git a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts new file mode 100644 index 00000000..ba7e1723 --- /dev/null +++ b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts @@ -0,0 +1,271 @@ +/*** + * Deploy the GoodDaoHouses governance contract + * steps required: + * 1. deploy GoodDaoHouses as a UUPS proxy via the ProxyFactory (deterministic address) + * 2. wire the FlowSplitter pool used to stream the vote outcome + * + * On development-celo, if a FlowSplitter address is set and no pool id is stored yet, this + * script creates the pool with GoodDaoHouses as the only admin, then calls + * configureFlowSplitter via the Avatar. Other networks still require an existing pool. + * + * Upgrades are gated by _onlyAvatar(), so the DAO Controller owns the implementation. + * Operational roles come from initialize(): DEFAULT_ADMIN_ROLE -> admin, and + * GOVERNANCE_COMMITTEE_ROLE -> committee plus admin when the two differ. Keeping admin = + * Avatar is what lets executeViaGuardian drive committee-only calls like + * configureFlowSplitter below. + */ + +import { network, ethers } from "hardhat"; +import { Contract } from "ethers"; +import { defaultsDeep } from "lodash"; + +import { + deployDeterministic, + printDeploy, + executeViaGuardian, + executeViaSafe, + verifyContract, + verifyProductionSigner +} from "./helpers"; +import releaser from "../releaser"; +import ProtocolSettings from "../../releases/deploy-settings.json"; +import dao from "../../releases/deployment.json"; + +const { name: networkName } = network; + +export const deployGoodDaoHouses = async () => { + const viaGuardians = false; + const isProduction = networkName.includes("production"); + + let release: { [key: string]: any } = dao[networkName]; + let settings = defaultsDeep({}, ProtocolSettings[networkName], ProtocolSettings["default"]); + + let [root] = await ethers.getSigners(); + + if (isProduction) verifyProductionSigner(root); + + console.log("got signers:", { + networkName, + root: root.address, + balance: await ethers.provider.getBalance(root.address).then(_ => _.toString()) + }); + + const houseSettings = settings.gooddaohouses; + const admin = houseSettings.admin || release.Avatar; + const committee = houseSettings.committee || settings.guardiansSafe || release.Avatar; + + if (isProduction && !houseSettings.committee) { + throw new Error("set gooddaohouses.committee in deploy-settings.json before a production deploy"); + } + + // Minimum stakes are configured in whole G$ -- scale by the token decimals of the target chain. + const gd = await ethers.getContractAt("IGoodDollar", release.GoodDollar); + const decimals = await gd.decimals(); + const citizensMinimumStake = ethers.utils.parseUnits(String(houseSettings.citizensMinimumStake), decimals); + const alignmentMinimumStake = ethers.utils.parseUnits(String(houseSettings.alignmentMinimumStake), decimals); + + console.log("deploying GoodDaoHouses...", { + nameService: release.NameService, + admin, + committee, + decimals, + citizensMinimumStake: citizensMinimumStake.toString(), + alignmentMinimumStake: alignmentMinimumStake.toString() + }); + + let Houses: Contract; + if (!release.GoodDaoHouses) { + Houses = (await deployDeterministic( + { + name: "GoodDaoHouses", + isUpgradeable: true + }, + [release.NameService, admin, committee, citizensMinimumStake, alignmentMinimumStake] + ).then(printDeploy)) as Contract; + + const torelease = { + GoodDaoHouses: Houses.address + }; + release = { + ...release, + ...torelease + }; + await releaser(torelease, networkName, "deployment", false); + } else { + Houses = await ethers.getContractAt("GoodDaoHouses", release.GoodDaoHouses); + console.log("GoodDaoHouses already deployed, reusing:", Houses.address); + } + + await wireFlowSplitter(Houses, release, settings, viaGuardians, root); + + await verifyContract(Houses.address, "contracts/governance/GoodDaoHouses.sol:GoodDaoHouses", networkName); + + return Houses; +}; + +const createHousesFlowSplitterPool = async (Houses: Contract, flowSplitter: string, gdAddress: string, root) => { + const splitter = await ethers.getContractAt("IFlowSplitter", flowSplitter); + const tx = await splitter.connect(root).createPool( + gdAddress, + { + transferabilityForUnitsOwner: false, + distributionFromAnyAddress: true + }, + { + name: "GoodDAO Houses", + symbol: "GDAH", + decimals: 18 + }, + [], + [Houses.address], + '{"listed":false}', + { gasLimit: 8000000 } + ); + const receipt = await tx.wait(); + const created = receipt.events.find(e => e.event === "PoolCreated"); + if (!created) { + throw new Error(`PoolCreated event missing from ${receipt.transactionHash}`); + } + + console.log("created FlowSplitter pool", { + txHash: receipt.transactionHash, + poolId: created.args.poolId.toString(), + poolAddress: created.args.poolAddress + }); + + return { + poolId: created.args.poolId, + poolAddress: created.args.poolAddress + }; +}; + +const wireFlowSplitter = async (Houses: Contract, release, settings, viaGuardians: boolean, root) => { + const houseSettings = settings.gooddaohouses || {}; + const flowSplitter = houseSettings.flowSplitter || release.GoodDaoHousesFlowSplitter; + + if (!flowSplitter) { + console.log("no flowSplitter configured -- skipping pool wiring."); + return; + } + + const configured = await Houses.flowSplitterConfig(); + if (configured.poolAddress !== ethers.constants.AddressZero) { + console.log("flowSplitter already configured, skipping", { + splitter: configured.splitter, + poolId: configured.poolId.toString(), + poolAddress: configured.poolAddress + }); + return; + } + + let flowSplitterPoolId = houseSettings.flowSplitterPoolId || release.GoodDaoHousesPoolId; + if (!flowSplitterPoolId || ethers.BigNumber.from(flowSplitterPoolId).eq(0)) { + if (networkName !== "development-celo") { + console.log( + `create a FlowSplitter pool with ${Houses.address} in its admins list, then set ` + + "gooddaohouses.flowSplitter / gooddaohouses.flowSplitterPoolId in deploy-settings.json and re-run" + ); + return; + } + + const created = await createHousesFlowSplitterPool(Houses, flowSplitter, release.GoodDollar, root); + flowSplitterPoolId = created.poolId; + await releaser( + { + GoodDaoHousesFlowSplitter: flowSplitter, + GoodDaoHousesPoolId: created.poolId.toString(), + GoodDaoHousesPool: created.poolAddress + }, + networkName, + "deployment", + false + ); + await releaser( + { + gooddaohouses: { + ...(ProtocolSettings[networkName].gooddaohouses || {}), + flowSplitter, + flowSplitterPoolId: Number(created.poolId.toString()) + } + }, + networkName, + "deploy-settings", + false + ); + } + + const splitter = await ethers.getContractAt("IFlowSplitter", flowSplitter); + const poolId = ethers.BigNumber.from(flowSplitterPoolId); + let isPoolAdmin = false; + for (let attempt = 0; attempt < 5 && !isPoolAdmin; attempt++) { + isPoolAdmin = await splitter.isPoolAdmin(poolId, Houses.address); + if (!isPoolAdmin && attempt < 4) { + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } + if (!isPoolAdmin) { + console.error( + `GoodDaoHouses (${Houses.address}) is not an admin of pool ${poolId.toString()} -- ` + + "have an existing pool admin call addPoolAdmin() and re-run this script" + ); + return; + } + + // The call is committee-gated, so the Avatar must hold GOVERNANCE_COMMITTEE_ROLE for the + // Controller.genericCall path to work. initialize() always grants it to `committee`, and also to `admin` when admin != committee. + const committeeRole = await Houses.GOVERNANCE_COMMITTEE_ROLE(); + if (!(await Houses.hasRole(committeeRole, release.Avatar))) { + console.error( + `Avatar (${release.Avatar}) does not hold GOVERNANCE_COMMITTEE_ROLE -- ` + + "the configured committee must call configureFlowSplitter directly" + ); + return; + } + + console.log(`configuring flow splitter via ${viaGuardians ? "guardians safe" : "guardian"}`, { flowSplitter, flowSplitterPoolId }); + + const proposalActions = [ + [ + Houses.address, + "configureFlowSplitter(address,uint256)", + ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [flowSplitter, flowSplitterPoolId]), + 0 + ] + ]; + + const [proposalContracts, proposalFunctionSignatures, proposalFunctionInputs, proposalEthValues] = [ + proposalActions.map(_ => _[0]), + proposalActions.map(_ => _[1]), + proposalActions.map(_ => _[2]), + proposalActions.map(_ => _[3]) + ]; + + try { + if (viaGuardians) { + await executeViaSafe( + proposalContracts, + proposalEthValues, + proposalFunctionSignatures, + proposalFunctionInputs, + settings.guardiansSafe + ); + } else { + await executeViaGuardian( + proposalContracts, + proposalEthValues, + proposalFunctionSignatures, + proposalFunctionInputs, + root + ); + } + } catch (e) { + // Log the full thrown value to preserve stack/context and avoid assuming Error shape + console.error("proposal execution failed...", e); + } +}; + +export const main = async () => { + await deployGoodDaoHouses(); +}; + +if (process.argv[1].includes("gooddaohouses")) main(); diff --git a/scripts/releaser.js b/scripts/releaser.js index 516faa10..5f0624ae 100644 --- a/scripts/releaser.js +++ b/scripts/releaser.js @@ -20,5 +20,5 @@ module.exports = async function ( previousDeployment: previousDeployment[network], finalDeployment: finalDeployment[network] }); - return fse.writeJson(dir + `/${filename}.json`, finalDeployment); + return fse.writeJson(dir + `/${filename}.json`, finalDeployment, { spaces: 2 }); }; diff --git a/test/governance/CompoundVotingMachine.propose.test.ts b/test/governance/CompoundVotingMachine.propose.test.ts index 0402145e..b4f578a0 100644 --- a/test/governance/CompoundVotingMachine.propose.test.ts +++ b/test/governance/CompoundVotingMachine.propose.test.ts @@ -132,8 +132,8 @@ describe("CompoundVotingMachine#propose", () => { it("reverts with active", async () => { await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_mine", []); - - grep.undelegate(); + + await grep.undelegate().catch(() => undefined); await expect( gov["propose(address[],uint256[],string[],bytes[],string)"]( diff --git a/test/governance/GoodDaoHouses.test.ts b/test/governance/GoodDaoHouses.test.ts index 8ee9ca5e..537a577e 100644 --- a/test/governance/GoodDaoHouses.test.ts +++ b/test/governance/GoodDaoHouses.test.ts @@ -519,7 +519,6 @@ describe("GoodDaoHouses", () => { identityDeployed } = await loadFixture(fixture); - await addWhitelisted(citizenOne.address, "did:gooddollar:citizen-unstake-vote"); await registerCitizen(goodDollar, houses, citizenOne, "citizen-one"); await registerAlignment(committee, goodDollar, houses, alignmentOne, "alignment-one"); await registerAlignment(committee, goodDollar, houses, alignmentTwo, "alignment-two"); @@ -532,6 +531,8 @@ describe("GoodDaoHouses", () => { const termDuration = await houses.termDuration(); await increaseTime(termDuration.toNumber()); + await addWhitelisted(citizenOne.address, "did:gooddollar:citizen-unstake-vote"); + const voteId = await moveToNextVotingWindow(houses); await houses.connect(alignmentTwo).castVote( [alignmentOne.address, alignmentTwo.address], diff --git a/test/helpers.ts b/test/helpers.ts index 2aa5bd36..7f0ec602 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -47,52 +47,54 @@ export const deploySuperGoodDollar = async (sfContracts, tokenArgs) => { const SuperGoodDollarFactory = await ethers.getContractFactory("SuperGoodDollar"); console.log("deploying supergooddollar logic"); - const SuperGoodDollar = await SuperGoodDollarFactory.deploy(sfContracts.host); + const logic = await SuperGoodDollarFactory.deploy(sfContracts.host); + const logicAddress = logic.address; console.log("deploying supergooddollar proxy"); const GoodDollarProxyFactory = await ethers.getContractFactory("contracts/token/superfluid/UUPSProxy.sol:UUPSProxy"); const GoodDollarProxy = await GoodDollarProxyFactory.deploy(); + const proxyAddress = GoodDollarProxy.address; console.log("deployed supergooddollar proxy, initializing proxy..."); - await GoodDollarProxy.initializeProxy(SuperGoodDollar.address); + await GoodDollarProxy.initializeProxy(logicAddress); + + const GoodDollar = await ethers.getContractAt("SuperGoodDollar", proxyAddress); if (sfContracts.host !== ethers.constants.AddressZero) { console.log("deploying flow nfts..."); const outNftProxy = await GoodDollarProxyFactory.deploy(); const inNftProxy = await GoodDollarProxyFactory.deploy(); + const outNftProxyAddress = outNftProxy.address; + const inNftProxyAddress = inNftProxy.address; - const constantInflowNFT = await ethers.deployContract("ConstantInflowNFT", [sfContracts.host, outNftProxy.address]); + const constantInflowNFTLogic = await ethers.deployContract("ConstantInflowNFT", [ + sfContracts.host, + outNftProxyAddress + ]); - const constantOutflowNFT = await ethers.deployContract("ConstantOutflowNFT", [ + const constantOutflowNFTLogic = await ethers.deployContract("ConstantOutflowNFT", [ sfContracts.host, - inNftProxy.address + inNftProxyAddress ]); - await outNftProxy.initializeProxy(constantOutflowNFT.address); - await inNftProxy.initializeProxy(constantInflowNFT.address); + await outNftProxy.initializeProxy(constantOutflowNFTLogic.address); + await inNftProxy.initializeProxy(constantInflowNFTLogic.address); console.log("initializing supergooddollar...."); - await SuperGoodDollar.attach(GoodDollarProxy.address)[ - "initialize(string,string,uint256,address,address,address,address)" - ](...tokenArgs); - const GoodDollar = await ethers.getContractAt("SuperGoodDollar", GoodDollarProxy.address); + await GoodDollar["initialize(string,string,uint256,address,address,address,address)"](...tokenArgs); console.log("supergooddollar created successfully"); - await constantOutflowNFT - .attach(outNftProxy.address) - .initialize((await GoodDollar.symbol()) + " Outflow NFT", (await GoodDollar.symbol()) + " COF"); - await constantInflowNFT - .attach(inNftProxy.address) - .initialize((await GoodDollar.symbol()) + " Inflow NFT", (await GoodDollar.symbol()) + " CIF"); - return GoodDollar; - } else { - console.log("initializing supergooddollar...."); - await SuperGoodDollar.attach(GoodDollarProxy.address)[ - "initialize(string,string,uint256,address,address,address,address)" - ](...tokenArgs); - const GoodDollar = await ethers.getContractAt("SuperGoodDollar", GoodDollarProxy.address); - console.log("supergooddollar created successfully"); + const symbol = await GoodDollar.symbol(); + const constantOutflowNFT = await ethers.getContractAt("ConstantOutflowNFT", outNftProxyAddress); + const constantInflowNFT = await ethers.getContractAt("ConstantInflowNFT", inNftProxyAddress); + await constantOutflowNFT.initialize(symbol + " Outflow NFT", symbol + " COF"); + await constantInflowNFT.initialize(symbol + " Inflow NFT", symbol + " CIF"); return GoodDollar; } + + console.log("initializing supergooddollar...."); + await GoodDollar["initialize(string,string,uint256,address,address,address,address)"](...tokenArgs); + console.log("supergooddollar created successfully"); + return GoodDollar; }; export const createDAO = async (tokenType: "super" | "regular" = "super", identity: "v2" | "v3" | "v4" = "v4") => { let [root, ...signers] = await ethers.getSigners(); diff --git a/test/token/SuperGoodDollar.nohost.test.ts b/test/token/SuperGoodDollar.nohost.test.ts index 845c8e9c..c35d7ffb 100644 --- a/test/token/SuperGoodDollar.nohost.test.ts +++ b/test/token/SuperGoodDollar.nohost.test.ts @@ -23,57 +23,54 @@ const tenDollarsPerDay = "124378109452730"; // flowrate per second const initialState = async () => {}; -before(async function () { - //get accounts from hardhat - [founder, alice, bob, eve, newHost] = await ethers.getSigners(); - - await createDAO(); - - const sfContracts = { - host: ethers.constants.AddressZero - }; - sfHost = sfContracts.host; - - // GoodDollar specific init - const FeesFormulaMockFactory = await ethers.getContractFactory( - "FeesFormulaMock", - founder - ); - - const feesFormula0PctMock = await FeesFormulaMockFactory.deploy(0); - - feesFormula10PctMock = await FeesFormulaMockFactory.deploy(100000); - - // the zero address is a placeholder for the dao contract - const IdentityMockFactory = await ethers.getContractFactory( - "IdentityMock", - founder - ); - identityMock = await IdentityMockFactory.deploy( - "0x0000000000000000000000000000000000000000" - ); - - receiverMock = await new ethers.ContractFactory( - TransferAndCallMockABI.abi, - TransferAndCallMockABI.bytecode, - founder - ).deploy(); - - console.log("deploying test supergooddollar..."); - sgd = (await deploySuperGoodDollar(sfContracts, [ - "SuperGoodDollar", - "SGD", - 0, // cap - feesFormula0PctMock.address, - identityMock.address, - receiverMock.address, - founder.address - ])) as ISuperGoodDollar; - - await sgd.mint(founder.address, alotOfDollars); -}); - describe("SuperGoodDollar No Host", async function () { + before(async function () { + [founder, alice, bob, eve, newHost] = await ethers.getSigners(); + + await createDAO(); + + const sfContracts = { + host: ethers.constants.AddressZero + }; + sfHost = sfContracts.host; + + const FeesFormulaMockFactory = await ethers.getContractFactory( + "FeesFormulaMock", + founder + ); + + const feesFormula0PctMock = await FeesFormulaMockFactory.deploy(0); + + feesFormula10PctMock = await FeesFormulaMockFactory.deploy(100000); + + const IdentityMockFactory = await ethers.getContractFactory( + "IdentityMock", + founder + ); + identityMock = await IdentityMockFactory.deploy( + "0x0000000000000000000000000000000000000000" + ); + + receiverMock = await new ethers.ContractFactory( + TransferAndCallMockABI.abi, + TransferAndCallMockABI.bytecode, + founder + ).deploy(); + + console.log("deploying test supergooddollar..."); + sgd = (await deploySuperGoodDollar(sfContracts, [ + "SuperGoodDollar", + "SGD", + 0, + feesFormula0PctMock.address, + identityMock.address, + receiverMock.address, + founder.address + ])) as ISuperGoodDollar; + + await sgd.mint(founder.address, alotOfDollars); + }); + it("check superfluid host", async () => { expect(await sgd.getHost()).equal(sfHost); }); diff --git a/test/token/SuperGoodDollar.test.ts b/test/token/SuperGoodDollar.test.ts index 48c7679b..6890f4a9 100644 --- a/test/token/SuperGoodDollar.test.ts +++ b/test/token/SuperGoodDollar.test.ts @@ -25,61 +25,57 @@ const tenDollarsPerDay = "124378109452730"; // flowrate per second const initialState = async () => {}; -before(async function () { - //get accounts from hardhat - [founder, alice, bob, eve, newHost] = await ethers.getSigners(); - - let { sfContracts } = await createDAO(); - - sfHost = sfContracts.host; - // initialize sdk-core to get a framework handle for more convenient access to Superfluid functionality - sf = await Framework.create({ - chainId: 4447, - provider: ethers.provider, - resolverAddress: sfContracts.resolver, - protocolReleaseVersion: "test" - }); +describe("SuperGoodDollar", async function () { + before(async function () { + [founder, alice, bob, eve, newHost] = await ethers.getSigners(); - // GoodDollar specific init - const FeesFormulaMockFactory = await ethers.getContractFactory( - "FeesFormulaMock", - founder - ); - - const feesFormula0PctMock = await FeesFormulaMockFactory.deploy(0); - - feesFormula10PctMock = await FeesFormulaMockFactory.deploy(100000); - - // the zero address is a placeholder for the dao contract - const IdentityMockFactory = await ethers.getContractFactory( - "IdentityMock", - founder - ); - identityMock = await IdentityMockFactory.deploy( - "0x0000000000000000000000000000000000000000" - ); - - receiverMock = await new ethers.ContractFactory( - TransferAndCallMockABI.abi, - TransferAndCallMockABI.bytecode, - founder - ).deploy(); - - console.log("deploying test supergooddollar..."); - sgd = (await deploySuperGoodDollar(sfContracts, [ - "SuperGoodDollar", - "SGD", - 0, // cap - feesFormula0PctMock.address, - identityMock.address, - receiverMock.address, - founder.address - ])) as ISuperGoodDollar; - - await sgd.mint(founder.address, alotOfDollars); -}); + let { sfContracts } = await createDAO(); + + sfHost = sfContracts.host; + sf = await Framework.create({ + chainId: 4447, + provider: ethers.provider, + resolverAddress: sfContracts.resolver, + protocolReleaseVersion: "test" + }); + + const FeesFormulaMockFactory = await ethers.getContractFactory( + "FeesFormulaMock", + founder + ); + + const feesFormula0PctMock = await FeesFormulaMockFactory.deploy(0); + + feesFormula10PctMock = await FeesFormulaMockFactory.deploy(100000); + + const IdentityMockFactory = await ethers.getContractFactory( + "IdentityMock", + founder + ); + identityMock = await IdentityMockFactory.deploy( + "0x0000000000000000000000000000000000000000" + ); + + receiverMock = await new ethers.ContractFactory( + TransferAndCallMockABI.abi, + TransferAndCallMockABI.bytecode, + founder + ).deploy(); + + console.log("deploying test supergooddollar..."); + sgd = (await deploySuperGoodDollar(sfContracts, [ + "SuperGoodDollar", + "SGD", + 0, + feesFormula0PctMock.address, + identityMock.address, + receiverMock.address, + founder.address + ])) as ISuperGoodDollar; + + await sgd.mint(founder.address, alotOfDollars); + }); -describe("SuperGoodDollar", async function () { it("check superfluid host", async () => { expect(await sgd.getHost()).equal(sfHost); }); diff --git a/test/utils/BuyGDClone.test.ts b/test/utils/BuyGDClone.test.ts index dd6218d7..d5f42de5 100644 --- a/test/utils/BuyGDClone.test.ts +++ b/test/utils/BuyGDClone.test.ts @@ -19,9 +19,18 @@ import deployments from "../../releases/deployment.json"; import * as networkHelpers from "@nomicfoundation/hardhat-network-helpers"; // Celo mainnet addresses -const CELO_MAINNET_RPC = "https://forno.celo.org"; +const CELO_MAINNET_RPC = process.env.CELO_RPC_URL || "https://forno.celo.org"; const CELO_CHAIN_ID = 42220; +async function getCeloForkBlock() { + if (process.env.CELO_FORK_BLOCK) { + return parseInt(process.env.CELO_FORK_BLOCK, 10); + } + const provider = new ethers.providers.JsonRpcProvider(CELO_MAINNET_RPC); + const latest = await provider.getBlockNumber(); + return latest - 5; +} + // Production Celo addresses from deployment.json (used for existing contracts on fork) const PRODUCTION_CELO = deployments["production-celo"]; const GOODDOLLAR = PRODUCTION_CELO.GoodDollar; @@ -70,7 +79,7 @@ describe("BuyGDClone - Celo Fork E2E", function () { await networkHelpers.reset(); }); before(async function () { - await networkHelpers.reset(CELO_MAINNET_RPC); + await networkHelpers.reset(CELO_MAINNET_RPC, await getCeloForkBlock()); }); async function forkCelo() {