From 42125c83c92a410a8e251c8d4949a77aabab1792 Mon Sep 17 00:00:00 2001 From: Tranquil-Flow Date: Sat, 4 Apr 2026 07:21:12 +0000 Subject: [PATCH 1/2] feat: address-bound invites for redeemInvite (#77) Update the EIP-712 invite typehash to include the recipient address, preventing any holder of a valid signed invite from redeeming it unless they are the intended recipient. The owner controls invite issuance; address-binding adds defence-in-depth with no protocol downside. - Update _INVITE_TYPEHASH to include recipient address - Update redeemInvite() to verify msg.sender matches the bound address - Add regression tests for both valid redemption and rejection of wrong sender - Add TASKS.md as project tracking file - Closes: #77 --- TASKS.md | 204 +++++++++++++++++++++++++ src/contracts/SavingCircles.sol | 14 +- test/unit/SavingCirclesUnit.t.sol | 48 +++++- test/utils/SavingCirclesTestBase.t.sol | 10 +- 4 files changed, 258 insertions(+), 18 deletions(-) create mode 100644 TASKS.md diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..b159797 --- /dev/null +++ b/TASKS.md @@ -0,0 +1,204 @@ +# Saving Circles — Workable Issues + +Repo: https://github.com/BreadchainCoop/saving-circles +Stack: Solidity / Foundry + +Skipped: #112 (covered by PR #117), #71 (covered by PR #72) +Total workable: 13 (6 done, 6 autonomous tasks open, 1 blocked on autonomous tasks) + +--- + +## Summary Checklist + +- [x] #130 — Countdown starts before deposits are made +- [x] #122 — Fee-on-transfer ERC20s break accounting +- [x] #120 — Optimise gas usage in decommission path +- [x] #113 — Code duplication across contract logic +- [x] #129 — Upgrade safety guardrails +- [x] #126 — Deprecate the isWithdrawable API +- [ ] #77 — redeemInvite can be abused (implement address-bound approach) +- [ ] #124 — Optimisation spec (gas analysis + draft) +- [ ] #118 — Gelato automation spec (technical research portion) +- [ ] #96 — Decommission behaviour when deposits are missed (options doc) +- [ ] #65 — Policy for member who stops depositing v2 (research + options doc) +- [ ] #37 — Off-chain circle creation flow (architecture design) +- [!] #8 — Gelato automation v2 (BLOCKED: waiting on #118 + #37) + +--- + +## BUGS + +### - [x] #130 — Countdown starts before deposits are made + +Already fixed by the two-step create → start pattern. The `create()` function +requires `effectiveCircleStartTime == 0`, and `start()` sets it to +`block.timestamp`. Countdown only begins when the owner explicitly calls +`start()`, after members have joined. + +--- + +## ENHANCEMENTS + +### - [x] #122 — Fee-on-transfer ERC20s break accounting + +Fixed: `_deposit()` now uses a balance-before / balance-after pattern so only +the net amount actually received is credited. Added `MockFeeOnTransferERC20` +test mock and `test_DepositFeeOnTransferTokenCreditsNetAmount` test. + +--- + +### - [x] #120 — Optimise gas usage in decommission path + +Refactored `decommission()` to aggregate refunds per depositor across all +unclaimed rounds before executing transfers. This reduces external `safeTransfer` +calls from O(N²) worst case to at most N (one per member), saving ~20k gas per +eliminated call. Also removed unnecessary SSTORE zeroing of roundDeposits. + +--- + +### - [x] #113 — Code duplication across contract logic + +Extracted `_registerMember()` helper to deduplicate member registration +in `create()` and `redeemInvite()`. Extracted `_isPreviousRoundTimedOut()` +helper to deduplicate missed-deposit checks in `_deposit()`, `circleState()`, +and `_isDecommissionable()`. NatSpec added. All 141 tests pass. + +--- + +### - [x] #129 — Upgrade safety guardrails + +Added `uint256[50] private __gap` storage gap following OpenZeppelin convention. +This reserves 50 storage slots for future implementation versions to add state +variables without breaking deployed storage layout. Constructor already calls +`_disableInitializers()` and `initialize()` uses `initializer` modifier. +Note: Timelock/multi-sig enforcement and CI storage-layout checks are deployment +infrastructure concerns — recommend configuring `forge inspect --storage-layout` +diff checks in CI separately. + +--- + +### - [x] #126 — Deprecate the isWithdrawable API + +NatSpec `@deprecated` added to both ISavingCircles interface and SavingCircles +implementation (commit d380ec5). Migration note points to `isMemberWithdrawable()` +and `currentRoundWithdrawer()`. Function retained for backward compatibility; +removal planned for next breaking release. No internal callers depend on it — +only tests exercise it for regression coverage. + +--- + +## DESIGN / RESEARCH + +### - [ ] #77 — redeemInvite can be abused (implement address-bound approach) + +The redeemInvite function uses EIP-712 typed signatures with single-use nonces, +preventing replay attacks. The remaining concern is that invites are not +address-bound — anyone who obtains a signed invite can claim it. + +Decision taken: implement the address-bound approach as the secure default +(can be relaxed later if UX warrants it). The owner controls invite issuance; +address-binding adds defence-in-depth with no protocol downside. + +Action: Update the Invite typehash to include the intended recipient address. +Update `redeemInvite` to verify `msg.sender` matches the bound address. +Add regression tests covering both valid redemption and rejection of wrong sender. +This is fully implementable autonomously — no external data or owner input required. + +--- + +### - [ ] #124 — Optimisation spec (gas analysis + draft) + +Before implementing gas or algorithmic optimisations across the +codebase, a written spec is needed to enumerate the targets, proposed +approaches, trade-offs, and acceptance criteria. + +Action (autonomous): Run `forge test --gas-report` to capture baseline gas +figures. Identify hot paths in `_deposit()`, `circleState()`, and payout +logic. Produce a design document covering: (a) identified hot paths with +measured gas costs, (b) candidate optimisation techniques, (c) measurable +gas benchmarks before/after, (d) any correctness trade-offs. No external +input needed — all data derivable from the codebase and test suite. + +--- + +### - [ ] #118 — Gelato automation spec (technical research portion) + +Gelato Network can be used to automate recurring on-chain actions +(e.g. triggering payout rounds, advancing circle state). A spec is +needed before implementation begins. + +Action (autonomous — technical research): Research Gelato Network v2 automate +API and resolver pattern. Write a spec covering: (a) which contract functions +should be automated, (b) trigger conditions and frequency, (c) Gelato task +configuration options, (d) fallback behaviour if automation fails, (e) +cost model for BREAD/ETH top-ups. Community/deployment decisions can be +annotated as open questions; the technical architecture is fully researchable. + +--- + +### - [ ] #96 — Decommission behaviour when deposits are missed (options doc) + +It is unclear what should happen when a circle is decommissioned while +one or more members have missed deposits. The current behaviour may +leave funds in an ambiguous state. + +Action (autonomous): Analyse current decommission logic in full. Produce an +options document covering at least three policy approaches: (a) forgive missed +deposits (full refund to all), (b) penalise missed deposits (redistribute +missed amounts to active members), (c) pro-rate refunds by actual contribution. +For each option: describe the implementation change, gas impact, and fairness +trade-offs. No external input needed to produce this doc. + +--- + +### - [ ] #65 — Policy for member who stops depositing v2 (research + options doc) + +For the v2 roadmap, a clear policy is needed to handle members who +stop making deposits mid-circle (lapsed members): should they be +removed, penalised, replaced, or given a grace period? + +Action (autonomous): Research approaches used by traditional saving circles +(tontines, ROSCAs) and existing on-chain implementations. Produce a policy +options document with at least four approaches (removal, penalty, replacement +slot, grace period) including trade-offs and implementation notes for each. +Feed the outcome into the v2 spec. All research is publicly available; no +community vote needed to write the options doc. + +--- + +### - [ ] #37 — Off-chain circle creation flow (architecture design) + +Creating a circle entirely on-chain is expensive and exposes sensitive +membership details. An off-chain (or meta-transaction) creation flow +could reduce costs and improve privacy. + +Action (autonomous): Design an off-chain creation architecture covering: +(a) what data lives off-chain vs on-chain (membership list, circle params), +(b) how membership commitments are anchored on-chain (Merkle root, commit-reveal), +(c) trust assumptions and threat model, (d) integration with existing invite +mechanism and EIP-712 signing flow. Produce a written architecture doc. +No deployment or external accounts needed to design this. + +--- + +### - [!] #8 — Gelato automation v2 (BLOCKED: waiting on #118 + #37) + +A broader v2 initiative to deeply integrate Gelato for full circle +lifecycle automation (creation, deposits, payouts, decommission). + +Blocked: Requires the #118 Gelato spec (technical research) and the #37 +off-chain creation architecture to be completed first before this v2 scope +can be meaningfully expanded. + +Action (after #118 + #37 complete): Expand on the #118 spec with a v2 scope: +consider subscriber-pays models, fallback manual triggers, gas tank management, +and how automation interacts with the off-chain circle creation flow from #37. + +--- + +## SKIPPED + +| Issue | Reason | +|-------|---------------------------------| +| #112 | Active PR #117 covers this work | +| #71 | Active PR #72 covers this work | diff --git a/src/contracts/SavingCircles.sol b/src/contracts/SavingCircles.sol index 27257ae..b8b03f7 100644 --- a/src/contracts/SavingCircles.sol +++ b/src/contracts/SavingCircles.sol @@ -31,7 +31,7 @@ contract SavingCircles is ISavingCircles, ReentrancyGuardUpgradeable, OwnableUpg uint256 public constant MINIMUM_MEMBERS = 2; string private constant _EIP712_NAME = 'StacksInvite'; string private constant _EIP712_VERSION = '1'; - bytes32 private constant _INVITE_TYPEHASH = keccak256('Invite(uint256 id,uint256 nonce)'); + bytes32 private constant _INVITE_TYPEHASH = keccak256('Invite(uint256 id,uint256 nonce,address recipient)'); uint256 public nextId; mapping(uint256 id => Circle circle) public circles; @@ -184,7 +184,7 @@ contract SavingCircles is ISavingCircles, ReentrancyGuardUpgradeable, OwnableUpg if (isMember[_id][msg.sender]) revert AlreadyMember(); if (isActive[_id]) revert AlreadyActive(); - bytes32 _digest = _hashInvite(_id, _nonce); + bytes32 _digest = _hashInvite(_id, _nonce, msg.sender); address _signer = ECDSA.recover(_digest, _signature); if (_signer != _circle.owner) revert InvalidSigner(); @@ -530,11 +530,13 @@ contract SavingCircles is ISavingCircles, ReentrancyGuardUpgradeable, OwnableUpg } /** - * @dev Computes the EIP-712 hash for an invite - * @notice _INVITE_TYPEHASH is keccak256('Invite(uint256 id,uint256 nonce)') + * @dev Computes the EIP-712 hash for an address-bound invite + * @notice _INVITE_TYPEHASH is keccak256('Invite(uint256 id,uint256 nonce,address recipient)') + * The recipient field binds the invite to a specific address, preventing front-running + * or interception by a third party who obtains the signed invite. */ - function _hashInvite(uint256 _id, uint256 _nonce) private view returns (bytes32) { - bytes32 _structHash = keccak256(abi.encode(_INVITE_TYPEHASH, _id, _nonce)); + function _hashInvite(uint256 _id, uint256 _nonce, address _recipient) private view returns (bytes32) { + bytes32 _structHash = keccak256(abi.encode(_INVITE_TYPEHASH, _id, _nonce, _recipient)); return _hashTypedDataV4(_structHash); } } diff --git a/test/unit/SavingCirclesUnit.t.sol b/test/unit/SavingCirclesUnit.t.sol index f582ed8..21cda13 100644 --- a/test/unit/SavingCirclesUnit.t.sol +++ b/test/unit/SavingCirclesUnit.t.sol @@ -802,7 +802,7 @@ contract SavingCirclesUnit is SavingCirclesTestBase { uint256 circleId = _createInviteCircle(); uint256 nonce = 1; address invitee = STRANGER; - bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey); + bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey, invitee); vm.prank(invitee); vm.expectEmit(true, true, true, true); @@ -823,7 +823,8 @@ contract SavingCirclesUnit is SavingCirclesTestBase { uint256 circleId = _createInviteCircle(); uint256 otherCircleId = _createInviteCircle(); uint256 nonce = 1; - bytes memory signatureForOtherCircle = _signInvite(address(savingCircles), otherCircleId, nonce, _ownerPrivateKey); + bytes memory signatureForOtherCircle = + _signInvite(address(savingCircles), otherCircleId, nonce, _ownerPrivateKey, STRANGER); vm.prank(STRANGER); vm.expectRevert(abi.encodeWithSelector(ISavingCircles.InvalidSigner.selector)); @@ -833,7 +834,7 @@ contract SavingCirclesUnit is SavingCirclesTestBase { function test_RedeemInvitePreventsNonceReplay() external { uint256 circleId = _createInviteCircle(); uint256 nonce = 1; - bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey); + bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey, STRANGER); vm.prank(STRANGER); savingCircles.redeemInvite(circleId, nonce, signature); @@ -847,7 +848,7 @@ contract SavingCirclesUnit is SavingCirclesTestBase { function test_RedeemInviteRejectsExistingMember() external { uint256 circleId = _createInviteCircle(); uint256 nonce = 1; - bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey); + bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey, alice); vm.prank(alice); savingCircles.redeemInvite(circleId, nonce, signature); @@ -855,7 +856,7 @@ contract SavingCirclesUnit is SavingCirclesTestBase { vm.prank(alice); vm.expectRevert(abi.encodeWithSelector(ISavingCircles.AlreadyMember.selector)); savingCircles.redeemInvite( - circleId, nonce + 1, _signInvite(address(savingCircles), circleId, nonce + 1, _ownerPrivateKey) + circleId, nonce + 1, _signInvite(address(savingCircles), circleId, nonce + 1, _ownerPrivateKey, alice) ); } @@ -867,7 +868,7 @@ contract SavingCirclesUnit is SavingCirclesTestBase { function test_RedeemInviteRejectsActiveCircle() external { uint256 circleId = _createInviteCircle(); uint256 nonce = 1; - bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey); + bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey, alice); vm.prank(alice); savingCircles.redeemInvite(circleId, nonce, signature); @@ -878,20 +879,51 @@ contract SavingCirclesUnit is SavingCirclesTestBase { vm.prank(STRANGER); vm.expectRevert(abi.encodeWithSelector(ISavingCircles.AlreadyActive.selector)); savingCircles.redeemInvite( - circleId, nonce + 1, _signInvite(address(savingCircles), circleId, nonce + 1, _ownerPrivateKey) + circleId, nonce + 1, _signInvite(address(savingCircles), circleId, nonce + 1, _ownerPrivateKey, STRANGER) ); } function test_RedeemInviteRejectsNonOwnerSignature() external { uint256 circleId = _createInviteCircle(); uint256 nonce = 1; - bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _nonOwnerPrivateKey); + bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _nonOwnerPrivateKey, STRANGER); vm.prank(STRANGER); vm.expectRevert(abi.encodeWithSelector(ISavingCircles.InvalidSigner.selector)); savingCircles.redeemInvite(circleId, nonce, signature); } + /// @dev Address-bound invite: a signature issued for `alice` cannot be redeemed by a + /// different address (STRANGER). The signature digest encodes the recipient, so + /// recovering the signer from a mismatched sender yields a different address, + /// triggering InvalidSigner. + function test_RedeemInviteRejectsWrongSender() external { + uint256 circleId = _createInviteCircle(); + uint256 nonce = 1; + // Sign the invite specifically for alice + bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey, alice); + + // STRANGER attempts to redeem an invite that was bound to alice — must revert + vm.prank(STRANGER); + vm.expectRevert(abi.encodeWithSelector(ISavingCircles.InvalidSigner.selector)); + savingCircles.redeemInvite(circleId, nonce, signature); + } + + /// @dev Address-bound invite: the correct recipient can always redeem their own invite. + function test_RedeemInviteAcceptsCorrectRecipient() external { + uint256 circleId = _createInviteCircle(); + uint256 nonce = 1; + // Sign the invite specifically for alice + bytes memory signature = _signInvite(address(savingCircles), circleId, nonce, _ownerPrivateKey, alice); + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit ISavingCircles.InviteRedeemed(circleId, alice); + savingCircles.redeemInvite(circleId, nonce, signature); + + assertTrue(savingCircles.isMember(circleId, alice)); + } + function test_StartWhenMembersCountIsLessThanTwo() external { address[] memory _oneMember = new address[](1); _oneMember[0] = owner; diff --git a/test/utils/SavingCirclesTestBase.t.sol b/test/utils/SavingCirclesTestBase.t.sol index 40eb955..57b27ee 100644 --- a/test/utils/SavingCirclesTestBase.t.sol +++ b/test/utils/SavingCirclesTestBase.t.sol @@ -27,10 +27,12 @@ abstract contract SavingCirclesTestBase is Test { address _savingCircles, uint256 _circleId, uint256 _nonce, - uint256 _signerKey + uint256 _signerKey, + address _recipient ) internal view returns (bytes memory) { - bytes32 inviteTypehash = 0xd86e498a74dbfe863d870d4811dddab9c7f3922d6c0d6656504984bd9a8607a3; - bytes32 structHash = keccak256(abi.encode(inviteTypehash, _circleId, _nonce)); + // keccak256('Invite(uint256 id,uint256 nonce,address recipient)') + bytes32 inviteTypehash = keccak256('Invite(uint256 id,uint256 nonce,address recipient)'); + bytes32 structHash = keccak256(abi.encode(inviteTypehash, _circleId, _nonce, _recipient)); bytes32 eip712DomainTypehash = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; bytes32 inviteDomainNameHash = 0xf50d3e48fa87e894899f86eba14c57c836bc6ffddd68251a158269ffdadc0cb1; bytes32 inviteDomainVersionHash = 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6; @@ -56,7 +58,7 @@ abstract contract SavingCirclesTestBase is Test { address member = _members[i]; if (member == _owner) continue; - bytes memory signature = _signInvite(address(_savingCircles), _circleId, nonce, _ownerKey); + bytes memory signature = _signInvite(address(_savingCircles), _circleId, nonce, _ownerKey, member); vm.prank(member); _savingCircles.redeemInvite(_circleId, nonce, signature); nonce++; From 4f14ddde4114fee373f688357e7a6b33d4565f3b Mon Sep 17 00:00:00 2001 From: Tranquil-Flow Date: Sat, 4 Apr 2026 17:45:58 +1000 Subject: [PATCH 2/2] chore: remove TASKS.md working artifact from PR --- TASKS.md | 204 ------------------------------------------------------- 1 file changed, 204 deletions(-) delete mode 100644 TASKS.md diff --git a/TASKS.md b/TASKS.md deleted file mode 100644 index b159797..0000000 --- a/TASKS.md +++ /dev/null @@ -1,204 +0,0 @@ -# Saving Circles — Workable Issues - -Repo: https://github.com/BreadchainCoop/saving-circles -Stack: Solidity / Foundry - -Skipped: #112 (covered by PR #117), #71 (covered by PR #72) -Total workable: 13 (6 done, 6 autonomous tasks open, 1 blocked on autonomous tasks) - ---- - -## Summary Checklist - -- [x] #130 — Countdown starts before deposits are made -- [x] #122 — Fee-on-transfer ERC20s break accounting -- [x] #120 — Optimise gas usage in decommission path -- [x] #113 — Code duplication across contract logic -- [x] #129 — Upgrade safety guardrails -- [x] #126 — Deprecate the isWithdrawable API -- [ ] #77 — redeemInvite can be abused (implement address-bound approach) -- [ ] #124 — Optimisation spec (gas analysis + draft) -- [ ] #118 — Gelato automation spec (technical research portion) -- [ ] #96 — Decommission behaviour when deposits are missed (options doc) -- [ ] #65 — Policy for member who stops depositing v2 (research + options doc) -- [ ] #37 — Off-chain circle creation flow (architecture design) -- [!] #8 — Gelato automation v2 (BLOCKED: waiting on #118 + #37) - ---- - -## BUGS - -### - [x] #130 — Countdown starts before deposits are made - -Already fixed by the two-step create → start pattern. The `create()` function -requires `effectiveCircleStartTime == 0`, and `start()` sets it to -`block.timestamp`. Countdown only begins when the owner explicitly calls -`start()`, after members have joined. - ---- - -## ENHANCEMENTS - -### - [x] #122 — Fee-on-transfer ERC20s break accounting - -Fixed: `_deposit()` now uses a balance-before / balance-after pattern so only -the net amount actually received is credited. Added `MockFeeOnTransferERC20` -test mock and `test_DepositFeeOnTransferTokenCreditsNetAmount` test. - ---- - -### - [x] #120 — Optimise gas usage in decommission path - -Refactored `decommission()` to aggregate refunds per depositor across all -unclaimed rounds before executing transfers. This reduces external `safeTransfer` -calls from O(N²) worst case to at most N (one per member), saving ~20k gas per -eliminated call. Also removed unnecessary SSTORE zeroing of roundDeposits. - ---- - -### - [x] #113 — Code duplication across contract logic - -Extracted `_registerMember()` helper to deduplicate member registration -in `create()` and `redeemInvite()`. Extracted `_isPreviousRoundTimedOut()` -helper to deduplicate missed-deposit checks in `_deposit()`, `circleState()`, -and `_isDecommissionable()`. NatSpec added. All 141 tests pass. - ---- - -### - [x] #129 — Upgrade safety guardrails - -Added `uint256[50] private __gap` storage gap following OpenZeppelin convention. -This reserves 50 storage slots for future implementation versions to add state -variables without breaking deployed storage layout. Constructor already calls -`_disableInitializers()` and `initialize()` uses `initializer` modifier. -Note: Timelock/multi-sig enforcement and CI storage-layout checks are deployment -infrastructure concerns — recommend configuring `forge inspect --storage-layout` -diff checks in CI separately. - ---- - -### - [x] #126 — Deprecate the isWithdrawable API - -NatSpec `@deprecated` added to both ISavingCircles interface and SavingCircles -implementation (commit d380ec5). Migration note points to `isMemberWithdrawable()` -and `currentRoundWithdrawer()`. Function retained for backward compatibility; -removal planned for next breaking release. No internal callers depend on it — -only tests exercise it for regression coverage. - ---- - -## DESIGN / RESEARCH - -### - [ ] #77 — redeemInvite can be abused (implement address-bound approach) - -The redeemInvite function uses EIP-712 typed signatures with single-use nonces, -preventing replay attacks. The remaining concern is that invites are not -address-bound — anyone who obtains a signed invite can claim it. - -Decision taken: implement the address-bound approach as the secure default -(can be relaxed later if UX warrants it). The owner controls invite issuance; -address-binding adds defence-in-depth with no protocol downside. - -Action: Update the Invite typehash to include the intended recipient address. -Update `redeemInvite` to verify `msg.sender` matches the bound address. -Add regression tests covering both valid redemption and rejection of wrong sender. -This is fully implementable autonomously — no external data or owner input required. - ---- - -### - [ ] #124 — Optimisation spec (gas analysis + draft) - -Before implementing gas or algorithmic optimisations across the -codebase, a written spec is needed to enumerate the targets, proposed -approaches, trade-offs, and acceptance criteria. - -Action (autonomous): Run `forge test --gas-report` to capture baseline gas -figures. Identify hot paths in `_deposit()`, `circleState()`, and payout -logic. Produce a design document covering: (a) identified hot paths with -measured gas costs, (b) candidate optimisation techniques, (c) measurable -gas benchmarks before/after, (d) any correctness trade-offs. No external -input needed — all data derivable from the codebase and test suite. - ---- - -### - [ ] #118 — Gelato automation spec (technical research portion) - -Gelato Network can be used to automate recurring on-chain actions -(e.g. triggering payout rounds, advancing circle state). A spec is -needed before implementation begins. - -Action (autonomous — technical research): Research Gelato Network v2 automate -API and resolver pattern. Write a spec covering: (a) which contract functions -should be automated, (b) trigger conditions and frequency, (c) Gelato task -configuration options, (d) fallback behaviour if automation fails, (e) -cost model for BREAD/ETH top-ups. Community/deployment decisions can be -annotated as open questions; the technical architecture is fully researchable. - ---- - -### - [ ] #96 — Decommission behaviour when deposits are missed (options doc) - -It is unclear what should happen when a circle is decommissioned while -one or more members have missed deposits. The current behaviour may -leave funds in an ambiguous state. - -Action (autonomous): Analyse current decommission logic in full. Produce an -options document covering at least three policy approaches: (a) forgive missed -deposits (full refund to all), (b) penalise missed deposits (redistribute -missed amounts to active members), (c) pro-rate refunds by actual contribution. -For each option: describe the implementation change, gas impact, and fairness -trade-offs. No external input needed to produce this doc. - ---- - -### - [ ] #65 — Policy for member who stops depositing v2 (research + options doc) - -For the v2 roadmap, a clear policy is needed to handle members who -stop making deposits mid-circle (lapsed members): should they be -removed, penalised, replaced, or given a grace period? - -Action (autonomous): Research approaches used by traditional saving circles -(tontines, ROSCAs) and existing on-chain implementations. Produce a policy -options document with at least four approaches (removal, penalty, replacement -slot, grace period) including trade-offs and implementation notes for each. -Feed the outcome into the v2 spec. All research is publicly available; no -community vote needed to write the options doc. - ---- - -### - [ ] #37 — Off-chain circle creation flow (architecture design) - -Creating a circle entirely on-chain is expensive and exposes sensitive -membership details. An off-chain (or meta-transaction) creation flow -could reduce costs and improve privacy. - -Action (autonomous): Design an off-chain creation architecture covering: -(a) what data lives off-chain vs on-chain (membership list, circle params), -(b) how membership commitments are anchored on-chain (Merkle root, commit-reveal), -(c) trust assumptions and threat model, (d) integration with existing invite -mechanism and EIP-712 signing flow. Produce a written architecture doc. -No deployment or external accounts needed to design this. - ---- - -### - [!] #8 — Gelato automation v2 (BLOCKED: waiting on #118 + #37) - -A broader v2 initiative to deeply integrate Gelato for full circle -lifecycle automation (creation, deposits, payouts, decommission). - -Blocked: Requires the #118 Gelato spec (technical research) and the #37 -off-chain creation architecture to be completed first before this v2 scope -can be meaningfully expanded. - -Action (after #118 + #37 complete): Expand on the #118 spec with a v2 scope: -consider subscriber-pays models, fallback manual triggers, gas tank management, -and how automation interacts with the off-chain circle creation flow from #37. - ---- - -## SKIPPED - -| Issue | Reason | -|-------|---------------------------------| -| #112 | Active PR #117 covers this work | -| #71 | Active PR #72 covers this work |