← Work

Capset

Fundraising where the rules are final. Presales on Base where the caps, price, deadline and refunds are enforced by contracts, not by an admin.

Role
Designed and built solo · contracts, backend, indexer, frontend, docs
When
Nov 2025 – Mar 2026
Status
Live on Base testnet
Links
8contracts: factory, router, presale, receipts, vesting, locker
393Foundry tests across the contract suite
1%of a raise per receipt token, 100 per raise
4 mofirst commit to public testnet
Figures from the repository and the testnet deployment. An external audit comes before mainnet.

Context

On most fundraising platforms, an admin can change the terms halfway through a raise: the cap, the price, the deadline. Contributors have to trust that nobody will.

Capset moves the terms into contracts. Once a raise is live, nobody can change them, including the founder and the platform. If the raise fails, the money goes back without anyone’s permission.

What I built

  • Eight Solidity contracts, built with Foundry and OpenZeppelin v5.
    • A factory and a router. The router creates a presale and its receipt token in one atomic transaction.
    • Presales with soft and hard caps, per-wallet limits and a 24-hour grace period before finalising.
    • Whitelisting by address list or Merkle root, and a platform-wide KYC registry.
    • Vesting vaults with a cliff and linear release, and a liquidity locker with a 30-day minimum.
  • Receipt tokens. Each raise mints exactly 100 ERC-20 receipt tokens, so one token is 1% of the allocation. A contributor’s claim becomes something they can hold, transfer and verify. The SHA-256 hash of the legal terms (a SAFE, a token warrant or a custom agreement) is stored on-chain next to it.
  • A custom indexer. It polls Base in 100-block batches and starts watching each new presale as soon as the factory creates it. Events are applied in strict block and log order, and every event is keyed on transaction hash, log index and chain, so replaying a range is always safe.
  • The backend and app. Express and Prisma on Postgres, BullMQ workers, live raise updates over websockets, and Sign-In with Ethereum. The frontend uses Next.js with wagmi and includes a viewer for each raise’s terms.

What was hard

Refund accounting. When an owner recovers leftover funds after a raise, that must never touch money still owed to contributors. Recovery is calculated against the unclaimed refund pool, not the contract’s balance. The first version got this wrong, and the fix changed what “leftover” means in the code.

Tokens that lie about transfers. Fee-on-transfer tokens deliver less than the amount sent. Contributions are measured by the change in the contract’s balance, not by the amount the user asked to send.

Large whitelists. I stress-tested address-list whitelists at 350, 400 and 500 entries to find where they stop being practical and Merkle proofs should take over. Both are supported, and a raise can use either.

How it fits together
Foundersets the terms Routerone transaction Presalecaps · whitelist ContributeETH · USDC · USDT Receipt100 tokens = 100% Finalise24h grace Vest · lockcliff · linear Refund soft cap missed Founder sets the terms Router one transaction Presale caps · whitelist Contribute ETH · USDC · USDT Receipt 100 tokens = 100% Finalise 24h grace Vest · lock cliff · linear Refund soft cap missed
The terms are fixed at creation. The refund path needs nobody’s permission.
A decision, and the option I rejected
Decision · RefundsAccepted
Decided

Contributors claim their own refunds on-chain when a raise misses its soft cap.

Rejected

An admin-triggered refund process.

Because

A refund that depends on someone pressing a button isn't a guarantee. The contract holds the funds and the rule, so the refund works even if the platform disappears.