feat: Variant D — the foremanless chit (reverse-auction saving circle) - #183
Open
RonTuretzky wants to merge 1 commit into
Open
feat: Variant D — the foremanless chit (reverse-auction saving circle)#183RonTuretzky wants to merge 1 commit into
RonTuretzky wants to merge 1 commit into
Conversation
A permissionless, capital-efficient reverse-auction ROSCA that removes the baseline SavingCircles trust bound and the ~4x over-collateralization of the fixed-rotation collateral variants, while paying patient members interest. Each round's pot is allocated by a sealed-bid (commit/reveal) reverse auction and secured at prize time by two-tier collateral: the winner's bid is withheld as first-loss security and she locks the remainder in cash, so 'withheld + coll == residual' at all times. Every pot is therefore made whole against any set of prized defaulters and no honest member ever loses principal (Theorem D3). Withheld bids are released to the other members as dividends (endogenous interest); non-prized default is handled by a one-round bond plus permissionless substitution; a stalled circle unwinds every member to net-zero via abort. - src/interfaces/IChitSavingCircles.sol: interface + full mechanism NatSpec - src/contracts/ChitSavingCircles.sol: upgradeable implementation - test/unit/ChitSavingCirclesUnit.t.sol: 18 unit tests (D3 safety, dividends, substitution, abort incl. abort-after-substitution, n=2, auction guards) - docs/variant-d-foremanless-chit.md: design rationale Standalone; does not touch SavingCircles/AutomaticSavingCircles.
There was a problem hiding this comment.
Pull request overview
Adds a new standalone “Variant D” mechanism (“foremanless chit”) to the codebase: a permissionless ROSCA that uses a sealed-bid reverse auction per round, prize-time two-tier security (withheld + collateral == residual), substitution for defaulted non-prized slots, and an abort unwind to net-zero on principal.
Changes:
- Introduces
ChitSavingCirclesupgradeable contract implementing Variant D (commit/reveal auction, award/settlement, substitution, abort/refunds). - Adds the public interface
IChitSavingCircles(events/errors/views) and a dedicated Foundry unit test suite. - Adds a design rationale document describing the mechanism, invariants, and limits.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/contracts/ChitSavingCircles.sol |
Implements Variant D core logic: deposits, commit/reveal bids, awarding with security invariants, substitution, and abort refunds. |
src/interfaces/IChitSavingCircles.sol |
Defines Variant D external API: lifecycle methods, bidding, settlement, exits, events, and errors. |
test/unit/ChitSavingCirclesUnit.t.sol |
Adds unit tests covering rotation fallback, auction dividends, prized default safety, substitution, abort behavior, and guards. |
docs/variant-d-foremanless-chit.md |
Documents the design rationale, safety invariant, dividend behavior, and abort/substitution semantics for Variant D. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+261
to
+284
| /// @dev Draw `m` into the pot for each member who did not deposit round `f`. | ||
| function _coverNonDepositors(uint256 id, uint256 f, uint256 m, address[] memory members) internal { | ||
| for (uint256 i = 0; i < members.length; i++) { | ||
| address x = members[i]; | ||
| if (_roundDeposit[id][f][x] >= m) continue; // deposited in full | ||
| if (_prized[id][x]) { | ||
| // Prized: cover from withheld first, then cash lock. Solvent because withheld + coll == | ||
| // remaining*m >= m while an obligation is outstanding. | ||
| uint256 fromWithheld = _withheld[id][x] >= m ? m : _withheld[id][x]; | ||
| _withheld[id][x] -= fromWithheld; | ||
| _coll[id][x] -= (m - fromWithheld); | ||
| _remaining[id][x] -= 1; | ||
| emit Covered(id, x, f, m); | ||
| } else { | ||
| // Non-prized: cover from the one-round bond and flag defaulted. If the bond is already | ||
| // spent the round cannot be completed; revert so the circle can be {abort}ed. | ||
| if (_bond[id][x] < m) revert NotAwardable(); | ||
| _bond[id][x] -= m; | ||
| _defaulted[id][x] = true; | ||
| emit BondCovered(id, x, f, m); | ||
| emit MemberDefaulted(id, x); | ||
| } | ||
| } | ||
| } |
Comment on lines
+158
to
+166
| function _deposit(uint256 id, address member, uint256 value) internal { | ||
| Circle storage circle = _existing(id); | ||
| if (circle.startTime == 0 || _aborted[id]) revert WrongState(); | ||
| if (!_isMember[id][member]) revert NotMember(); | ||
| if (value == 0) revert InvalidParameters(); | ||
|
|
||
| uint256 t = _round(circle); | ||
| if (t >= circle.numSlots) revert WrongState(); // cycle over | ||
|
|
Comment on lines
+306
to
+312
| /** | ||
| * @notice Submit a sealed bid for the current round during its commit phase. | ||
| * @dev `commitment` MUST equal `keccak256(abi.encode(discount, salt, msg.sender, id, round))`. | ||
| * The caller must be a non-prized, non-defaulted member who has fully deposited the round. | ||
| * @param id The circle id. | ||
| * @param commitment The sealed bid hash. | ||
| */ |
Comment on lines
+128
to
+131
| Implemented: the sealed-bid auction, prize-time two-tier security, dividends, prized-default cover, | ||
| no-bid rotation fallback, defaulted-slot substitution, and the net-zero abort valve — on a cohort | ||
| fixed at `start`, with permissionless create / join / start (no owner, no invite). See the 16 unit | ||
| tests in `test/unit/ChitSavingCirclesUnit.t.sol`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR implements Variant D — "the foremanless chit", a permissionless, capital-efficient
reverse-auction rotating savings circle (ROSCA), together with a design document and a full unit-test
suite. It is the on-chain compilation of the Indian chit fund (Chit Funds Act, 1982).
It is the follow-up promised in #182
(Variant A, collateral-first): that PR's spec derives four collateralized permissionless designs and
recommends Variant D as the one that supersedes the rolling-bond Variant B at ~¼ the locked capital.
This PR builds that contract. It is standalone and does not touch
SavingCircles/AutomaticSavingCircles; it can be reviewed and merged independently of #182.The idea
A plain ROSCA is secretly a loan — an early recipient takes the pot and is trusted to keep paying —
so the baseline can only guarantee an honest saver recovers
(n − t)·magainsttcolluders, which iswhy it must gate entry with owner-signed invites and freeze the cohort. The collateral variants drive
t → 0by making everyone post security, but they over-collateralize: they secure every member fora whole pot at all times. The chit fund shows this is ~4× too much, because only a member who has
already won can steal, and only for what she still owes.
Variant D acts on that:
Sealed-bid reverse auction per round. Members
deposit(m)andcommitBid(H(discount, salt))inthe commit phase, then
revealBidin the reveal phase; the highest revealed discount wins. Sealedbids make the auction robust to an adversarial block scheduler (an open-outcry on-chain auction hands
the sequencer a last-look — censor rivals, snipe with
d+ε). No bids ⇒ fixed rotation, so a plainrotation ROSCA is D's degenerate case.
Prize-time two-tier security (the safety core, "Theorem D3"). Collateral is demanded only at
award and only for the exact residual
(n−1−j)·m. The winner's biddis withheld as first-losssecurity and she locks the rest in cash, so the invariant
holds at award and is preserved by every rule. Every pot is therefore made whole against any set
of prized defaulters and no honest member ever loses principal. A would-be thief who bids high
merely pre-pays her own first-loss cover — her realizable theft is
0whatever she bids.Dividends = endogenous interest. A prized member's withheld bid is released to the other members as
she makes her on-time deposits; over her residual period an honest winner pays out exactly
d(herinterest) and recovers her cash lock. Patient late-winners earn interest; impatient early-winners
pay it (zero-sum).
Non-prized default & churn. A non-prized default is not a credit event; it is covered from a
one-round membership
bondofmand the slot is freed for permissionlesssubstitute(the Act's§§28–30). A stalled circle (a defaulted slot with no successor) can be
aborted by anyone, unwindingevery member to net-zero on principal.
Capital held tracks the outstanding-credit parabola
K(j)=(j+1)(n−1−j)·mplus one-round bonds — withinn·mof the information-theoretic floor, vs Variant B's ≈ 4× it at peak.Contents
src/interfaces/IChitSavingCircles.sol— interface; the full mechanism is documented in NatSpec.src/contracts/ChitSavingCircles.sol— upgradeable implementation (Ownable/ReentrancyGuard,SafeERC20, admin token allow-list, pull-based
claim), following repo conventions. Conservation holdsby construction: value enters only on
deposit/join/substituteand leaves only onclaim/reclaimBond/abort; covers, releases and awards only move value internally.test/unit/ChitSavingCirclesUnit.t.sol— 18 unit tests: rotation happy-path (net-zero for all),auction dividends as zero-sum interest, D3 (bid the max then default on everything → every recipient
still whole, defaulter nets zero), substitution completing a circle, stalled-circle abort, abort after
substitution, the
n=2minimum, and the commit/reveal + parameter guards.docs/variant-d-foremanless-chit.md— design rationale, with the two honest limits from the spec:the auction must not be combined with under-collateralized Variant C (adverse selection), and the
same-asset trilemma (safety / anonymity / same-asset credit — pick two), which makes this a
commitment-savings + mutual-insurance + interest-market device rather than a lender.
Testing
The implementation was put through an adversarial safety review (independent lenses each trying to break
D3 / conservation / abort solvency / the auction, with findings verified against the code by PoC). It
surfaced one fund-lock bug —
abortafter asubstitutestranded the substituted-out defaulter'sforfeited bond, because
substituteremoved them from the members array thatabortiterates — which isfixed in this PR (a historical-roster refund, made idempotent) and covered by the
test_Abort_AfterSubstitution_DrainsToZeroregression.Scope (v1)
Ships the auction, prize-time two-tier security, dividends, prized-default cover, no-bid rotation
fallback, defaulted-slot substitution, and the net-zero abort valve, on a cohort fixed at
startwithpermissionless create / join / start (no owner, no invite). Documented extension points: continuous
mid-cycle resize, voluntary (non-default) substitution with claim transfer, a Vickrey price rule, and
heterogeneous / surety collateral for genuine same-cycle credit.
Full comparative spec (Variants A–D, proofs, and the chit-fund analysis) is in #182.