From 7f1342bde6d9c1ee80f87345fefdba8b385090d2 Mon Sep 17 00:00:00 2001 From: blueogin Date: Thu, 13 Aug 2026 14:31:29 -0400 Subject: [PATCH 01/12] feat: add GoodDaoHouses governance contract deployment - Introduced a new deployment script for the GoodDaoHouses governance contract. - Updated deploy-settings.json to include configuration for GoodDaoHouses, including admin and committee roles, as well as minimum stake requirements. - Added GoodDaoHouses address to deployment.json for network integration. --- releases/deploy-settings.json | 8 + releases/deployment.json | 3 +- .../9_gooddaohouses-deploy.ts | 198 ++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 scripts/multichain-deploy/9_gooddaohouses-deploy.ts diff --git a/releases/deploy-settings.json b/releases/deploy-settings.json index 63ae0436..d468dbc6 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": { diff --git a/releases/deployment.json b/releases/deployment.json index 1fbee156..13eb3537 100644 --- a/releases/deployment.json +++ b/releases/deployment.json @@ -482,7 +482,8 @@ "MentoExchangeProvider": "0x558eC7E55855FAC9403De3ADB3aa1e588234A92C", "MentoExpansionController": "0x0F6bEc7ba1e7f5D98c4Dc04c2F1F219d6B650963", "MentoReserve": "0x1cbDc8C2F57C3988cbE1B7bD2a323AaDb17379a7", - "MentoBroker": "0xE60cf1cb6a56131CE135c604D0BD67e84B57CA3C" + "MentoBroker": "0xE60cf1cb6a56131CE135c604D0BD67e84B57CA3C", + "GoodDaoHouses": "0x4Bc3Cdc036f21b68E034C0f1d90775fc3D725735" }, "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..08edf934 --- /dev/null +++ b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts @@ -0,0 +1,198 @@ +/*** + * 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 + * + * The FlowSplitter pool must already exist and have GoodDaoHouses as one of its pool + * admins before step 2 -- configureFlowSplitter reverts with "Not pool admin" otherwise. + * The proxy address is deterministic (CREATE2 over keccak("GoodDaoHouses") + deployer), so + * either create the pool up front with the address printed by this script in the pool + * `_admins` list, or have an existing pool admin call addPoolAdmin(poolId, ) and + * re-run this script. + * + * 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; +}; + +// Points GoodDaoHouses at the FlowSplitter pool it manages, via the DAO guardians. +const wireFlowSplitter = async (Houses: Contract, release, settings, viaGuardians: boolean, root) => { + const { flowSplitter, flowSplitterPoolId } = settings.gooddaohouses; + + if (!flowSplitter || !flowSplitterPoolId) { + console.log("no flowSplitter configured -- skipping pool wiring."); + 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 configured = await Houses.flowSplitterConfig(); + if ( + configured.splitter.toLowerCase() === flowSplitter.toLowerCase() && + configured.poolId.eq(flowSplitterPoolId) + ) { + console.log("flowSplitter already configured, skipping", { flowSplitter, flowSplitterPoolId }); + return; + } + + // configureFlowSplitter requires the houses proxy to already be an admin of the pool. + const splitter = await ethers.getContractAt("IFlowSplitter", flowSplitter); + const isPoolAdmin = await splitter.isPoolAdmin(flowSplitterPoolId, Houses.address); + if (!isPoolAdmin) { + console.error( + `GoodDaoHouses (${Houses.address}) is not an admin of pool ${flowSplitterPoolId} -- ` + + "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 + // guardian path to work. initialize() grants it whenever 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 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) { + console.error("proposal execution failed...", e.message); + } +}; + +export const main = async () => { + await deployGoodDaoHouses(); +}; + +if (process.argv[1].includes("gooddaohouses")) main(); From da037588ad3bbedba762c0ce2adb3c149ed1413d Mon Sep 17 00:00:00 2001 From: blueogin <43612769+blueogin@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:38:00 -0400 Subject: [PATCH 02/12] Update scripts/multichain-deploy/9_gooddaohouses-deploy.ts Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- scripts/multichain-deploy/9_gooddaohouses-deploy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts index 08edf934..81c2e867 100644 --- a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts +++ b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts @@ -187,7 +187,8 @@ const wireFlowSplitter = async (Houses: Contract, release, settings, viaGuardian ); } } catch (e) { - console.error("proposal execution failed...", e.message); + // Log the full thrown value to preserve stack/context and avoid assuming Error shape + console.error("proposal execution failed...", e); } }; From 1a20dca4d8a6757226fb9d00575cb985797e14bf Mon Sep 17 00:00:00 2001 From: blueogin <43612769+blueogin@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:44:13 -0400 Subject: [PATCH 03/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/multichain-deploy/9_gooddaohouses-deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts index 81c2e867..1d141d60 100644 --- a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts +++ b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts @@ -150,7 +150,7 @@ const wireFlowSplitter = async (Houses: Contract, release, settings, viaGuardian return; } - console.log("configuring flow splitter via guardian", { flowSplitter, flowSplitterPoolId }); + console.log(`configuring flow splitter via ${viaGuardians ? "guardians safe" : "guardian"}`, { flowSplitter, flowSplitterPoolId }); const proposalActions = [ [ From aeb6680481b6597e2e897f21f811e4092ec949cc Mon Sep 17 00:00:00 2001 From: blueogin <43612769+blueogin@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:44:26 -0400 Subject: [PATCH 04/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/multichain-deploy/9_gooddaohouses-deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts index 1d141d60..59303712 100644 --- a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts +++ b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts @@ -140,7 +140,7 @@ const wireFlowSplitter = async (Houses: Contract, release, settings, viaGuardian } // The call is committee-gated, so the Avatar must hold GOVERNANCE_COMMITTEE_ROLE for the - // guardian path to work. initialize() grants it whenever admin != committee. + // 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( From 715bb40ef0991d25d4d5c47ba5310b42269ba744 Mon Sep 17 00:00:00 2001 From: blueogin Date: Thu, 13 Aug 2026 15:15:02 -0400 Subject: [PATCH 05/12] Refactor GoodDaoHouses tests to reintroduce whitelisting for citizenOne - Removed the initial whitelisting call for citizenOne and added it back before the voting window. - Ensured proper setup for testing the voting process in GoodDaoHouses. --- test/governance/GoodDaoHouses.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/governance/GoodDaoHouses.test.ts b/test/governance/GoodDaoHouses.test.ts index 9554daee..e6a6ba0b 100644 --- a/test/governance/GoodDaoHouses.test.ts +++ b/test/governance/GoodDaoHouses.test.ts @@ -517,7 +517,6 @@ describe("GoodDaoHouses", () => { addWhitelisted } = 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"); @@ -530,6 +529,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], From 5b14ab193f2287185411a47c3715f18fe290d5f3 Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 17 Aug 2026 08:56:22 -0400 Subject: [PATCH 06/12] refactor: improve deployment logic in SuperGoodDollar helper functions - Updated the deploySuperGoodDollar function to use clearer variable names for logic and proxy addresses. - Enhanced initialization of NFT proxies to ensure correct contract interactions. - Streamlined the deployment process for better readability and maintainability. - Adjusted test setup in SuperGoodDollar tests for consistency and clarity. - Added error handling in BuyGDClone test to manage network reset failures gracefully. --- test/helpers.ts | 52 +++++------ test/token/SuperGoodDollar.nohost.test.ts | 97 ++++++++++---------- test/token/SuperGoodDollar.test.ts | 102 +++++++++++----------- test/utils/BuyGDClone.test.ts | 10 ++- 4 files changed, 132 insertions(+), 129 deletions(-) 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..0f631895 100644 --- a/test/utils/BuyGDClone.test.ts +++ b/test/utils/BuyGDClone.test.ts @@ -70,7 +70,15 @@ describe("BuyGDClone - Celo Fork E2E", function () { await networkHelpers.reset(); }); before(async function () { - await networkHelpers.reset(CELO_MAINNET_RPC); + try { + await networkHelpers.reset(CELO_MAINNET_RPC); + } catch (e: any) { + const message = e?.message || String(e); + if (message.includes("historical state") || message.includes("not available")) { + this.skip(); + } + throw e; + } }); async function forkCelo() { From c4ebb55113b20b5ceefb672c46cbef4c4723d8da Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 17 Aug 2026 09:11:13 -0400 Subject: [PATCH 07/12] refactor: simplify error handling in BuyGDClone test setup - Removed try-catch block for network reset in BuyGDClone tests to streamline the setup process. - Directly called networkHelpers.reset with CELO_MAINNET_RPC for improved clarity and efficiency. --- test/utils/BuyGDClone.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/utils/BuyGDClone.test.ts b/test/utils/BuyGDClone.test.ts index 0f631895..dd6218d7 100644 --- a/test/utils/BuyGDClone.test.ts +++ b/test/utils/BuyGDClone.test.ts @@ -70,15 +70,7 @@ describe("BuyGDClone - Celo Fork E2E", function () { await networkHelpers.reset(); }); before(async function () { - try { - await networkHelpers.reset(CELO_MAINNET_RPC); - } catch (e: any) { - const message = e?.message || String(e); - if (message.includes("historical state") || message.includes("not available")) { - this.skip(); - } - throw e; - } + await networkHelpers.reset(CELO_MAINNET_RPC); }); async function forkCelo() { From 6a645df357ab5d52873567cc0b0d3f260899ed54 Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 17 Aug 2026 09:16:00 -0400 Subject: [PATCH 08/12] refactor: enhance BuyGDClone test setup with dynamic Celo fork block retrieval - Updated CELO_MAINNET_RPC to use an environment variable for flexibility. - Added getCeloForkBlock function to dynamically determine the fork block number, improving test reliability. - Adjusted network reset call in tests to incorporate the new fork block logic. --- test/utils/BuyGDClone.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/utils/BuyGDClone.test.ts b/test/utils/BuyGDClone.test.ts index dd6218d7..526c4a5c 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 - 50; +} + // 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() { From 6df2eb94cbc3d0b4a93450948809eb7e0047ee61 Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 17 Aug 2026 09:29:21 -0400 Subject: [PATCH 09/12] fix: improve error handling in CompoundVotingMachine propose test - Updated the undelegate call in the propose test to handle potential rejections gracefully by using a catch block. - This change enhances the robustness of the test by preventing unhandled promise rejections during execution. --- test/governance/CompoundVotingMachine.propose.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/governance/CompoundVotingMachine.propose.test.ts b/test/governance/CompoundVotingMachine.propose.test.ts index 0402145e..f275337a 100644 --- a/test/governance/CompoundVotingMachine.propose.test.ts +++ b/test/governance/CompoundVotingMachine.propose.test.ts @@ -133,8 +133,6 @@ describe("CompoundVotingMachine#propose", () => { await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_mine", []); - grep.undelegate(); - await expect( gov["propose(address[],uint256[],string[],bytes[],string)"]( targets, From 32e5c46e43ff23379c35e9bb1315efbf6fc26d6b Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 17 Aug 2026 09:30:12 -0400 Subject: [PATCH 10/12] test: enhance error handling in CompoundVotingMachine propose test - Added a catch block to the undelegate call in the propose test to gracefully handle potential rejections. - This improvement increases the robustness of the test by preventing unhandled promise rejections during execution. --- test/governance/CompoundVotingMachine.propose.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/governance/CompoundVotingMachine.propose.test.ts b/test/governance/CompoundVotingMachine.propose.test.ts index f275337a..b4f578a0 100644 --- a/test/governance/CompoundVotingMachine.propose.test.ts +++ b/test/governance/CompoundVotingMachine.propose.test.ts @@ -132,6 +132,8 @@ describe("CompoundVotingMachine#propose", () => { it("reverts with active", async () => { await ethers.provider.send("evm_mine", []); await ethers.provider.send("evm_mine", []); + + await grep.undelegate().catch(() => undefined); await expect( gov["propose(address[],uint256[],string[],bytes[],string)"]( From f9f2e1b5c54f6a4adb06d4860a916e71ce53c7dc Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 17 Aug 2026 09:57:03 -0400 Subject: [PATCH 11/12] fix: adjust Celo fork block retrieval in BuyGDClone test - Modified the getCeloForkBlock function to return the latest block number minus 5 instead of 50, improving the accuracy of the test environment setup. --- test/utils/BuyGDClone.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/BuyGDClone.test.ts b/test/utils/BuyGDClone.test.ts index 526c4a5c..d5f42de5 100644 --- a/test/utils/BuyGDClone.test.ts +++ b/test/utils/BuyGDClone.test.ts @@ -28,7 +28,7 @@ async function getCeloForkBlock() { } const provider = new ethers.providers.JsonRpcProvider(CELO_MAINNET_RPC); const latest = await provider.getBlockNumber(); - return latest - 50; + return latest - 5; } // Production Celo addresses from deployment.json (used for existing contracts on fork) From f1b1a486236285987a415a73d827cff7a10ee302 Mon Sep 17 00:00:00 2001 From: blueogin Date: Mon, 17 Aug 2026 11:38:12 -0400 Subject: [PATCH 12/12] feat: enhance GoodDaoHouses deployment configuration - Added flowSplitter and flowSplitterPoolId to deploy-settings.json for GoodDaoHouses. - Updated deployment.json to include GoodDaoHousesFlowSplitter and GoodDaoHousesPoolId. - Improved releaser script to handle FlowSplitter pool creation and configuration dynamically. - Enhanced error handling in the multichain deployment script for better robustness. --- releases/deploy-settings.json | 4 + releases/deployment.json | 5 +- .../9_gooddaohouses-deploy.ts | 114 ++++++++++++++---- scripts/releaser.js | 2 +- 4 files changed, 102 insertions(+), 23 deletions(-) diff --git a/releases/deploy-settings.json b/releases/deploy-settings.json index d468dbc6..5edd5e39 100644 --- a/releases/deploy-settings.json +++ b/releases/deploy-settings.json @@ -275,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 13eb3537..d117ffec 100644 --- a/releases/deployment.json +++ b/releases/deployment.json @@ -483,7 +483,10 @@ "MentoExpansionController": "0x0F6bEc7ba1e7f5D98c4Dc04c2F1F219d6B650963", "MentoReserve": "0x1cbDc8C2F57C3988cbE1B7bD2a323AaDb17379a7", "MentoBroker": "0xE60cf1cb6a56131CE135c604D0BD67e84B57CA3C", - "GoodDaoHouses": "0x4Bc3Cdc036f21b68E034C0f1d90775fc3D725735" + "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 index 59303712..ba7e1723 100644 --- a/scripts/multichain-deploy/9_gooddaohouses-deploy.ts +++ b/scripts/multichain-deploy/9_gooddaohouses-deploy.ts @@ -4,12 +4,9 @@ * 1. deploy GoodDaoHouses as a UUPS proxy via the ProxyFactory (deterministic address) * 2. wire the FlowSplitter pool used to stream the vote outcome * - * The FlowSplitter pool must already exist and have GoodDaoHouses as one of its pool - * admins before step 2 -- configureFlowSplitter reverts with "Not pool admin" otherwise. - * The proxy address is deterministic (CREATE2 over keccak("GoodDaoHouses") + deployer), so - * either create the pool up front with the address printed by this script in the pool - * `_admins` list, or have an existing pool admin call addPoolAdmin(poolId, ) and - * re-run this script. + * 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 @@ -106,34 +103,109 @@ export const deployGoodDaoHouses = async () => { return Houses; }; -// Points GoodDaoHouses at the FlowSplitter pool it manages, via the DAO guardians. +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 { flowSplitter, flowSplitterPoolId } = settings.gooddaohouses; + const houseSettings = settings.gooddaohouses || {}; + const flowSplitter = houseSettings.flowSplitter || release.GoodDaoHousesFlowSplitter; - if (!flowSplitter || !flowSplitterPoolId) { + if (!flowSplitter) { console.log("no flowSplitter configured -- skipping pool wiring."); - 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 configured = await Houses.flowSplitterConfig(); - if ( - configured.splitter.toLowerCase() === flowSplitter.toLowerCase() && - configured.poolId.eq(flowSplitterPoolId) - ) { - console.log("flowSplitter already configured, skipping", { flowSplitter, flowSplitterPoolId }); + if (configured.poolAddress !== ethers.constants.AddressZero) { + console.log("flowSplitter already configured, skipping", { + splitter: configured.splitter, + poolId: configured.poolId.toString(), + poolAddress: configured.poolAddress + }); return; } - // configureFlowSplitter requires the houses proxy to already be an admin of the pool. + 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 isPoolAdmin = await splitter.isPoolAdmin(flowSplitterPoolId, Houses.address); + 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 ${flowSplitterPoolId} -- ` + + `GoodDaoHouses (${Houses.address}) is not an admin of pool ${poolId.toString()} -- ` + "have an existing pool admin call addPoolAdmin() and re-run this script" ); return; 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 }); };