Technical documentation
How Zero works
The contracts, the math, and the guarantees, written for developers, auditors and anyone who refuses to take our word for it.
75% of all protocol revenue is used to market-buy $ZERO and burn it.
This is not a policy; it is a hard-coded constant in the Factory (PLATFORM_BURN_BPS = 7500). No function exists that can change it, pause it, or redirect it. Every platform fee claim buys $ZERO from the real pool and sends it to 0xdEaD, forever.
Architecture
In plain terms: one contract creates your token and its trading pool, burns the liquidity so nobody can pull it, and splits every trading fee automatically.
Zero is six contracts on Ethereum mainnet, built directly on Uniswap v4. There is no bonding curve and no graduation step; every token trades in a real v4 pool from its first block. The Factory/Hook pair exists twice: once for ETH-quoted launches and once for launches quoted in a tokenized stock (see Pairs & RWA).
Launching
Every swap
| Contract | Role | Key detail |
|---|---|---|
| Factory | Creates tokens, seeds pools, splits fees, pays claims | Owns the solvency invariant; LP is minted straight to 0xdEaD |
| ZeroHook | Our Uniswap v4 hook, charging the creator-chosen swap fee | Permission bits encoded in its address (0xCC), CREATE2 salt-mined |
| Locker | Trustless token locks: fixed, linear, cliff+linear | No admin path to locked funds, extend-only |
| ZeroRWAFactory | Same launch flow, quoted in an allowlisted ERC-20 (tokenized stocks) | Per-currency accounting; the token must sort above the quote |
| ZeroRWAHook | The v4 hook for stock-quoted pools | Charges the fee in the quote token |
| Token | Minimal ERC-20 | Fixed supply minted to the Factory at deploy; creator is immutable |
Pairs & RWA
In plain terms: your token can trade against ETH, or against a tokenized stock like $TSLA or $NVDA — and then your fees are paid in that stock.
Every launch picks a quote asset: either native ETH or one of the Ondo tokenized equities on mainnet. The two cases run on separate contracts — Factory + ZeroHook for ETH, ZeroRWAFactory + ZeroRWAHook for ERC-20 quotes — so the ETH path stays exactly as audited while stock pairs get the rules they need. Everything else is identical: burned LP, the same fee split, the same locker, the same solvency invariant.
| ETH pair | Tokenized stock pair | |
|---|---|---|
| Contracts | Factory + ZeroHook | ZeroRWAFactory + ZeroRWAHook |
| You pay with | ETH | The stock token (e.g. TSLAon) |
| Creator fees paid in | ETH | The same stock token |
| Protocol revenue | 75% buys and burns $ZERO | Transferred to the platform; conversion happens off-chain |
| Quote list | — | Owner-allowlisted, one liquidity ladder per stock |
Why fees arrive in the stock
A pool only ever holds its two assets. When someone buys your $TSLA-paired token, they pay TSLAon, so the swap fee is charged in TSLAon and that is what accrues to you. The dashboard therefore lists earnings per currency, andcollectFees(quote) pays each one out separately.
The ordering rule
A Uniswap v4 pool sorts its two currencies by address. For stock pairs Zero requires the new token to sort above the quote, so the token is always currency1 and the fee path has a single branch — the same one the ETH hook uses. The launch UI mines a CREATE2 salt until the predicted address satisfies that (milliseconds); a salt that fails reverts with TokenBelowQuote.
Supported quotes
$TSLA, $AAPL, $NFLX, $SPCX, $AMZN, $META, $QQQ, $GOOGL, $TSM, $SPY, $NVDA — all 18-decimal Ondo tokens with no transfer restrictions. New quotes are added by the owner; existing pools are never affected.
Launch lifecycle: one transaction
In plain terms: you sign once. When the transaction confirms, your token exists, trading is live, and the liquidity is already burned.
deployCoin(name, symbol, metadata, salt, configId, feeBps, deployer, lock) does everything atomically. If any step fails, the whole launch reverts; there is no half-launched state.
| # | Step | What happens |
|---|---|---|
| 1 | Token deploy | New ERC-20 via CREATE2: the address is predictable from the salt, so frontends show it before the transaction (getTokenBytecode) |
| 2 | Registry write | One TokenInfo record + packed FeeState (recipient, renounced): a single storage slot read on every swap |
| 3 | Fee registration | The hook stores tokenFeeBps once; the tier must be on the owner-managed allowlist (1–10% at launch). It can never change; the only transition is renouncing to a flat 0.1% |
| 4 | Pool init | v4 pool with currency0 = ETH, fee = 0, tickSpacing = 200, hooked to our ZeroHook, which is the only fee |
| 5 | Liquidity | The entire supply deposits as single-sided liquidity (see Liquidity math) |
| 6 | LP burn | The LP NFT is minted directly to 0xdEaD. The Factory has no function that removes liquidity; the burn is verifiable on-chain |
| 7 | First buy (optional) | If ETH is attached, it buys through the Universal Router in the same transaction, optionally locked in the Locker with a cliff/vesting schedule |
Measured cost on a mainnet fork: deployCoin ≈ 1.36M gas including the pool, the LP mint and bookkeeping.
Fee mechanics
In plain terms: every buy and sell pays the fee the creator chose. There is no cheap side-door: all four ways of swapping cost the same rate.
The hook implements beforeSwap and afterSwap with return-delta permissions. Its deployment address literally encodes 0xCC in the low bits, which Uniswap v4 validates. The fee is always charged on the ETH side, for all four swap shapes, and the exact-out formulas are grossed up so no shape is cheaper than another:
| Swap shape | Charged in | Formula |
|---|---|---|
| Buy, exact-in | beforeSwap | fee = ethIn × bps / 10000 |
| Buy, exact-out | afterSwap | fee = ethIn × bps / (10000 − bps) |
| Sell, exact-in | afterSwap | fee = ethOut × bps / 10000 |
| Sell, exact-out | beforeSwap | fee = ethOut × bps / (10000 − bps) |
Collection is a single ETH transfer: the hook calls poolManager.take(currency0, factory, fee). The ETH lands on the Factory directly, and the hook then reports the amount with depositFees(token, amount). The hook never holds ETH and has no receive().
Pools created around the Factory can attach this hook but are unusable: _feeBps reverts with TokenNotRegisteredfor any token the Factory didn't register.
The solvency invariant
In plain terms: the Factory only believes money it can actually see. Nobody can invent fee balances: the math makes lying revert.
Because the hook only reports amounts, the Factory refuses to believe it blindly. It keeps one aggregate liability counter and checks it against the real balance on every deposit:
function depositFees(address token, uint256 amount) external {
if (msg.sender != tokenHook[token]) revert OnlyTokenHook();
totalAccrued += amount;
if (address(this).balance < totalAccrued) revert InsufficientETHReceived();
// ... 90/10 split
}take() runs before depositFees() in the same transaction, so the ETH is already on the Factory when the check runs. Over-reporting by even 1 wei reverts the whole swap, verified in fork tests. Under-reporting only donates to the platform. Claims (collectFees, claimProtocolFees) decrement totalAccrued, so balance ≥ totalAccrued holds at all times: the Factory can always pay every outstanding claim.
Fee split & creator rights
In plain terms: creators earn 90% of every fee, can redirect it, or give it up forever. The platform's 10% mostly ends up as burned $ZERO.
| Action | Who | Effect |
|---|---|---|
| Earn | Fee recipient | 90% of every fee accrues per-recipient across all their tokens; claimed in one call via collectFees() |
| Redirect | Deployer (permanent right) | setTokenFeeRecipient points future fees at any address. Already-accrued fees stay with the old recipient |
| Renounce | Deployer, one-way | renounceCreatorFees drops the fee to a flat 0.1%, all of it to the platform, forever. Disables redirecting. Traders can verify it |
| Community takeover | Platform owner | communityTakeover hands the deployer role (and future fees) to a new address; not available after renounce |
| Platform claim | Platform owner | On every claimProtocolFees call, a fixed 75% of the accumulated revenue market-buys $ZERO through the real pool and burns it at 0xdEaD; 25% pays out. The split is the immutable constant PLATFORM_BURN_BPS = 7500, an unchangeable rule |
Locker
In plain terms: locked tokens are provably untouchable, even by us, until the schedule says otherwise.
Anyone can lock any ERC-20 for any beneficiary. The owner role exists for future admin needs only; there is deliberately no path to locked funds and no way to shorten a lock.
| Kind | Schedule | Notes |
|---|---|---|
| Fixed | Everything unlocks after N days | Beneficiary can extend, never shorten |
| Linear | Vests continuously from day 0 to N | Withdraw any vested amount at any time |
| CliffLinear | Nothing during the cliff, then linear | Cliff and vesting lengths are independent |
Implementation details
Each lock packs into 3 storage slots (token+kind+timestamps / beneficiary+vestEnd / amount+claimed as uint128). Deposits are fee-on-transfer safe: the contract locks what actually arrived, measured by balance difference. Lock lists are not stored on-chain; indexers rebuild them from Locked/Withdrawn events, while totalLocked[token] keeps the live locked balance for badges.
Liquidity math
In plain terms: your two launch choices, supply and starting liquidity, are just a price: how much ETH the pool pretends to hold on day one.
A launch config is a pair: total supply S and virtual ETH V (the opening market cap). The entire supply deposits as single-sided currency1 liquidity, priced as if V ETH were already in the pool:
sqrtPriceX96 = √(S / V) × 2⁹⁶ // pool opens at S/V tokens per ETH
initTick = log₁.₀₀₀₁(S / V)
tickUpper = floor(initTick, 200) // price sits just above the range →
tickLower = -887200 // the whole supply deposits one-sided,
// buy-side liquidity never runs outThe tick-floor gap makes the effective opening price at most ~1.6% below nominal. All 20 precomputed configs (1M–1B supply × 0.69–10 ETH) are verified against the 200 tick spacing: divisibility, min/max bounds, and the single-sided condition.
Events & indexing
The contracts store the minimum on-chain and emit everything an indexer needs. Metadata, deploy timestamps and per-token fee totals are intentionally not in storage; they are derived from events:
| Event | Emitted by | Answers |
|---|---|---|
ERC20TokenCreated(token, name, symbol, deployer, metadata) | Factory | Token list, identity, metadata, launch time (block timestamp) |
FeesDeposited(token, recipient, creatorShare, platformShare) | Factory | Per-token volume of fees, creator earnings, platform take |
TokenFeeSet(token, feeBps) | Hook | The fee tier, and which hook the token trades through (the emitter address) |
CreatorFeesCollected / PlatformFeesBurned | Factory | Claim history and the ZERO burn tally |
Locked / Withdrawn / LockExtended | Locker | Lock lists, vesting state, locked-% per token |
TokenFeeRecipientUpdated / CreatorFeesRenounced / CommunityTakeover | Factory | The full history of a token's fee stream |
Security properties
| Property | Mechanism |
|---|---|
| Liquidity cannot be pulled | LP NFT owned by 0xdEaD; no removal function exists anywhere in the Factory |
| Fees cannot be inflated | The solvency invariant: crediting unbacked ETH reverts the swap |
| The fee cannot change | Registered once on the hook; the only transition is the one-way renounce to 0.1% |
| Locks cannot be touched | No owner path in the Locker; fixed locks extend-only |
| Reentrancy | OpenZeppelin ReentrancyGuardTransient (EIP-1153) on every ETH-paying path |
| Predictable addresses | CREATE2 everywhere: token addresses are verifiable pre-launch; the hook address encodes its own permissions |
| Tested against mainnet | The full launch flow, fee routing, invariant and buyback run in fork tests against the real Uniswap v4 contracts |
Contract reference
In plain terms: every function you can call, what each input means, and who is allowed to call it.
Factory — ETH pairs
deployCoin(name, symbol, metadata, salt, configId, feeBps, deployer, lock)The launch. Creates the ERC-20, initialises the v4 pool, adds the whole supply as liquidity, burns the LP position, and — if you attached ETH — performs your first buy, all in one transaction.
namestring- Token name, e.g. “Ethereum”.
symbolstring- Ticker, e.g. “ETH”.
metadatastring- An
ipfs://CIDpointing at the JSON document with image, description and socials. Emitted in the event; never stored on-chain. saltbytes32- CREATE2 salt — decides the token address. Any value works for ETH pairs.
configIduint256- Picks a supply x virtual-ETH ladder entry (the opening market cap). Ladders are set by the owner;
getLiquidityConfigreads them. feeBpsuint256- Your swap fee in basis points (100 = 1%). Must be one of the tiers the hook allows.
deployeraddress- Who owns the fee stream and the creator rights. Usually you; can be a treasury or multisig.
locktuplekind0 fixed / 1 linear / 2 cliff+linear, pluscliffDaysandvestDays. Applies to the tokens bought by your first buy;kind 0 + 0 daysmeans no lock.
*Gated by publicDeployEnabled: only the owner can launch until the platform opens it. msg.value is the size of your first buy — send 0 for none.
collectFees()Pays out everything you have accrued across all your tokens, in one transfer. No arguments — the contract knows who is calling.
setTokenFeeRecipient(token, newRecipient)Redirects FUTURE fee accrual to another address. Fees already accrued stay claimable by the current recipient.
tokenaddress- The token you launched.
newRecipientaddress- Where new fees should accrue from now on. Cannot be the zero address.
Control travels WITH the stream: after you redirect it, only the new recipient can redirect it again (or renounce). The launching address keeps no special power — the one exception is communityTakeover, which the platform owner can always use.
renounceCreatorFees(token)Permanently gives up your fee stream. The swap fee drops to a flat 0.1% that goes entirely to the platform.
tokenaddress- The token to renounce fees on.
One-way. No function can restore the fee, and already-accrued fees remain claimable.
claimProtocolFees()Settles the platform's share: 75% market-buys $ZERO and burns it, 25% goes to the platform. The 75% is the immutable constant PLATFORM_BURN_BPS.
communityTakeover(token, newDeployer)Hands the deployer role and the fee stream of an abandoned token to a new address.
tokenaddress- The token being handed over.
newDeployeraddress- The new owner of the creator rights.
depositFees(tokenAddress, amount)How the hook reports a swap fee it already sent. Credits the split and is rejected unless the Factory's ETH balance actually covers totalAccrued — see the solvency invariant.
tokenAddressaddress- Which token the fee came from (decides whose creator share it is).
amountuint256- Fee amount in wei that was transferred to the Factory in this same transaction.
ZeroRWAFactory — tokenized stock pairs
Same functions, with two differences: a quote address selects which stock the pool is priced in, and money moves as ERC-20 transfers instead of msg.value.
deployCoin(name, symbol, metadata, salt, quote, configId, feeBps, deployer, initialBuyAmount, lock)The stock-paired launch. Identical flow to the ETH factory, quoted in an allowlisted ERC-20.
quoteaddress- The tokenized stock the pool trades against (e.g. TSLAon). Must be allowlisted by the owner.
saltbytes32- Must produce a token address ABOVE the quote address, otherwise the call reverts with
TokenBelowQuote. The UI mines this for you. configIduint256- Ladder entry for THIS quote — each stock has its own supply x virtual-quote ladder.
initialBuyAmountuint256- Your first buy, denominated in the quote token. Approve the factory for this amount first;
0skips it. locktuple- Same lock options as the ETH factory, applied to the tokens your first buy receives.
Name, symbol, metadata, feeBps and deployer behave exactly as in the ETH factory. Not payable — no ETH is involved anywhere in this path.
collectFees(quote)Pays out your accrued fees IN THAT QUOTE. Fees are tracked per currency, so claiming TSLAon and NVDAon are two separate calls.
quoteaddress- Which quote currency to withdraw.
claimProtocolFees(quote)Transfers the platform's share of that currency to the owner as-is. There is deliberately NO on-chain buyback here — stock revenue has no guaranteed route to $ZERO; conversion happens off-chain.
quoteaddress- Which quote currency to settle.
setQuoteAllowed(quote, allowed)Adds or removes a stock from the allowlist and grants the router the Permit2 allowance it needs. Existing pools are never affected.
quoteaddress- The ERC-20 to allow as a pair asset.
allowedbool- True to enable launches against it, false to stop new ones.
setTokenFeeRecipient, renounceCreatorFees, communityTakeover and depositFees have the same signatures and rules as the ETH factory — only the currency differs.
Hooks (ZeroHook / ZeroRWAHook)
The hooks are called by the PoolManager on every swap; you never call them directly. Their only writable surface is administrative.
setTokenFee(token, feeBps)Registers a token's fee tier at launch. Can only ever be set once per token.
tokenaddress- The freshly launched token.
feeBpsuint256- Fee in basis points; must be an allowed tier.
setRenouncedFee(token)Drops a token to the flat 0.1% platform fee after its creator renounces.
tokenaddress- The token being renounced.
setFeeAllowed(feeBps, allowed)Manages which fee tiers creators may pick. Affects future launches only — a live token's fee can never be changed.
feeBpsuint256- The tier, in basis points.
allowedbool- Whether new launches may select it.
Locker
lockFixed(token, amount, lockDays, beneficiary)Locks tokens until a date. Nothing is claimable before it; everything is claimable after.
tokenaddress- Token to lock (approve the Locker first).
amountuint256- Raw amount, 18 decimals.
lockDaysuint256- Days until the unlock date.
beneficiaryaddress- Who may withdraw — can be someone other than you.
lockLinear(token, amount, vestDays, beneficiary)Vests continuously from the moment of the lock until the end date.
vestDaysuint256- Length of the vesting period in days.
token / amount / beneficiary…- As above.
lockCliffLinear(token, amount, cliffDays, vestDays, beneficiary)Nothing vests until the cliff, then it vests linearly until the end.
cliffDaysuint256- Days before anything becomes claimable.
vestDaysuint256- Days of linear vesting AFTER the cliff.
withdraw(lockId)Claims everything vested so far. Safe to call repeatedly; it only ever sends what is newly available.
lockIduint256- Id from the Locked event / your dashboard.
extend(lockId, extraDays)Pushes a fixed lock's unlock date further out. The date can never move earlier.
lockIduint256- The lock to extend.
extraDaysuint256- Days to add to the current unlock date.
claimable(lockId)Vested minus already claimed, per that lock's schedule. Free to call.
Deployments
In plain terms: these are the exact contracts your transactions touch. Verify them on Etherscan before you sign anything.
Zero contracts
| Contract | Address |
|---|---|
| Factory (ETH pairs) | 0x43c92da5b5b5cbde841ebced4cfdf04fb10f9392 |
| ZeroHook | 0x4717c4634ef366200278212c12f946db9355c0cc |
| ZeroRWAFactory (stock pairs) | 0x125d1d3f10bde713552a0f3826e14516666e36f1 |
| ZeroRWAHook | 0xecad6eb41f714ee7c733afc21e0fbe80d1eec0cc |
| Locker (shared) | 0xd8c777b19e31ee244e96f9ca4e2474e955c1c6e5 |
| $ZERO (protocol token) | not deployed yet |
Uniswap v4 infrastructure
Zero deploys none of these — they are Uniswap's canonical mainnet contracts, and your funds move through them, not through us.
| Contract | Address |
|---|---|
| PoolManager | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
| PositionManager | 0x58daec3116aae6D93017bAAea7749052E8a04fA7 |
| Universal Router | 0x8876789976dEcBfCbBbe364623C63652db8C0904 |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
Burned liquidity goes to 0x000000000000000000000000000000000000dEaD — the standard dead address, which has no known private key.
Ready to launch?
One transaction, and every guarantee above applies to your token.