Funding Pools (spec 103)¶
Funding pools let a group pool money toward a stated purpose and goal and either hand the pot to the organizer or give everyone their money back. They live on the receive side of the Payments home — Request ▸ Pool — and are the spec-034 wager-pool architecture with the wager removed.
Spec: specs/103-funding-pools/. Decisions:
research.md. Interface + invariants:
contracts/funding-pool.md.
What a member sees¶
| Surface | Where |
|---|---|
| Create | /app → Request → the Pool kind (beside Direct, the one-time request) (or /app?kind=pool): purpose, goal, contribution window |
| Share | the four words, one link (/fund/<w1>-<w2>-<w3>-<w4> or /fund/0x…), a QR |
| Pool page | /fund/:ref — progress bar, contribute, organizer close / refund, refund vote + status bar, activity feed |
| My Pools | the bottom sheet from the Pool kind — organized + contributed pools, Active / Finished, next action, find by words or link |
| Phrase lookup | the wager-side "Accept a challenge" lookup also resolves funding pools and says which kind it found |
Lifecycle¶
Open ──close() [organizer, any time, goal met or not]───▶ Closed (pot → organizer)
Open ──cancel() [organizer]─────────────────────────────▶ Refunding (reason 1)
Open ──voteRefund() [contributors, > N/2]───────────────▶ Refunding (reason 2)
Open ──pokeDeadline() [anyone, after settleDeadline]────▶ Refunding (reason 3)
Refunding ──claimRefund() [each contributor, once] (contributed[addr] → addr)
- Contributions: any amount > 0, any number of times, until
contributeDeadline(≤ 30 days out). - The organizer may close between creation and
settleDeadline(≤ 180 days out), including after contributions closed. AftersettleDeadline, anyone can start refunds — funds are never stranded. - Refunds are pull-based (each contributor collects), so an unbounded contributor set cannot block the transition, and each refund is relayable per member.
- The majority is by contributor count, ⌊N/2⌋ + 1, evaluated at each vote (research R2).
Architecture¶
FundingPoolFactory(contracts/pools/FundingPoolFactory.sol, UUPS proxy; deployment keysfundingPoolFactory/fundingPoolFactoryImpl/fundingPoolImpl) — screens the organizer on the real wallet (sanctions +POOL_PARTICIPANT_ROLEmembership, the same role wager pools use), assigns a unique four-word BIP-39 index tuple in its own namespace, clones an immutableFundingPool, records it, and forwards relayer twins. Same view names asWagerPoolFactory(poolByPhrase,phraseOfPool,poolById,poolAddressToId) solib/pools/gateway.jsworks against either factory. Storage is append-only with a__gap, registered inscripts/deploy/check-storage-layout.js.FundingPool(contracts/pools/FundingPool.sol) — the immutable ERC-1167 clone holding the escrow. CEI +nonReentranton every value-moving path; the ONLY escrow exits areclose(→ organizer) andclaimRefund(→ the claimant's own recorded amount). Norecipientonclose, no admin sweep, nosetGoal/setPurpose.purposeis an on-chain public string (≤ 200 bytes) so the link is self-describing on chains with no indexer.- Relayer-ready, self-submit first. Every actor action has a
…WithSigtwin (CloseFundingPool,CancelFundingPool,VoteRefund,ClaimRefund,CreateFundingPool— in@fairwins/intent-typesunderFUNDING_POOL_TYPES, gated byTypehashParity) and contribution has an EIP-3009 form; the factory carries…Forforwarders. The frontend ships the self-submit rail and the passkeysendCallsrail; relay-gateway wiring is a follow-up (research R8) — the structs are deliberately NOT inINTENT_TYPES/INTENT_ACTIONSuntil it lands. - Frontend:
lib/funding/(contracts, deep link, pure helpers, device record),hooks/useFundingPools.js(reads/writes; the activity feed is the clone's own event log bounded atcreatedBlock),hooks/useMyFundingPools.js,components/funding/*,pages/FundingPoolPage.jsx. Every number on the page is a chain read; an unreadable pool renders as unreadable with a retry. - No subgraph entity in this release; totals are state reads and the feed is the pool's log.
Deploy¶
GAS_PRICE_WEI=… npx hardhat run scripts/deploy/deploy-funding-pool-factory.js --network mordor # then polygon
npm run sync:frontend-contracts -- --network <name> --chainId <id>
Same knobs as the wager-pool deploy (POOL_ENABLE_MEMBERSHIP, POOL_SCREENING_REQUIRED,
POOL_USDC_<chainId>). Locally it is the LAST step of setup:e2e / setup:local
(deploy:local:funding), so no earlier nonce-derived address moves; HARDHAT_CONTRACTS.fundingPoolFactory
is recorded from a real 80002 run and gated by check:e2e-addresses.
Tests and gates¶
- Hardhat:
test/pools/FundingPool*.test.js,test/upgradeable/FundingPoolFactory.upgrade.test.js(lifecycle, every revert, twins + EIP-3009, forwarders, upgrade, invariants I1–I5 under randomized sequences, reentrancy probes with a malicious token). - Vitest:
frontend/src/test/funding/*,lib/lookup/__tests__/resolvePhraseLookup.funding.test.js. - Cypress:
fast/42-funding-pools.cy.js(no chain) andfull/39-funding-pools.cy.js(create → contribute → close; majority / organizer / deadline refunds, judged by chain reads). Matrix row103-funding-pools. - Actor–critic screenshots:
scripts/ui/capture-funding-pools.mjs→specs/103-funding-pools/screenshots/.
Things that will bite you¶
- Two factories, two phrase namespaces. The same four words can (in principle) name a wager pool
and a funding pool; the lookup reports both, wager first. Never resolve a funding phrase through
wagerPoolFactory. - The organizer can close while a refund vote is short of the majority. That is the stated rule (the vote's confirm copy says so), not a race to fix.
totalRaised == balanceonly for a well-behaved token. The factory allow-list exists for that reason; do not allow a fee-on-transfer or rebasing token.- The local factory address is nonce-derived. Inserting any deployer transaction before
deploy:local:fundinginsetup:e2emoves it; append instead, then re-derive.