Forma
A staking protocol where the stake is an object you own — an NFT carrying its own lock, multiplier, reward stream and routing policy.
- Category
- Staking Protocol
- Type
- Personal Project
- Year
- 2026
- Status
- In Development
- Role
- Solo Developer
- Technologies
- Solidity, Foundry, Next.js, React, TypeScript, viem, Tailwind CSS
- Index
- 02 / 05

01Overview
Forma is a staking protocol where a stake is not a number in a mapping — it is an object. Each stake mints an ERC-721 that carries its own lock, multiplier, reward stream and routing policy, with the terms snapshotted at stake time so they cannot silently change. Alongside it sits stFORGE, an ERC-4626 vault for people who would rather stay liquid. Five non-upgradeable contracts, a generated TypeScript SDK, and a Next.js front end that reads every number from chain.
02Problem
Most staking contracts reduce a stake to a balance in a mapping. That makes locked stake illiquid and opaque — you cannot hold, transfer or inspect “my 90-day stake” as a thing. Reward handling becomes one-size-fits-all, and the interfaces on top tend to show an invented APY rather than what the protocol is actually doing.
03Approach
Give the position an identity. ERC-721 already provides ownership, transfer, inspection and wallet support, so the design question becomes what travels with the token. Lock, multiplier, penalty and pending rewards do — a transfer can never bypass a lock. The reward-routing policy does not: it records who configured it and silently reverts to “keep” for any new owner, so a buyer never inherits a redirect paying out to the seller. That needed no transfer hook.
04System
One contract holds all funds and accounting. Rewards use accumulator-per-weight arithmetic over a single global stream — the accumulator advances by emitted × 1e30 / totalWeight, and a position earns weight × Δacc / 1e30 — so lock tiers share one stream weighted by principal × multiplier rather than each running its own. Positions settle before any weight change. Principal and reward reserve are separate buckets, emissions during zero-stake periods and forfeited rewards are recycled into the next stream, and every rounding decision favours the protocol. The solvency relations are asserted as invariants rather than assumed.
- 01
Surface
- Stake · PositionsOpen a position by lock tier, then hold, configure, compound or close it. Each position has its own page rendered from the NFT's on-chain owner index — no indexer involved.
- Earn · LiquidThe reward stream and keeper board, and the stFORGE vault for staying liquid instead of locking.
- Docs · ActivityThe protocol spec shipped as a page, and event history read from bounded eth_getLogs ranges — labelled as indexed rather than authoritative.
- 02
Client
- wagmi · viemReads and writes from the browser through RainbowKit, with TanStack Query holding chain state. Every figure on screen comes from a contract call.
- 03
SDK
- Generated ABIs · typesExported from the Foundry build by scripts/export-abis.mjs, so the client cannot drift from the contracts it is calling.
- Rates · glyph · errorsRate maths, the position glyph model and custom-error decoding live in the SDK — derived from chain state, never fetched from a price or stats API.
- Deployment manifestdeployments/*.json carries addresses, roles and transaction hashes per chain. Only anvil.json exists today; base-sepolia.json is what a testnet deployment would add.
- 04
Chain
- FormaStaking · PositionNFTAll funds and accounting in one contract, plus the ERC-721 that represents a position and the renderer that draws its metadata on-chain.
- LiquidStakingVault · ForgeTokenThe ERC-4626 vault over a single no-lock position, and the test token with a rate-limited faucet. Neither is upgradeable.
- OpenStaked and locked. Earning at principal × multiplier.
- UnlockedLock elapsed. Boost drops to 1.00×.
- CompoundingRouting set to COMPOUND; any keeper may compound it.
- RedirectedRewards routed to another wallet by the owner.
- ClosedterminalPrincipal and rewards withdrawn.
- Emergency exitterminalLeft while locked; penalty kept and recycled.
Transitions
- OpenUnlocked
lock elapses - OpenCompounding
setRouting(COMPOUND) - OpenRedirected
setRouting(REDIRECT) - CompoundingOpen
compound() by keeper - UnlockedClosed
withdraw() - OpenEmergency exit
emergencyWithdraw()
A position's life. Terms are snapshotted at stake time, so none of these transitions can change the deal after the fact.
05Interface
The interface is built so a number can always be traced back to a call. Estimates say what they assume instead of printing a headline APY, the hero glyph is labelled “specimen — not a real position” so a demo is never mistaken for state, and a transaction moves through five real stages rather than reporting success before a receipt exists. The testnet disclosure is section 00 of the docs page, not a footnote.



06Build
- Routing does not travel with the tokenLock, multiplier, penalty and pending rewards move with a transfer so a sale cannot bypass a lock. Routing deliberately does not: it records who set it and falls back to “keep” for a new owner, so a buyer never inherits a redirect paying the seller. No transfer hook was needed.
- One reward stream, weighted — not a stream per tierAccumulator-per-weight accounting means adding a lock tier is a parameter, not another accounting surface. Positions settle before any weight change, and rounding always favours the protocol so the reserve cannot be drained by repeated small operations.
- ERC-4626 done defensivelyThe standard is what makes stFORGE legible to an integrator, but it ships with known traps. Internal asset accounting so donations cannot move the share price, a virtual-share decimals offset, a minimum first deposit, zero-share reverts, and max* functions that reflect protocol pauses. Inflation and donation attacks have dedicated tests.
- Invariants over assertions193 Foundry tests: unit, fuzz at 1,000 runs, and invariant campaigns driven by a handler that exercises every user, keeper, vault and admin path against exact ghost accounting. Six of them run the real deployment script. End-to-end tests against a local chain found three bugs a unit test would not have — a clock-dependent lock state, a lost confirmation after closing a position, and a missing accessible label.
07Current Status
Contracts, SDK, tests and front end are complete and work end to end against a local chain, including a two-wallet Playwright run with real mined transactions. The front end is deployed to Vercel and browsable, but the contracts are not on a public network: only `anvil.json` exists in the deployment manifests, so the panels that read chain state sit idle there. The deployment pipeline — allow-listed networks, keystore signing, Basescan verification, manifest with transaction hashes, role hand-over — is built and tested locally, with Base Sepolia next. Nothing here is audited, FORGE and stFORGE have no intended value, and testnet rates are not returns.