Skip to content

Implement GoodDaoHouses MVP smart contract build#299

Open
Copilot wants to merge 14 commits into
masterfrom
copilot/gooddao-implementation-plan
Open

Implement GoodDaoHouses MVP smart contract build#299
Copilot wants to merge 14 commits into
masterfrom
copilot/gooddao-implementation-plan

Conversation

Copilot AI commented Jun 10, 2026

Copy link
Copy Markdown

This PR implements the GoodDaoHouses MVP as an actual smart-contract deliverable, replacing the earlier planning-only artifact. It adds the governance contract, the FlowSplitter integration surface it depends on, a mock for focused testing, and targeted governance tests covering the core membership and vote execution flows.

  • GoodDaoHouses contract

    • Adds contracts/governance/GoodDaoHouses.sol.
    • Implements the Citizens / Alignment house model with per-house minimum stake configuration.
    • Supports ERC677 transferAndCall registration, direct register-and-stake, additional staking, approval, revocation, and unstake flows.
    • Stores the requested house fields on-chain: citizen name / socialLinks, and alignment name / projectWebpage / missionStatement / distributionStrategy.
    • Uses repo-consistent upgradeable and DAO-aware patterns via DAOUpgradeableContract, NameService, access control, pausing, and avatar-authorized upgrades.
  • Membership and voting model

    • Stores member lifecycle state (None, Pending, Active, Revoked, Unstaked) and alignment eligibility records onchain.
    • Activates Citizens members immediately after staking and gates Alignment members behind committee-managed eligibility and approval.
    • Implements quarterly term voting with termDuration and votingTermLength, where the first ballot cast during the voting window lazily creates the vote record and uses timestamp / termDuration as the voteId.
    • Removes dedicated active-member and voter snapshot sets, and derives voter eligibility from active membership plus joinedAt relative to the current term vote start.
    • Stores direct recipient vote units in voteRecipientWeightedVotes, dividing weighted allocations by BASIS_POINTS.
    • Removes the separate finalize step and executes FlowSplitter updates directly from the stored recipient vote units after the voting window closes.
  • FlowSplitter execution boundary

    • Adds contracts/governance/IFlowSplitter.sol to mirror the pool-based integration surface needed by the MVP.
    • Executes vote results by translating stored voteRecipientWeightedVotes values into IFlowSplitter.Member[] at the integration boundary.
    • Creates the pool on first execution, reuses the same pool on later executions, and sends zero-unit updates on member removal or unstake.
  • Tests and supporting mock

    • Adds contracts/mocks/MockFlowSplitter.sol for focused governance testing.
    • Adds test/governance/GoodDaoHouses.test.ts.
    • Covers ERC677 registration, on-chain member field storage, alignment eligibility and approval, lazy term vote creation, ballot replacement, join-date-based voter eligibility, direct weighted unit storage, first pool creation, subsequent member-unit updates, and zero-unit cleanup on unstake.
  • Cleanup

    • Removes the mistakenly committed installed skills folder contents and skills-lock.json.

Testing

  • ⚠️ yarn compile (blocked in this sandbox because Hardhat could not reach binaries.soliditylang.org to download the Solidity compiler)
  • ⚠️ yarn hardhat test test/governance/GoodDaoHouses.test.ts (blocked for the same compiler-download reason)
  • ✅ Local solc compile check for contracts/governance/GoodDaoHouses.sol
  • ✅ TypeScript transpile check for test/governance/GoodDaoHouses.test.ts

Example of the implemented execution boundary:

_voteRecipientWeightedVotes[voteId][recipient] +=
  (allocation * voterWeight) / BASIS_POINTS;

IFlowSplitter.Member[] memory members = new IFlowSplitter.Member[](recipients.length);
members[i] = IFlowSplitter.Member({
  account: recipient,
  units: uint128(_voteRecipientWeightedVotes[voteId][recipient])
});

Summary by Sourcery

Introduce the GoodDaoHouses governance contract MVP with member-staking houses and FlowSplitter-based vote execution.

New Features:

  • Add GoodDaoHouses upgradeable governance contract implementing Citizens and Alignment houses with configurable staking requirements and ERC677-based registration and staking flows.
  • Store rich on-chain metadata for members and manage their lifecycle and voting eligibility across term-based voting cycles.
  • Integrate with a FlowSplitter pool to translate finalized weighted vote results into member unit allocations, including zero-unit cleanup on removal or unstake.

Enhancements:

  • Add IFlowSplitter integration interface and a MockFlowSplitter implementation to provide a testable execution boundary for distributing vote outcomes.

Tests:

  • Add GoodDaoHouses governance test suite covering registration paths, eligibility gating, term voting behavior, FlowSplitter execution, and lifecycle edge cases.

Copilot AI linked an issue Jun 10, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Implement MVP outline for GoodDaoHouses Add maintainer-reviewable GoodDaoHouses MVP implementation plan Jun 10, 2026
Copilot AI requested a review from L03TJ3 June 10, 2026 10:00
@L03TJ3

L03TJ3 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

@copilot The task was to execute on the plan, not create a new implementation plan. issue #298 is supposed be the implementation plan

Copilot AI commented Jun 10, 2026

Copy link
Copy Markdown
Author

@copilot The task was to execute on the plan, not create a new implementation plan. issue #298 is supposed be the implementation plan

Addressed in 9395d71.

@L03TJ3

L03TJ3 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
  1. remove the installed skills folder
  2. I want you to actually execute and implement the requested contract. please re-read [Plan]: GoodDaoHouses MVP implementation outline #298

The deliverible is: the smart-contract build.

@copilot

Copilot AI commented Jun 10, 2026

Copy link
Copy Markdown
Author
  1. remove the installed skills folder
  2. I want you to actually execute and implement the requested contract. please re-read [Plan]: GoodDaoHouses MVP implementation outline #298

...

Addressed in 478c1c7: removed the committed skills payload and replaced the plan-only change with the GoodDaoHouses contract implementation, FlowSplitter integration surface, and focused governance tests.

Copilot AI changed the title Add maintainer-reviewable GoodDaoHouses MVP implementation plan Implement GoodDaoHouses MVP smart contract build Jun 10, 2026
@openzeppelin-code

openzeppelin-code Bot commented Jun 10, 2026

Copy link
Copy Markdown

Implement GoodDaoHouses MVP smart contract build

Generated at commit: a465cbf36534b2f58da0ee30089626460692cf37

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
3
5
0
17
45
70
Dependencies Critical
High
Medium
Low
Note
Total
0
0
1
0
139
140

For more details view the full report in OpenZeppelin Code Inspector

@L03TJ3 L03TJ3 requested a review from sirpy June 11, 2026 05:54

@sirpy sirpy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. the fields should be written on chain. for distribution strategy it should be a url to a post on our forum.
  2. quarterly/term vote: modulo timestamp by termDuration if result is between 0 and votingTermLength then we are in voting period time frame, first vote will "create" the vote record by calling an internal createAlignmentVote, the voteId can simply be the result of timestamp divided by termDuration..
  3. no need for _activeAlignment or _activeCitizens.
  4. no need for _voteAlignmentVoters,_voteCitizensVoters, _voteWeightSnapshot assume voting weight for citizen/alignment wont change during vote. check member join date to verify he can participate in current vote.
  5. totalUnits + totalWeights not required for "vote" record in createAlignmentVote
  6. voteRecipientWeightedVotes should be divided by BASIS_POINTS
    7, finalizeAlignmentVote step is not required.
  7. in executeVote each member units is simply the value he got in voteRecipientWeightedVotes

@sirpy

sirpy commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
  1. the fields should be written on chain. for distribution strategy it should be a url to a post on our forum.
  2. quarterly/term vote: modulo timestamp by termDuration if result is between 0 and votingTermLength then we are in voting period time frame, first vote will "create" the vote record by calling an internal createAlignmentVote, the voteId can simply be the result of timestamp divided by termDuration..
  3. no need for _activeAlignment or _activeCitizens.
  4. no need for _voteAlignmentVoters,_voteCitizensVoters, _voteWeightSnapshot assume voting weight for citizen/alignment wont change during vote. check member join date to verify he can participate in current vote.
  5. totalUnits + totalWeights not required for "vote" record in createAlignmentVote
  6. voteRecipientWeightedVotes should be divided by BASIS_POINTS
    7, finalizeAlignmentVote step is not required.
  7. in executeVote each member units is simply the value he got in voteRecipientWeightedVotes

@copilot

Copilot AI commented Jun 11, 2026

Copy link
Copy Markdown
Author
  1. the fields should be written on chain. for distribution strategy it should be a url to a post on our forum.
  2. quarterly/term vote: modulo timestamp by termDuration if result is between 0 and ...

Addressed in 6fb9881.

Copilot AI requested a review from sirpy June 11, 2026 13:38
Copilot finished work on behalf of sirpy June 11, 2026 13:38
@L03TJ3

L03TJ3 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Last fixes include:

  • bytesize was exceeding the allowed limit on deployment. removing the flowsplitter logic brought it below the limit
  • readable revert/error names
  • voting term logic was based on unix timestamp and 90 day chunks since 1 january 1970.

Related to 'removing flowsplitter logic': the contract should only govern the update of units in a flowsplitter pool and adding/removing members.

@sirpy sirpy marked this pull request as ready for review June 24, 2026 12:19
Copilot AI review requested due to automatic review settings June 24, 2026 12:19

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • In registerAndStake, amount can become negative when the caller’s existing stake already exceeds the new minimum, which then gets cast to uint and fed into _registerMember; this will overflow and revert in 0.8.x — consider clamping the delta to zero (or reverting explicitly) when members[msg.sender].stakedAmount >= minimumStake[house] before passing it along.
  • The isVotingPeriod check uses <= votingTermLength, which makes the voting window effectively votingTermLength + 1 seconds long; if you want an exact-length window, consider switching this to a strict < votingTermLength comparison.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `registerAndStake`, `amount` can become negative when the caller’s existing stake already exceeds the new minimum, which then gets cast to `uint` and fed into `_registerMember`; this will overflow and revert in 0.8.x — consider clamping the delta to zero (or reverting explicitly) when `members[msg.sender].stakedAmount >= minimumStake[house]` before passing it along.
- The `isVotingPeriod` check uses `<= votingTermLength`, which makes the voting window effectively `votingTermLength + 1` seconds long; if you want an exact-length window, consider switching this to a strict `< votingTermLength` comparison.

## Individual Comments

### Comment 1
<location path="contracts/governance/GoodDaoHouses.sol" line_range="234-243" />
<code_context>
+		string calldata distributionStrategy
+	) external whenNotPaused {
+		// Collect only the missing delta between current stake and the house minimum.
+		int amount = int(minimumStake[house]) -
+			int(members[msg.sender].stakedAmount);
+		if (amount > 0) {
+			require(
+				_goodDollar().transferFrom(msg.sender, address(this), uint256(amount)),
+				"G$ transferFrom"
+			);
+		}
+
+		_registerMember(
+			msg.sender,
+			house,
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid casting potentially negative `int` to `uint` in `registerAndStake`.

If a member’s existing `stakedAmount``minimumStake[house]`, `amount` is ≤ 0. You correctly skip the transfer when `amount <= 0`, but still pass `uint(amount)` to `_registerMember`, which will revert for negative `amount` due to the `int -> uint` cast in Solidity 0.8.

Compute a nonnegative delta before casting, e.g.:
```solidity
int256 delta = int256(minimumStake[house]) - int256(members[msg.sender].stakedAmount);
uint256 stakeDelta = delta > 0 ? uint256(delta) : 0;
if (stakeDelta > 0) {
    require(
        _goodDollar().transferFrom(msg.sender, address(this), stakeDelta),
        "G$ transferFrom"
    );
}
_registerMember(
    msg.sender,
    house,
    stakeDelta,
    name,
    socialLinks,
    projectWebpage,
    missionStatement,
    distributionStrategy
);
```
This avoids casting negative values and prevents over-staked calls from reverting.
</issue_to_address>

### Comment 2
<location path="contracts/governance/GoodDaoHouses.sol" line_range="454-456" />
<code_context>
+		require(!vote.executed, "Vote executed");
+
+		// Translate finalized weighted votes into FlowSplitter unit updates.
+		for (uint256 i = 0; i < count; i++) {
+			address recipient = recipients[i];
+			uint128 units = uint128(voteRecipientWeightedVotes[voteId][recipient]);
+			flowMembers[i] = IFlowSplitter.Member({
+				account: recipient,
</code_context>
<issue_to_address>
**issue (bug_risk):** Casting accumulated vote weights from `uint256` to `uint128` can silently truncate.

Here `voteRecipientWeightedVotes[voteId][recipient]` is a `uint256` but is cast to `uint128` without validation. If total weights exceed `2**128 - 1`, this will silently truncate and produce incorrect weights for the FlowSplitter.

To avoid this, either:
- Enforce and document an invariant that total weights always fit in `uint128`; or
- Add an explicit bound check before casting:
```solidity
uint256 raw = voteRecipientWeightedVotes[voteId][recipient];
require(raw <= type(uint128).max, "Units overflow");
uint128 units = uint128(raw);
```
so that overflow fails loudly instead of corrupting the result.
</issue_to_address>

### Comment 3
<location path="contracts/governance/GoodDaoHouses.sol" line_range="464-465" />
<code_context>
+			weights[i] = units;
+		}
+
+		IFlowSplitter(flowConfig.splitter).updateMembersUnits(
+			flowConfig.poolId,
+			flowMembers
+		);
</code_context>
<issue_to_address>
**issue:** Add validation that FlowSplitter configuration is present before executing a vote.

`executeVote` assumes `flowConfig.splitter` and `flowConfig.poolId` are initialized. If `configureFlowSplitter` wasn’t called, `splitter` will be `address(0)` and the call will revert with a low-level error; a zero `poolId` may also be rejected by the FlowSplitter.

Add explicit guards, e.g.:
```solidity
require(flowConfig.splitter != address(0), "FlowSplitter not configured");
require(flowConfig.poolId != 0, "Pool not configured");
```
so misconfiguration fails clearly instead of via a generic revert.
</issue_to_address>

### Comment 4
<location path="test/governance/GoodDaoHouses.test.ts" line_range="153" />
<code_context>
+    expect(activeAlignmentMembers).to.deep.equal([alignmentOne.address]);
+  });
+
+  it("lets admin or committee set the voting schedule anchor and term lengths", async () => {
+    const { admin, committee, houses } = await loadFixture(fixture);
+    const latestBlock = await ethers.provider.getBlock("latest");
</code_context>
<issue_to_address>
**suggestion (testing):** Add negative-path tests for voting schedule configuration

Currently only the happy path for `setVotingSchedule` is tested. Please also cover:
- Calls from non-admin / non-committee signers reverting as expected.
- Invalid parameter combinations (e.g. `votingTermLength > termDuration`, zero durations, or disallowed past cycle start) reverting with the correct error.
This will better lock in access control and validation behavior.

Suggested implementation:

```typescript
    const cycleStartTime = (await houses.cycleStartTime()).toNumber();
    const termDuration = (await houses.termDuration()).toNumber();
    const delta =
      latestBlock.timestamp < cycleStartTime
        ? cycleStartTime - latestBlock.timestamp
        : (() => {
            const offset = (latestBlock.timestamp - cycleStartTime) % termDuration;
            return offset === 0 ? 1 : termDuration - offset + 1;
          })();

    const newCycleStartTime = latestBlock.timestamp + delta;
    const validVotingTermLength = Math.floor(termDuration / 2);

    // happy-path: admin configures schedule
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, validVotingTermLength),
    )
      .to.emit(houses, "VotingScheduleUpdated")
      .withArgs(newCycleStartTime, termDuration, validVotingTermLength);

    // negative-path: non-admin / non-committee cannot configure schedule
    const { stranger } = await loadFixture(fixture);
    await expect(
      houses
        .connect(stranger)
        .setVotingSchedule(newCycleStartTime, termDuration, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "OnlyAdminOrCommittee");

    // negative-path: voting term longer than term duration
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, termDuration + 1),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: zero term duration
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, 0, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: zero voting term length
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, 0),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: cycle start time in the past
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(latestBlock.timestamp - 1, termDuration, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

```

To make this compile and match your actual contracts/test helpers you’ll likely need to:
1. Adjust the `setVotingSchedule` call signature and argument ordering if it differs (e.g. if it accepts `(cycleStart, votingTermLength, termDuration)` or additional parameters).
2. Replace `"VotingScheduleUpdated"` with the actual event name (or drop the `.to.emit` if no event is emitted).
3. Replace the custom error identifiers `"OnlyAdminOrCommittee"` and `"InvalidVotingSchedule"` with the actual custom error names (or `.to.be.revertedWith` messages) used in `GoodDaoHouses`.
4. Ensure `fixture` returns a `stranger` or similar non-privileged signer (e.g. `{ admin, committee, houses, stranger }`). If it currently only returns `{ admin, committee, houses }`, extend it accordingly or derive `stranger` via `ethers.getSigners()`.
5. If you prefer separate test cases instead of a single expanded one, you can extract the negative-path sections into their own `it(...)` blocks reusing the same fixture setup.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread contracts/governance/GoodDaoHouses.sol Outdated
Comment thread contracts/governance/GoodDaoHouses.sol Outdated
Comment thread contracts/governance/GoodDaoHouses.sol
expect(activeAlignmentMembers).to.deep.equal([alignmentOne.address]);
});

it("lets admin or committee set the voting schedule anchor and term lengths", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add negative-path tests for voting schedule configuration

Currently only the happy path for setVotingSchedule is tested. Please also cover:

  • Calls from non-admin / non-committee signers reverting as expected.
  • Invalid parameter combinations (e.g. votingTermLength > termDuration, zero durations, or disallowed past cycle start) reverting with the correct error.
    This will better lock in access control and validation behavior.

Suggested implementation:

    const cycleStartTime = (await houses.cycleStartTime()).toNumber();
    const termDuration = (await houses.termDuration()).toNumber();
    const delta =
      latestBlock.timestamp < cycleStartTime
        ? cycleStartTime - latestBlock.timestamp
        : (() => {
            const offset = (latestBlock.timestamp - cycleStartTime) % termDuration;
            return offset === 0 ? 1 : termDuration - offset + 1;
          })();

    const newCycleStartTime = latestBlock.timestamp + delta;
    const validVotingTermLength = Math.floor(termDuration / 2);

    // happy-path: admin configures schedule
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, validVotingTermLength),
    )
      .to.emit(houses, "VotingScheduleUpdated")
      .withArgs(newCycleStartTime, termDuration, validVotingTermLength);

    // negative-path: non-admin / non-committee cannot configure schedule
    const { stranger } = await loadFixture(fixture);
    await expect(
      houses
        .connect(stranger)
        .setVotingSchedule(newCycleStartTime, termDuration, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "OnlyAdminOrCommittee");

    // negative-path: voting term longer than term duration
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, termDuration + 1),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: zero term duration
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, 0, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: zero voting term length
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, 0),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: cycle start time in the past
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(latestBlock.timestamp - 1, termDuration, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

To make this compile and match your actual contracts/test helpers you’ll likely need to:

  1. Adjust the setVotingSchedule call signature and argument ordering if it differs (e.g. if it accepts (cycleStart, votingTermLength, termDuration) or additional parameters).
  2. Replace "VotingScheduleUpdated" with the actual event name (or drop the .to.emit if no event is emitted).
  3. Replace the custom error identifiers "OnlyAdminOrCommittee" and "InvalidVotingSchedule" with the actual custom error names (or .to.be.revertedWith messages) used in GoodDaoHouses.
  4. Ensure fixture returns a stranger or similar non-privileged signer (e.g. { admin, committee, houses, stranger }). If it currently only returns { admin, committee, houses }, extend it accordingly or derive stranger via ethers.getSigners().
  5. If you prefer separate test cases instead of a single expanded one, you can extract the negative-path sections into their own it(...) blocks reusing the same fixture setup.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an MVP implementation of the GoodDaoHouses governance contract, including a minimal FlowSplitter integration interface, a mock FlowSplitter for testing, and a dedicated Hardhat test suite covering membership and term-based voting/execution flows.

Changes:

  • Added GoodDaoHouses upgradeable governance contract with Citizens/Alignment membership, staking, and term voting/execution that updates FlowSplitter units.
  • Added a minimal IFlowSplitter interface plus a MockFlowSplitter implementation for deterministic governance testing.
  • Added targeted governance tests for registration/staking, approval/revocation/unstake flows, vote lifecycle, and FlowSplitter unit updates.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
contracts/governance/GoodDaoHouses.sol New governance contract implementing house membership, staking, term voting, and FlowSplitter execution boundary.
contracts/governance/IFlowSplitter.sol Minimal interface surface used by GoodDaoHouses to drive pool member unit updates.
contracts/mocks/MockFlowSplitter.sol Mock FlowSplitter + pool used to test execution behavior without external dependencies.
test/governance/GoodDaoHouses.test.ts Test suite validating membership lifecycle, voting window behavior, and FlowSplitter unit updates/cleanup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread contracts/governance/GoodDaoHouses.sol
Comment thread contracts/governance/GoodDaoHouses.sol
Comment thread contracts/governance/GoodDaoHouses.sol
Comment thread contracts/governance/GoodDaoHouses.sol
Comment thread contracts/governance/GoodDaoHouses.sol Outdated
Comment thread contracts/governance/IFlowSplitter.sol Outdated
Comment thread contracts/governance/GoodDaoHouses.sol
Comment thread contracts/governance/GoodDaoHouses.sol
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@sirpy

sirpy commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@copilot fix unresolved code review conversations. make sure fixes are matched by relevant unit tests

Copilot AI commented Jun 29, 2026

Copy link
Copy Markdown
Author

@copilot fix unresolved code review conversations. make sure fixes are matched by relevant unit tests

Fixed in the latest commit. Here's what was addressed:

  1. _registerMember() – invalid house enum — added require(uint8(house) <= uint8(House.Alignment), "Invalid house") at the top of the function so an out-of-range value reverts before any state is written.

  2. revokeMember() – non-member revocation — added require(members[account].status != MemberStatus.None, "Not a member") so the function cannot create a phantom Revoked record for an address that never registered.

  3. IFlowSplitter.sol – duplicate ISuperToken — removed the empty interface ISuperToken {} stub and replaced it with an import of the existing contracts/token/superfluid/ISuperToken.sol.

  4. registerAndStake() – negative-delta underflow — renamed int amount to int256 delta and introduced a separate uint256 transferAmount (0 when delta ≤ 0). Only the non-negative case is transferred and only that amount is forwarded to _registerMember, preventing the uint(negative) underflow.

Each fix is accompanied by a new unit test in test/governance/GoodDaoHouses.test.ts.

Copilot finished work on behalf of sirpy June 29, 2026 10:05
@sirpy

sirpy commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@copilot add the suggested tests
suggestion (testing): Add negative-path tests for voting schedule configuration

Currently only the happy path for setVotingSchedule is tested. Please also cover:

  • Calls from non-admin / non-committee signers reverting as expected.
  • Invalid parameter combinations (e.g. votingTermLength > termDuration, zero durations, or disallowed past cycle start) reverting with the correct error.
    This will better lock in access control and validation behavior.

Suggested implementation:

    const cycleStartTime = (await houses.cycleStartTime()).toNumber();
    const termDuration = (await houses.termDuration()).toNumber();
    const delta =
      latestBlock.timestamp < cycleStartTime
        ? cycleStartTime - latestBlock.timestamp
        : (() => {
            const offset = (latestBlock.timestamp - cycleStartTime) % termDuration;
            return offset === 0 ? 1 : termDuration - offset + 1;
          })();

    const newCycleStartTime = latestBlock.timestamp + delta;
    const validVotingTermLength = Math.floor(termDuration / 2);

    // happy-path: admin configures schedule
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, validVotingTermLength),
    )
      .to.emit(houses, "VotingScheduleUpdated")
      .withArgs(newCycleStartTime, termDuration, validVotingTermLength);

    // negative-path: non-admin / non-committee cannot configure schedule
    const { stranger } = await loadFixture(fixture);
    await expect(
      houses
        .connect(stranger)
        .setVotingSchedule(newCycleStartTime, termDuration, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "OnlyAdminOrCommittee");

    // negative-path: voting term longer than term duration
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, termDuration + 1),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: zero term duration
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, 0, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: zero voting term length
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(newCycleStartTime, termDuration, 0),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

    // negative-path: cycle start time in the past
    await expect(
      houses
        .connect(admin)
        .setVotingSchedule(latestBlock.timestamp - 1, termDuration, validVotingTermLength),
    ).to.be.revertedWithCustomError(houses, "InvalidVotingSchedule");

To make this compile and match your actual contracts/test helpers you’ll likely need to:

  1. Adjust the setVotingSchedule call signature and argument ordering if it differs (e.g. if it accepts (cycleStart, votingTermLength, termDuration) or additional parameters).
  2. Replace "VotingScheduleUpdated" with the actual event name (or drop the .to.emit if no event is emitted).
  3. Replace the custom error identifiers "OnlyAdminOrCommittee" and "InvalidVotingSchedule" with the actual custom error names (or .to.be.revertedWith messages) used in GoodDaoHouses.
  4. Ensure fixture returns a stranger or similar non-privileged signer (e.g. { admin, committee, houses, stranger }). If it currently only returns { admin, committee, houses }, extend it accordingly or derive stranger via ethers.getSigners().
  5. If you prefer separate test cases instead of a single expanded one, you can extract the negative-path sections into their own it(...) blocks reusing the same fixture setup.

@copilot fix and add tests
issue (bug_risk): Casting accumulated vote weights from uint256 to uint128 can silently truncate.

Here voteRecipientWeightedVotes[voteId][recipient] is a uint256 but is cast to uint128 without validation. If total weights exceed 2**128 - 1, this will silently truncate and produce incorrect weights for the FlowSplitter.

To avoid this, either:

  • Enforce and document an invariant that total weights always fit in uint128; or
  • Add an explicit bound check before casting:
uint256 raw = voteRecipientWeightedVotes[voteId][recipient];
require(raw <= type(uint128).max, "Units overflow");
uint128 units = uint128(raw);

so that overflow fails loudly instead of corrupting the result.

Copilot AI commented Jun 29, 2026

Copy link
Copy Markdown
Author

@copilot add the suggested tests
suggestion (testing): Add negative-path tests for voting schedule configuration

...

Done in the latest commit. Here's what was added/changed:

Contract (GoodDaoHouses.sol)

  • setVotingSchedule: added require(newVotingTermLength > 0, "Vote term=0") guard alongside the existing checks, so all three invalid-parameter paths now revert with a clear message.
  • executeVote: added explicit require(raw <= type(uint128).max, "Units overflow") before the uint128(raw) cast. Note: with the current fixed voter weights (max 400 000 per vote), overflow is practically unreachable — the check is a defensive safety net.

Tests (GoodDaoHouses.test.ts)

  • Added stranger signer to the shared fixture.
  • setVotingSchedule: non-admin/committee reverts — verifies "Not admin/committee".
  • setVotingSchedule: votingTermLength > termDuration reverts — verifies "Vote term > term".
  • setVotingSchedule: zero termDuration reverts — verifies "Term=0".
  • setVotingSchedule: zero votingTermLength reverts — verifies "Vote term=0".
  • executeVote: uint128 cast safety — verifies that a normal two-voter run completes without hitting "Units overflow" and that the stored FlowSplitter units are non-zero.

Copilot finished work on behalf of sirpy June 29, 2026 11:06
@sirpy

sirpy commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@copilot add the negative test case executeVote: uint128 cast safety

Copilot AI commented Jun 29, 2026

Copy link
Copy Markdown
Author

@copilot add the negative test case executeVote: uint128 cast safety

Done in the latest commit. Since normal voting accumulation can never reach type(uint128).max (fixed weights top out at ~400K per voter), the test uses a GoodDaoHousesHarness contract that extends GoodDaoHouses with a committee-gated setVoteWeightForTest setter. The approach required changing voteRecipientWeightedVotes from private to internal (zero external-security impact).

The negative test (executeVote: uint128 cast safety – reverts when accumulated weight exceeds uint128 max):

  1. Deploys GoodDaoHousesHarness via UUPS proxy
  2. Registers and approves two alignment members, configures the FlowSplitter pool
  3. Casts a normal vote (creating the vote record and registering the recipient)
  4. Moves past the voting window
  5. Injects 2**128 (= type(uint128).max + 1) directly via setVoteWeightForTest
  6. Asserts executeVote reverts with "Units overflow"

Copilot finished work on behalf of sirpy June 29, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Plan]: GoodDaoHouses MVP implementation outline

4 participants