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.