Skip to content

feat: multi-round stacks — let a stack run N cycles instead of one #171

Description

@franrolotti

Summary

Let a stack be configured to run the rotation N times (each member claims N times) instead of exactly once. Requires contract changes in saving-circles plus a redeploy.

Motivation

A stack currently runs the rotation exactly once, and that's baked into the contract rather than being a product choice:

  • hasClaimed is a single bool per member (MemberState), never reset — a member structurally cannot claim twice
  • the round's claimer is circleMembers[_id][currentRound], and currentRound >= members.length means expired
  • circleEnd = effectiveCircleStartTime + (depositInterval * members.length)
  • the payout is depositAmount * members.length

So a group that wants to keep saving together has no path other than creating a whole new stack.

#170 gives that path without a contract change — replicate into a new circle id. This issue is the durable version: one stack, one continuous history, no per-cycle re-enrollment, configured up front. Worth doing when the contract is next redeployed, alongside the circleState() / start() terminal-state fix described in #160.

Required contract changes (saving-circles)

  1. Circle gains a rounds / cycles field. Circle is a mapping value in an upgradeable contract — this needs an explicit storage-layout / upgrade-safety review, not an assumption that appending is free.
  2. Per-cycle claim tracking. hasClaimed (bool) becomes per-cycle state; the round's claimer becomes circleMembers[_id][round % members.length]. roundDeposits is already keyed by round, so deposits should carry over as-is — worth confirming.
  3. Timing. circleEnd = start + depositInterval * members.length * cycles; _currentRoundIndex, _roundEndTime and _isDecommissionable re-checked against a round index that now exceeds the roster length.
  4. Terminal state. circleState() still has no completed value — fold the Completed stack shows "Not started" and Start button is clickable again #160 contract fix (terminal state + start() rejecting an already-run circle) into the same redeploy.
  5. Gas. decommission()'s refund loop is already O(n²) over members; with cycles the refund surface grows. Check the ceiling at MAX_MEMBERS (25) × N cycles — this may cap how large N can be.

App scope

Acceptance criteria

  • A stack can be created with N > 1 cycles and completes all N without manual intervention
  • Each member claims exactly once per cycle, in the same rotation order
  • After the final cycle the stack reports a terminal finished state and cannot be restarted (closes the contract half of Completed stack shows "Not started" and Start button is clickable again #160)
  • Deposit windows, decommission and refunds behave correctly in cycles 2..N, not just cycle 1
  • N = 1 behaves exactly as today (no regression for existing stacks)
  • Existing on-chain circles are unaffected by the upgrade

Open questions

  • Fixed N vs open-ended? A stack that "runs until the group stops" is the more natural product framing, but circleEnd and the decommission refund path both need a bound, and gas grows with N. Suggest scoping this issue to a fixed N chosen at creation, with a sensible upper limit.
  • Can N be changed after start? Suggest no for v1.
  • Existing circles — is anything migrated, or does multi-cycle only apply to circles created after the upgrade? Suggest the latter.

Related / avoid duplication

Source

Spun out of #160 — the restart path was found as a bug during that fix; the feature framing came out of that discussion (Fran, Jul 2026).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions