
Captable
Compliance-aware venue for Hedera ATS securities - bids breaching the investor cap get refused.
Demo
About
Hedera's Asset Tokenization Studio lets issuers create and administer compliant securities (ERC-3643/ERC-1400) with KYC, freezes, and transfer restrictions — but it provides no venue to trade them. Cap Table is that venue: a compliance-aware secondary market for ATS securities running on Hedera testnet (chain id 296). Price discovery runs on Uniswap's Continuous Clearing Auction, deployed and used unmodified. The novel part is a validation hook attached to submitBid() that asks a question no gated auction asks today: not just "is this bidder permitted to hold the token?", but "if this bid fills to its worst case, does the resulting shareholder register still satisfy the issuer's compliance rules?" Because compliance for a security is a property of the whole register rather than a per-address flag, a permissionless auction with pro-rata fills can turn one large bid into dozens of small holders and silently build a cap table the issuer cannot legally own. Cap Table refuses to build that register: every bid is projected against the live investor-count and ownership-concentration limits read from the token's own compliance module, and bids that would breach them are rejected on-chain with legible, typed errors — visible in the UI before submission via a live chain-state preview. Cleared allocations settle the way a compliant security should: through ATS's own hold and clearing facets via a settlement router (the auction discovers the price, ATS settles the trade — delivery-versus-payment with a secret-reveal leg, no raw transfers). A compliance-aware claim path refunds any bidder whose eligibility lapsed between bidding and claiming, at the clearing price from a funded settlement reserve, so funds are never stranded. A Chainlink AggregatorV3-compatible NAV feed sets the auction floor price and a deviation band rejects out-of-band bids — failing open when stale so an oracle outage can never halt the auction. Everything is live on Hedera testnet: seven Sourcify-verified contracts (auction, hook, settlement router, compliance module, NAV oracle, bond), a TypeScript indexer serving a REST API on Railway, and an SSR web app on Cloudflare Workers where you can get KYC from a self-serve faucet, place bids, and watch them be refused or accepted — with every refusal recorded in a public ledger along with the exact compliance rule it would have broken.
How it's made
The stack is Foundry/Solidity on-chain, TypeScript off-chain, and three partner technologies stitched together through extension points rather than forks. Price discovery is Uniswap's Continuous Clearing Auction — deployed and used completely unmodified. The whole contribution hangs off the validationHook parameter CCA passes into submitBid(): a hook that projects the post-fill shareholder register against the issuer's compliance rules and reverts with typed errors. The two genuinely tricky parts there: five simultaneous bids each individually project currentHolders + 1 and all pass, so the settlement router keeps pending-beneficiary accounting written before submitBid (and rolled back on revert) and the hook is router-only — that indirection is what makes the projection sound. And it turned out ATS's balanceOf returns available balance while getTotalSecurityHolders counts total, so a fully-held holder reads as zero and the projection fails open; we closed it with a total-of helper (available + held + locked) proven by a fork test against the deployed bond. Hedera's Asset Tokenization Studio is driven through raw ABIs against its diamond facets (the published docs describe a newer version than the deployed v4, so every selector was resolved through DiamondLoupe first). The nastiest integration detail: CCA's constructor reverts unless the full token supply already sits at its address, and ATS only transfers to compliant addresses — so the auction's CREATE2 address had to be KYC-granted before the contract existed (getAddress → grantKyc → transfer → create). Settlement uses ATS's own hold/clearing facets for DvP: a secret-hashed hold, then a reveal leg — with the hash stashed in the hold's data field because the documented lockHash field doesn't exist in the deployed version. The Chainlink integration is an AggregatorV3 NAV feed powering the floor price and a ±10% deviation band that fails open on staleness, wrapped in a bounded-gas staticcall so a reverting oracle can't brick bidding. Off-chain: a viem indexer backfills 200k blocks of auction/ATS events into Postgres (chunked, checkpointed, idempotent schema) and serves a REST API on Railway; the SSR frontend on Cloudflare Workers calls the hook's previewValidate staticcall on every keystroke so refusals render before you sign — and "Submit anyway" broadcasts refused bids with a fixed gas limit so the rejection is a real reverted transaction on HashScan, recorded in a public refusal ledger. 30 Foundry tests cover the four adversarial cases; fork tests replay the live testnet deployment end-to-end, including the DvP hold settlement and the frozen-bidder refund path.