Skip to content

Latest commit

 

History

History
150 lines (122 loc) · 6.81 KB

File metadata and controls

150 lines (122 loc) · 6.81 KB

CLAUDE.md — KOLO monorepo

What this is

KOLO is a non-custodial savings protocol on Arc (Circle's USDC-native L1). A saver opens a vault locked to a purpose ("Adaeze final year fees"); anyone — including diaspora family cross-chain via CCTP — contributes USDC toward the goal; when it fills, the vault executes its purpose: funds move to an immutable execution target (settlement treasury or direct merchant wallet), and the backend pays the actual bill, anchored to purposeRef.

Founder: Chiemelie (Nigeria). Stack: Solidity 0.8.28, NestJS + BullMQ/Redis + Prisma/Postgres, ethers v6. Frontend is founder-supplied (Vite + React + TS expected) and lives in frontend/ when it lands.

Layout

contracts/   Solidity vaults + factory, solc-js compile, e2e tests, deploy
backend/     NestJS: indexer, auto-executor, settlement pipeline, REST API
frontend/    (founder-supplied UI goes here)
docker-compose.yml   Postgres 16 + Redis 7 for local backend dev

Commands

# contracts/
npm run compile        # solc 0.8.28 → ./abi/*.json (abi + bytecode)
npm test               # 24-assertion e2e vs anvil (anvil --chain-id 5042002)
npm run deploy:arc     # deploy factory to Arc Testnet (.env PRIVATE_KEY)

# repo root
docker compose up -d   # postgres + redis

# backend/
cp .env.example .env   # fill FACTORY_ADDRESS + EXECUTOR_PRIVATE_KEY
npm run prisma:generate && npm run prisma:migrate
npm run start:dev      # API :3001 + indexer + workers in one process

Contracts (contracts/)

  • KoloVault.sol — one vault per goal. States Open → Executed | Cancelled | Expired. All config immutable (no proxies — deliberate). Key functions: contribute, contributeFor(contributor, amount) (CCTP attribution: relayer pays, funder's own EVM address keeps refund rights), execute (goal reached: saver or factory.executor; flex partial: saver only, post-deadline), cancel (saver), expire (permissionless post-deadline; flex vaults +14-day grace), claimRefund (pull-based).
  • KoloVaultFactory.solnew KoloVault(...) (no clones), registry (allVaults, vaultsBySaver, isKoloVault), config (feeRecipient, executor, default bps). Ownable2Step + Pausable; pause affects createVault ONLY.

Invariants — NEVER break these in any edit

  1. Vault funds have exactly two exits: the immutable executionTarget, or refunds to contributors. No new withdrawal paths, ever.
  2. claimRefund stays callable in Cancelled/Expired regardless of any factory state. Refunds are unpausable.
  3. Fees are snapshotted at vault creation, hard-capped at 500 bps in the vault itself. Factory changes never affect existing vaults.
  4. Forfeit applies only to the saver's own contribution, only on cancel. Expiry refunds are always fee-free for everyone.
  5. Expiry stays permissionless (post-deadline, +FLEX_GRACE_PERIOD for flex) so funds can never be stranded.
  6. Only the saver may partial-execute a flex vault; the executor role may only trigger execution when the goal is fully reached.

Any edit touching these paths must extend contracts/test/kolo.e2e.mjs and keep all existing assertions green.

Backend (backend/)

Single NestJS process, three concerns:

  • indexer/ — polls Arc getLogs from a DB cursor (IndexerCursor), ingests VaultCreated (factory) + all vault events into Postgres. Event args are authoritative (totalRaised comes from the event) → idempotent, safe to re-run. On goal reached → enqueues kolo-execute. On VaultExecuted → creates SettlementJob, enqueues kolo-settle.
  • executor/ — BullMQ worker holding the factory executor key. Re-checks on-chain state before spending gas; contract only accepts the call at full goal, funds only move to the vault's immutable target — worst case is a reverted tx, never misdirected money.
  • settlement/ — BullMQ worker paying the real bill via a BillerAdapter (settlement/billers/). MockBillerAdapter for testnet; real aggregator adapters (Airbills' rails) implement the same interface and swap in via settlement.module.ts. Terminal failures → status FAILED for ops (money sits in the settlement treasury — delayed, never lost).

REST for the UI (vaults/): GET /vaults?saver=, GET /vaults/:address (funding-page data source), POST /purpose-ref (UI calls this BEFORE createVault; stores full bill payload, returns keccak256 purposeRef + factory address). Amount fields serialize as strings (USDC 6-dec base units).

Arc network reference

Testnet chain ID 5042002
RPC https://rpc.testnet.arc.network
USDC ERC-20 0x3600000000000000000000000000000000000000
EURC (testnet) 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a
Explorer https://testnet.arcscan.app
Faucet https://faucet.circle.com

Decimals trap: Arc's native-gas USDC reports 18 decimals; the ERC-20 interface reports 6. Same balance, two representations. All code uses the ERC-20 interface (6 dec) exclusively — parseUnits(x, 6) everywhere; never mix provider.getBalance values in without converting.

Hard-won gotchas

  • ethers v6 default provider caches ~250ms and batches RPC; against instant-mining anvil this causes phantom stale-nonce/stale-timestamp failures. Test providers use { batchMaxCount: 1, cacheTimeout: -1, staticNetwork: true }.
  • Custom errors surface in estimateGas failures as raw selectors (e.info.error.data) — match by selector (see expectRevert in the e2e).
  • partial is a reserved word in Solidity ≥0.8.
  • prisma generate needs network for engines; in restricted sandboxes use prisma generate --no-engine for types-only. Backend model rows have structural interface fallbacks in vaults.controller.ts so tsc passes even pre-generate.

Conventions

  • USDC amounts: bigint, 6 decimals; API serializes as strings.
  • Addresses stored lowercase.
  • Solidity: 0.8.28 pinned, custom errors, NatSpec on externals, checks-effects-interactions, SafeERC20.
  • Never commit .env; keys only in local env.

Status & next milestones

Done: contracts compiled clean + 24/24 e2e passing; backend typechecks clean (indexer, executor, settlement, REST all implemented; MockBillerAdapter).

  1. Deploy factory to Arc Testnet (contracts: npm run deploy:arc) → put address in backend/.env FACTORY_ADDRESS and set the factory executor to the backend key (factory.setExecutor).
  2. Wire founder's UI in frontend/: reads via GET /vaults/:address + live chain reads; writes via ethers against contracts/abi/*.json (createVault, approve + contribute). The shareable funding page a diaspora aunty opens from WhatsApp is the hero screen — highest polish.
  3. Real biller adapter behind BILLER_ADAPTER when rails are chosen.
  4. Phase 2 contracts: CCTP v2 hooks, USYC yield adapter, ajo/esusu circles, EURC vaults.