Skip to content
Takshak Mudgal
EVM contracts

Zyora, a token launchpad on Arc

I wrote the Solidity contracts, the chain indexer and the launchpad API for a token launchpad on Circle's Arc chain: 304 tests, 47 launches, and launch liquidity that no wallet can withdraw.

Status
Live on Arc mainnet
When
Sep 2026
Role
Wrote the contracts, the chain indexer and the launchpad API
Team
Contracts solo; two other engineers on the app
Stack
Solidity, Foundry, Solady, TypeScript, Next.js, Cloudflare Workers, PostgreSQL
3041
test functions: 260 unit and integration, 29 fuzz, 15 invariants
472
tokens launched on Arc mainnet by 16 Oct 2026
279 B3
headroom under the 24,576-byte contract size limit
1%2
maximum swap fee, fixed as a constant in the router

Summary

Zyora is a token launchpad on Arc, Circle's chain where USDC is both the gas token and the asset every launch trades against. I wrote its Solidity contracts in Foundry: the bonding-curve launchpad, a constant-product AMM, a swap router, the liquidity locker and a launcher for NFT collections. The curve follows the economics of Meteora's Dynamic Bonding Curve, ported to the EVM, and the AMM adapts Uniswap V2's design. In the app I built the chain indexer and the launchpad API that the site reads from.

It went live on Arc mainnet on 11 September 2026. By 16 October, 47 tokens had launched through it2, and the liquidity behind 44 of them sits in locks that no wallet can withdraw, including mine.

Constraints

  • No DEX to graduate into. Arc had no audited AMM a launch could hand its liquidity to, so the AMM had to ship in the same repository.
  • No upgrades. The contracts are not proxies. Every fix is a redeploy, and tokens that already launched have to keep trading through it.
  • A hard size limit. EIP-170 caps deployed code at 24,576 bytes, and the launchpad holds the curve, buys and sells, fee splits, graduation and claims.
  • An unusual native token. USDC has 6 decimals as an ERC-20 and 18 as the gas token, and public RPCs throttle and cap a log query at 2,000 results.

Decisions

Liquidity that cannot be pulled

The docs promised that launch liquidity was locked for good, but the first version named the creator as the beneficiary of each lock, so after 30 days a creator could withdraw the pool's liquidity and drain it. I made the launching contract the beneficiary instead. It has no code path that withdraws, so the lock is permanent by construction rather than by policy. Three tokens launched before the fix; their locks still belong to their creators2.

Limits written into the bytecode

The swap router routes trades through outside venues and takes a platform fee. The fee cap is a constant, MAX_FEE_BPS = 100, so no owner key can raise it above 1%. Each call approves exactly the amount it forwards, resets the approval to zero in the same call and checks the output by balance difference rather than trusting the venue's return value.

Fitting under 24,576 bytes

The launchpad compiles at 24,297 bytes, 279 under the limit3. Getting there meant compiling that one contract for size, moving the NFT collection's creation code into its own deployer and splitting the standard launch into a second contract, because both launch mechanisms did not fit in one.

An opening price that costs nothing to fund

A standard launch opens at a 6,000 USDC market cap through a virtual quote reserve that is never paid out. Any swap that would pay out more than the pool really holds reverts, and liquidity burns are capped at real holdings, so the virtual reserve can set the price but can never be withdrawn.

Where the bytes went

ContractBarBytes
ZyoraLaunchpad
24,297
ZyoraStandardLauncher
17,271
ZyoraCollectionDeployer
15,346
AmmFactory
13,001
ZyoraRegistry
10,764
AmmPair
9,482
ZyoraCollectionToken
8,205
AmmRouter
4,297
ZyoraCollectionMirror
3,857
ZyoraSwapRouter
2,785
LiquidityLocker
2,609
The vertical line marks the EIP-170 limit, 24,576 bytes.
Fig. 1. Runtime size of each Zyora contract from the CI size report on 16 Sep 20263, against the EIP-170 limit of 24,576 bytes. The launchpad is the one contract built with the optimizer set for size.

The hard part: redeploying without stranding anyone

Standard launches failed on mainnet with no revert reason. The factory embeds the pair's creation code at compile time, so the deployed factory kept creating pairs from before setVirtualQuote existed, and every launch call reverted inside them. Redeploying the launchers alone could not fix that.

I redeployed the factory, the router and both launchers, kept the old pairs trading on the old router, and taught the app to look up which factory holds each pair. The upgrade script also missed a call to setRouter, and pairs only accept swaps from factory.router(), so every swap through the new router would have reverted. Both fixes went out the same day, and the new factory's router reads back correctly on chain2.

The indexer and API

The app runs on Cloudflare Workers with Neon Postgres. In production every API route alternated between a response and error 1101, and the indexer never caught up. The cause was a Postgres pool kept alive across requests: Workers do not let one request use a socket opened by another. Each query now opens and closes its own connection, and no request awaits a promise or shares an RPC client created by another.

The indexer moved to a GitHub Actions job that runs every five minutes, resumes from a saved cursor and stops within a time budget. Log windows are sized to Arc's 2,000-result cap and split when a node asks for smaller ranges, inserts are batched, and RPC calls fall back through an ordered list of providers. I wrote most of the indexer and the API's read and write paths; a teammate wrote the indexer for outside venues and the charts.

Verification

  • 304 test functions in 25 files: 260 unit and integration tests, 29 fuzz tests at 4,096 runs each and 15 invariants run as 384 sequences of 192 calls, where any revert fails the run1.
  • The last green CI run, on 16 September 2026, ran 318 tests because two suites run twice through inheritance1. CI also checks formatting, contract sizes and ABI drift.
  • Slither across 102 detectors, with a written disposition for each of its 26 findings.
  • A script that rehearses the launch lifecycle read-only against Arc mainnet's real USDC, using eth_call state overrides.
KindBarFunctions
Unit and integration
260
Fuzz
29
Invariant
15
Fig. 2. Zyora's test functions by kind, counted from the test sources1.

Deployment register

Current contracts on Arc mainnet, chain ID 50424. Earlier launchpads and the first AMM factory are still indexed so their tokens keep trading.

Zyora contracts on Arc mainnet
ContractAddressDeployed
ZyoraRegistry0xf2Bc89A82485770887645046704a20dBA519593711 Sep 2026
ZyoraLaunchpad0x0351FC87f7F4bFb73a263559Ce5Cb570800Ab6cf16 Sep 2026
ZyoraStandardLauncher0x781fb303EB07d7b7B472865DD50019F62f374d3B16 Sep 2026
ZyoraSwapRouter0xb3a18a472FBb57F0F4FFFe83900e511675c2671011 Sep 2026
AmmFactory0x1f0740C83E9acC8bc2a83cdE7a42e58F018C817C16 Sep 2026
AmmRouter0x9d754eAAC30c454D218d9c87efdE15Add3793C9e16 Sep 2026
LiquidityLocker0xEe0Ea91c7c1bc7F77ADCe09592EAE1ED59D3076911 Sep 2026

What I would change

Both mainnet bugs were wiring mistakes that a review catches only some of the time. Next time the deploy scripts end with assertions that read the chain back, such as the factory's router, every launcher's registration and the fee constant, so a deploy that leaves the system half-wired fails on the spot instead of waiting for a careful reader.

Sources

  1. 1.Test functions counted in the zyora-program sources; CI run of 16 Sep 2026. Private repository, counted on 16 Oct 2026.
  2. 2.Read-only calls to Arc mainnet and zyora.fun/api/stats. MAX_FEE_BPS, launch and lock counts, and the factory's router, read on 16 Oct 2026.
  3. 3.forge build --sizes in CI, 16 Sep 2026. Matches the runtime code length on chain.
  4. 4.Foundry broadcast receipts and the app's production config. All receipts have status 1.