Skip to content

feat: Variant D — the foremanless chit (reverse-auction saving circle) - #183

Open
RonTuretzky wants to merge 1 commit into
BreadchainCoop:devfrom
RonTuretzky:feat/variant-d-foremanless-chit
Open

feat: Variant D — the foremanless chit (reverse-auction saving circle)#183
RonTuretzky wants to merge 1 commit into
BreadchainCoop:devfrom
RonTuretzky:feat/variant-d-foremanless-chit

Conversation

@RonTuretzky

Copy link
Copy Markdown
Contributor

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)·m against t colluders, which is
why it must gate entry with owner-signed invites and freeze the cohort. The collateral variants drive
t → 0 by making everyone post security, but they over-collateralize: they secure every member for
a 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) and commitBid(H(discount, salt)) in
    the commit phase, then revealBid in the reveal phase; the highest revealed discount wins. Sealed
    bids 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 plain
    rotation 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 bid d is withheld as first-loss
    security
    and she locks the rest in cash, so the invariant

    withheld(x) + coll(x) == residual(x)
    

    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 0 whatever 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 (her
    interest) 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 bond of m and the slot is freed for permissionless substitute (the Act's
    §§28–30). A stalled circle (a defaulted slot with no successor) can be aborted by anyone, unwinding
    every member to net-zero on principal.

Capital held tracks the outstanding-credit parabola K(j)=(j+1)(n−1−j)·m plus one-round bonds — within
n·m of 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 holds
    by construction: value enters only on deposit/join/substitute and leaves only on
    claim/reclaimBond/abort; covers, releases and awards only move value internally.
  • test/unit/ChitSavingCirclesUnit.t.sol18 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=2 minimum, 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

forge test --match-contract ChitSavingCirclesUnit   # 18 passed
forge test                                          # 186 passed (full suite)
forge fmt --check … && solhint …                    # clean (0 errors)

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 — abort after a substitute stranded the substituted-out defaulter's
forfeited bond, because substitute removed them from the members array that abort iterates — which is
fixed in this PR (a historical-roster refund, made idempotent) and covered by the
test_Abort_AfterSubstitution_DrainsToZero regression.

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 start with
permissionless 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.

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.

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

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 ChitSavingCircles upgradeable 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`.
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.

2 participants