market design live web3 dapp

ERC-20 CFMM DEX

A secondary xy=kxy = k venue. Tailored to the ERC-20 prediction market’s claim tokens, to demonstrate transferability and composability with other protocols. An independent market venue without rewriting LMSR inventory.

Practice mix
Legal Build Model Spec Research Teach

Context

Why a second venue at all

The primary market (LMSR PM) mints portable Yes / No ERC-20s. This DEX is the first concrete place those tokens can go, but we also want to demonstrate dual price discovery before event resolution and price alignment between multiple venues.

Intent

The ERC-20 prediction market proves that LMSR claims can leave the venue as ordinary tokens. Now we want to ask where they can go to and build a contract that uses those tokens without knowing anything about logarithmic market scoring rules or any claims associated with event outcomes.

We select a contract from the CFMM contract family, a Uniswap-V2-style constant-product DEX whose pools hold two tokens and price them against each other on a curve. We allow Yes / No ERC-20 tokens of the same event (and optionally PTE) which have been already issued by the LMSR primary market. By this construction we want to demonstrate a few things: secondary trading, liquidity provision, and how prices and price finding processes might differ between different venues, especially if the underlying mechanisms are different.

Where it runs

The DEX dapp is live at dex.tokenengineeringlabs.com on the same networks as the tokenized market: Sepolia and UZH ETH PoS. Pools are created against markets from pm-erc20.tokenengineeringlabs.com; using the same token notations across both frontends (for example PTE-E9S, E9S-{id}-Y / -N).

What this is not

This is not a primary market, and has nothing to do with the event resolution, the eligibility to claim collateral at resoution, not is it a required step in the prediction-market lifecycle. You can buy Yes on the primary market, never open the DEX, and still settle after resolution. The DEX is an optional secondary surface: useful when someone wants to rebalance exposure against pool reserves (although this is also directly possibly by adjusting LMSR inventory), provide liquidity, or study how two independent pricing rules sit on the same ERC-20 balances.

Architecture

Factory, pair, router

Three contract types — who may create pools, where reserves live, and what wallets approve — plus how a pool stays linked to an LMSR marketId.

Contract types

The DEX splits the usual constant-product pattern into three pieces:

  • DexFactory — registry and gate for creating pools. Pool creation requires POOL_CREATOR_ROLE; swaps do not.
  • ConstantProductPair — one pool instance: holds the two ERC-20 reserves, enforces xy=kxy = k, and issues its own LP ERC-20 to liquidity providers.
  • DexRouter — the entrypoint traders and LPs approve. It routes swap, addLiquidity, and removeLiquidity into the right pair.

Outcome tokens and PTE are not deployed by the DEX. They are the same contracts the prediction market already minted or referenced. The pool only needs their addresses, allowances, and balances.

DEX architecture: external Yes/No/PTE ERC-20s from the prediction market; DexFactory (POOL_CREATOR_ROLE) deploys ConstantProductPair pools with xy=k reserves and LP tokens; traders and LPs use DexRouter for swap, add, and remove.
Factory deploys pairs; pairs hold reserves under xy = k; the router is what wallets approve. Claim tokens are imported addresses, not DEX-minted assets.

Pool types and fees

DexFactory.createMarketPool creates at most one pool per (marketId, poolType). v1 PoolType values are only:

  • YES_NO — Yes claim ↔ No claim.
  • YES_PTE — Yes claim ↔ PTE collateral (predictionMarket.bettingToken()). There is no NO_PTE enum in this deployment.

The swap fee is set once as immutable feeBps on the pair (default constant DEFAULT_FEE_BPS = 30, i.e. 0.30%; hard max MAX_FEE_BPS = 1000). After creation the fee does not change — operator policy is visible up front and cannot drift mid-experiment.

Resolution gate (contract vs UI)

On-chain, ConstantProductPair gates trading with _requireMarketActive(): it reads the linked market’s resolved flag and reverts with "market resolved" on swap and mint (add liquidity). burn (remove liquidity) does not call that check, so LPs can always unwind residual reserves after resolution.

The dapp is stricter for teaching UX: when the underlying PM market is expired or resolved, the pool workspace freezes swap / add controls and leaves remove-liquidity as the exit path. Expiry alone does not flip the Solidity gate — only resolution does — but the UI treats a closed market as done for new flow.

Settlement of winning claims still happens on the primary market (burn winners, pay PTE). The DEX does not pay out event outcomes; it only returns whatever ERC-20 balances remain in the pair.

Lifecycle

Flows

Operator creates and seeds; anyone swaps; LPs enter and exit — all against tokens that already exist in wallets.

Create pool

Pool creation is the privileged step that wires a secondary market to a primary one:

  1. An address with POOL_CREATOR_ROLE picks an existing LMSR marketId, a pool type (Yes/No or Yes/PTE), and a feeBps.
  2. The factory deploys a ConstantProductPair and records the link to that market.
  3. The operator (or another funded LP) seeds initial reserves via addLiquidity so the invariant has a starting xy=kxy = k.

Until someone deposits both sides, the pool exists but has nothing to trade against. Seeding is a separate economic choice from creating the market on the LMSR venue.

DEX operator Create pool form: LMSR market #38 Will the demo work?, pool type Yes / No, swap fee 30 bps, Create pool button. Requires POOL_CREATOR_ROLE; fee immutable per pool.
Operator — create an xy = k pool linked to an existing LMSR market (here #38). The pool will trade that market’s claim ERC-20s; it does not call buyShares. Fee is fixed at creation.

Add and remove liquidity

Liquidity providers deposit both reserve assets in the ratio the pool currently prices, receive the pair’s LP ERC-20, and later burn LP tokens to withdraw a proportional share of reserves (minus fees collected by the pool’s accounting). After market resolution, new deposits stop; withdrawals remain so inventory is not stranded.

Swap

A swap is an ordinary ERC-20 exchange against the pair’s reserves under the constant-product rule (with the pool’s feeBps taken from the input). The trader’s wallet sends token A and receives token B; the pair’s balances update; Yes/No totalSupply on the outcome contracts does not change.

That last point is the whole composability claim in one sentence: the secondary venue can rearrange who holds claims without rewriting the primary market’s inventory (qA,qB)(q_A,q_B). Only LMSR mint and burn move qq.

Economics

Two prices, one token

Dual venues share the same claim ERC-20s but not the same pricing rule. Curvature, issuance, LP, and exit paths are the interesting part — not just that two numbers can differ.

Dual price discovery

After tokenization, the same Yes token can show two different prices at once:

  • Primary (LMSR). The cost of minting or burning against the market’s cost function C(q+Δ)C(q)C(q+\Delta)-C(q) — the quote the prediction-market UI shows for buyShares / sellShares.
  • Secondary (CFMM). The marginal rate implied by pool reserves under xy=kxy = k, after the pool’s immutable feeBps.

Those quotes are free to diverge. When they do, anyone who can trade on both venues can attempt arbitrage: buy cheap on one side, sell rich on the other, until costs (fees, gas, inventory, resolution risk) stop the trade. That loop is an economic coupling. There is no protocol call from the DEX into the LMSR that “syncs” prices.

Same Yes ERC-20 priced on primary LMSR at 0.62 PTE and secondary CFMM at 0.57 PTE. An arbitrage trader loops between venues economically; only primary mint/burn moves inventory q; secondary swaps change pool reserves only.
Same Yes token, two quotes. Divergence is allowed; arbitrage is a trader using both venues — not a DEX→LMSR sync call. Example numbers are illustrative.

LMSR is already a market — primary and secondary in one

It helps to be precise about what “primary” means here. An LMSR venue is not only an issuer. It is a continuous automated market against a shared cost function: every buy mints claims into existence (issuance), every sell burns them back (redemption), and the marginal price after each trade is whatever the cost function says at the new inventory (qA,qB)(q_A,q_B). In that sense the LMSR is both the place where shares are created and the place where they are priced against collateral — a primary issue desk and a secondary book in one mechanism.

How sensitive that book is to a given trade size is governed by the liquidity / curvature parameter bb chosen at market creation. Small bb: the same Δ\Delta moves prices a lot (cheap to open, twitchy). Large bb: prices move slowly (deeper book, more subsidy). That curvature is the LMSR’s answer to “how much does this trade hurt the mid?”

The DEX is secondary only — same idea, different bonding curve

The CFMM is also a bonding-curve market, but it never issues claims. It only re-prices and redistributes tokens that already exist. Liquidity providers deposit Yes / No (or Yes / PTE) into a pair; those balances become the inventory other traders can swap against under xy=kxy = k. A swap changes pool reserves and therefore the DEX mid — and nothing else about LMSR qq.

So both venues answer “what is Yes worth right now?” with a curve, but they answer different operational questions:

  • LMSR: what does it cost to create or destroy this exposure against the shared subsidy / collateral pool?
  • DEX: what does it cost to exchange existing balances against whoever LPed into this pair?

That is why a trade of size Δ\Delta can have completely different slippage on the two venues even when the starting mids look close. LMSR slippage is shaped by bb and the current (qA,qB)(q_A,q_B); CFMM slippage is shaped by reserve depth, the constant-product geometry, and feeBps. Cross-checking average trade slippage, quoted mid, or a median fill across venues is therefore a real empirical exercise on this stack — not a duplicate meter reading the same formula twice.

Same trade buy delta Yes: primary LMSR mints and updates q with illustrative price impact +0.04 PTE; secondary YES/PTE CFMM moves pool reserves with q unchanged and illustrative impact +0.11 PTE. Compare mids or slippage — not the same formula twice.
Same Δ\Delta, two bonding curves. Illustrative impacts only — LMSR follows bb and qq; the CFMM follows reserve depth and xy = k.

What do you do with your shares?

Once Yes or No sits in a wallet, the holder faces a portfolio choice the ledger market never offered:

  • Hold to resolution. Do nothing until the market resolves, then claim through the primary venue if you hold the winning side. You earn the event payoff (or zero) — not trading fees.
  • Sell back to the LMSR. Burn via sellShares while the market is still open; exit into PTE at the primary curve.
  • Trade on the DEX. Swap into the other claim or into PTE against pool reserves without touching issuance.
  • Provide liquidity. Deposit into a Yes/No or Yes/PTE pool, receive LP tokens, and earn a share of the pool’s swap fees while your inventory sits in the curve. You are paid for making the secondary book — and you take on inventory / impermanent-loss style risk relative to simply holding the claims.

LP is the clearest “new verb” tokenization unlocks here: the same shares that used to be trapped as ledger balances can now earn fee income by staffing a second venue.

Wallet holding Yes and No ERC-20 claims with four paths: A hold to resolution, B sell to LMSR via sellShares burn, C swap on DEX, D provide liquidity for fees with inventory and IL-style risk — the new verb unlocked by tokenization.
Four exits from a tokenized claim: wait, sell primary, swap secondary, or LP for fees (with inventory risk).

Withdrawing, selling back, and waiting for convergence

LP is not free optionality. To sell back into the LMSR, or to hold a naked claim through resolution, you usually need the underlying Yes/No (or PTE) in your wallet again — which means removeLiquidity first. That exit has consequences:

  • You realize whatever reserve ratio the pool currently prices (your LP share of token0 / token1), which may differ from the mix you deposited.
  • You stop earning fees the moment you leave the pool.
  • Only after withdrawal can you sellShares on the primary market, transfer peer-to-peer, or simply wait for resolution and claim.

If the two venues disagree, waiting can be a strategy as much as trading: in expectation, active arbitrage and overlapping trader sets push the quotes toward each other until frictions dominate. That convergence in expectation is an economic claim, not a consensus rule — spreads can persist when pools are thin, fees are wide, gas is expensive, or resolution is near and inventory risk dominates.

Left: LP exit sequence removeLiquidity then Yes/No/PTE in wallet before sellShares, hold to claim, or swap again. Right: LMSR mid 0.62 vs CFMM mid 0.38 with arbitrage trader; convergence in expectation not guaranteed when pools are thin, fees, gas, or near-resolution risk dominate.
Unwind the pool before primary exit or resolution hold. Dual-venue quotes are expected to converge when arbitrage is cheap — not when frictions dominate.

Closing the book: resolution and exploit prevention

After the event is decided, continuing to swap against stale reserves would be a gift to anyone who already knows the outcome. The pair therefore gates swap and mint (add liquidity) once the linked market’s resolved flag is true; burn / remove liquidity stays open so LPs are not stranded. The dapp goes further for teaching UX and freezes new flow when the market is expired or resolved, leaving remove as the exit path.

Settlement value still comes only from the primary market’s claim path. The DEX never pays winners; it only returns whatever ERC-20 balances remain in the pair. That separation is deliberate exploit prevention: secondary trading cannot mint a free lottery ticket after the result is known, and it cannot rewrite LMSR inventory after the fact.

Closing the secondary book: market open allows swap and LP; expired UI freezes swap and add; resolved on-chain reverts swap and mint while burn/remove stays OK and settlement is primary claim only. Solidity gate is resolved; UI also freezes expired. DEX never pays winners or rewrites q.
Open → expired (UI freeze) → resolved (on-chain revert). Remove liquidity stays available; winners are paid only on the primary venue.

What a DEX swap does not do

  • It does not mint or burn Yes / No.
  • It does not move PTE collateral held by the prediction market.
  • It does not change LMSR qq.
  • It does not resolve the event or pay winners.
  • It does not sync the LMSR quote on-chain.

It only moves ERC-20 balances between the trader and the pair (and updates the pair’s internal reserves / LP accounting). For the ownership / mint-boundary diagram on the primary-market article, see ERC-20 prediction market (docs/pm-erc20-lmsr-dex-composability-brief.md).

Surface

How the dapp presents it

Course-style onboarding, a pools list linked to LMSR markets, and a Simple / Analytical mode toggle for teaching depth.

Onboarding

The live UI mirrors the prediction-market dapp’s checklist pattern: connect a wallet, match the app network (Sepolia or UZH ETH PoS), import the deployment-tagged PTE, approve the DEX router, then open a pool to swap or provide liquidity. Gas is native ETH on the chosen chain; trading inventory is PTE and claim ERC-20s.

Pools as the unit of attention

Traders do not browse “all ERC-20s.” They browse pools linked to prediction markets — each row carries the market question, pool type, fee, and the actions that matter (swap, add, remove). That framing keeps the secondary venue readable in a classroom: the pool is a consequence of a named event market, not an anonymous pair address.

Simple vs Analytical

The dapp exposes a header toggle (shared pattern with the PM app). Simple (default) keeps swap and liquidity controls — connect, approve, trade — without CFMM curves or heavy stats. Analytical adds the curve / reserve breakdown on the pool workspace. Same contracts; two depths of UI.

Curation chrome

Pool cards also carry light curation controls from the E9 UI sweep: Hide for me (browser-local), and curator Archive for all when the caller has PM CREATOR_ROLE or factory POOL_CREATOR_ROLE. Purple wallet affordances import the pool’s outcome claim tokens and the LP ERC-20 the same way the PM dapp imports Yes/No.

Bridge

Toward the World Cup experiment

Dual-venue rails on UZH ETH PoS are what make a live classroom market more than a single buy button.

Why this comes before the field write-up

The UZH World Cup prediction market is a field experiment: participants traded live during a summer-school setting on the tokenized market stack. To talk honestly about what they could do — primary buys, wallet custody of claims, optional secondary swaps, operator-seeded liquidity — the secondary venue has to be documented as its own surface.

This article is that surface. The World Cup write-up can then focus on experiment design, participation, and what the classroom learned, instead of pausing to explain xy=kxy = k and POOL_CREATOR_ROLE.

Takeaway

ERC-20 claims make a second venue possible; the CFMM DEX makes it concrete. The LMSR both issues and prices; the DEX only re-prices what LPs supply — same token, different bonding curve, different slippage for the same Δ\Delta. Holders can wait for resolution, sell primary, swap secondary, or earn fees as LPs; in expectation the two quotes converge when arbitrage is cheap enough. Operators gate pool creation; resolution freezes trading but not LP exit; LMSR inventory moves only on the primary market. That dual-venue surface is the composability demo — and the mechanical preface to the World Cup experiment narrative.

Related