market design live web3 dapp

ERC-20 Prediction Market

Same LMSR economics as the ledger market — but every Yes / No claim is an ERC-20 in the user’s wallet, so positions become portable and composable.

Practice mix
Legal Build Model Spec Research Teach

Context

From ledger balances to ERC-20 claims

Same LMSR economics as the on-ledger market — different representation. Why tokenize, where it runs, and what changes when Yes/No leave the venue as ordinary tokens.

Intent

The ledger market keeps Yes / No exposure as internal balances. That is enough to trade (and to interpret prices as probabilities), but the position never leaves the venue: it cannot be sent to another address, parked it in a wallet as an ordinary asset, or plugged into another contract or protocol.

This project changes the claim representation, and makes it transferable, without changing the economics. We are still dealing with a binary (two exclusive & exhaustive event options) LMSR (logarithmic market scoring rule, shared by all market participants, allowing inventory mutations by changing collateral amounts) and use PTE (the erc20 "stable" token) as collateral. We allow the same create market → trade position → close market → resolve market → claim winnings lifecycle. What changes is that a buy mints Yes or No ERC-20s into the trader’s wallet, and a sell-back or payout burns them. Pricing still uses the LMSR cost function: a trade that moves outstanding quantities from qq to q+Δq+\Delta costs C(q+Δ)C(q)C(q+\Delta)-C(q). What changes is only where qq lives: as the Yes/No totalSupply of the inventury quantity vector ( (qA,qB)(q_A,q_B)) in the respective contracts, instead of an internal mapping in the market contract itself.

Where it runs

This version of the market is live at pm-erc20.tokenengineeringlabs.com on Sepolia and UZH ETH PoS. What we've built on top of that — since the ERC-20 native transferability of claims allows for that & we wanted to immediately demonstrate this property — is a companion secondary market for these outcome tokens, live at dex.tokenengineeringlabs.com and covered in the ERC-20 CFMM DEX write-up.

Why tokenize at all

Once a claim is an ERC-20, the prediction market stops being a closed loop, and it becomes an open economy. Claim ownership is detached from the market mechanisms and the balance starts to live in the user’s wallet (instead of being forced into the market contract) under the same transfer and allowance rules as every other fungible token in DeFi. This allows composability with any contract that can hold ERC-20s

That separation between claim representation and market logic matters. The LMSR contract remains the primary venue — minting and burning against collateral, setting the on-market price. But the claim itself is no longer trapped there. Anything that understands ERC-20s can interact with it without knowing anything about LMSR, oracles, or market roles. Claim ERC20s become open market instruments.

Architecture

How the market creates claim tokens

Three contract types — market venue, token factory, and per-market outcome ERC-20s — plus where PTE collateral actually sits, and what is shared versus duplicated per marketId.

Contract types

The system design splits several concerns into multiple contract types:

  • the market contract: is the LMSR venue, market registry, and collateral custody,
  • a token factory: has to be invoked once, and then deploys Yes/No claim ERC-20s for each new market
  • per-market outcome ERC-20s: keeps track of the claim tokens traders actually hold.

The first two are unique on-chain and the latter is duplicated per market.

High-level system architecture: Creator CREATE flow and Trader TRADE with PTE go to shared PredictionMarketERC20.sol, which calls OutcomeTokenFactory.sol to deploy per-market Yes and No OutcomeToken.sol contracts, and mints or burns those tokens on trade.
High-level view: one shared market venue and factory; each market gets a fresh Yes/No ERC-20 pair. The market holds PTE and is the only minter / burner of outcome tokens.

1. Market contract — PredictionMarketERC20.sol

One LMSR venue per system deployment. It owns the economics, the market registry, and custody of PTE used as collateral for all transaction. It does not store trader Yes/No balances internally (that was the ledger design); claims live on outcome-token contracts instead.

Three different kinds of things “live” on this contract:

  • External functions (what callers invoke). The public lifecycle API: createMarket, buyShares, sellShares, allow to close markets / resolve outcomes / claim winnings, plus allow a role-gated admin functionality. These are the transactions users and operators send, through interacting with the UI or by calling these functions directly.
  • Pricing logic (how those trades are costed). Internal / view helpers such as costFunction, costToBuy, sell-refund math, and quote / implied-probability reads. This is necessary for the UI and the trades to "know" the correct prices at the right time.
  • On-chain state (what the venue remembers). A markets[marketId] registry: each id has a struct with the question, options, end time, liquidity parameter b, resolution flags, the tokenYes / tokenNo addresses for that market, and per-market collateral / subsidy accounting fields.

Where is the PTE?

PTE is its own ERC-20 (the collateral token), referenced by the market as bettingToken. The market does not “embed” PTE; it holds PTE balances the way any address holds an ERC-20:

  • On create / buy, PTE is transferred into the market contract (bettingToken.safeTransferFrom(…, address(this), …)).
  • On sell / claim / residual withdraw, PTE is transferred out of the market contract to the trader or creator.
  • So the tokens themselves sit as PTE.balanceOf(PredictionMarketERC20) — one pooled ERC-20 balance at the market’s address.
  • Separately, markets[marketId].collateral (and subsidy) is bookkeeping: how much of that pool is attributed to each market id for LMSR / settlement math. That uint is accounting over the shared PTE held by the venue.

2. Token factory — OutcomeTokenFactory.sol

Also a single contract, deployed in the market’s constructor (new OutcomeTokenFactory(address(this))) with the market locked in as its only allowed caller. Its only job is one function, createOutcomePair(…): when the market calls it, the factory deploys two fresh OutcomeToken contracts (Yes and No, with the given names/symbols) and returns their addresses as (tokenYes, tokenNo) back to the market contract.

The factory does not price trades, mint or burn claims, hold PTE, or store trader balances. It only deploys the outcome tokens for a new market, and reports them back.

3. Per-market outcome tokens — OutcomeToken.sol

A standard ERC-20 plus two restricted hooks: mint(to, amount) and burn(from, amount). Those two can be called only by the market contract (have an onlyMarket guard, such that any other caller reverts). While other functions such as transfer, approve, and balanceOf are ordinary ERC-20 functions: anyone holding the token can move it. There is no marketId inside the token: each deployed instance is exactly one fungible claim asset for one side of one market.

Each createMarket deploys two new instances (Yes and No) for that id. After NN markets there are 2N2N outcome-token contracts. Shared across all of them: one market venue, one factory, and one PTE token. Duplicated per marketId: the Yes/No pair and the venue’s markets[id] row.

Lifecycle

Flows

How create, buy, sell, and claim wire those contracts together — separate from the contract inventory above.

Create flow

One transaction creates one new marketId, writes the market parameters, and deploys that market’s claim contracts:

  1. An address with CREATOR_ROLE calls createMarket(question, optionA, optionB, duration, liquidityParam) on PredictionMarketERC20.sol.
  2. The venue assigns a marketId and writes the Market struct (question, options, creator, b, end time, …).
  3. Still in that transaction, it calls outcomeFactory.createOutcomePair(marketId, …).
  4. The factory deploys two OutcomeToken contracts (each with the venue as immutable market for mint/burn) and returns (tokenYes, tokenNo). The venue stores those addresses on markets[marketId].
  5. The venue calculates the LMSR subsidy / collateral required by the bounded loss and pulls that PTE from the creator into address(this). Outcome-token supplies start at zero until someone buys claims.

Buy flow

Once those contracts exist, any trader can call buyShares(marketId, …). The venue then:

  1. looks up markets[marketId].tokenYes / tokenNo,
  2. reads totalSupply() on each as (qA,qB)(q_A,q_B),
  3. prices the trade via the LMSR cost function ( C(q+Δ)C(q)C(q+\Delta)-C(q)),
  4. pulls PTE from the trader into the market’s PTE balance and updates markets[marketId].collateral,
  5. calls mint(trader, Δ) on the Yes or No OutcomeToken for that id. The market is the only allowed caller; the tokens are credited to the trader’s address as a normal ERC-20 balanceOf(trader).

From that moment the claim lives in the buyer’s wallet address. Wallets like MetaMask still usually need those token contracts added / imported before they show a balance (each market ships a fresh Yes/No pair). See Holding claims in a wallet below for the UI path — visibility in the wallet is separate from ownership; importing only surfaces a balance that is already on-chain.

Sell flow

Once a trader owns claims, one option is to sell them back to the LMSR at current prices via sellShares(marketId, …). The venue then:

  1. looks up that id’s tokenYes / tokenNo and reads totalSupply() as (qA,qB)(q_A,q_B),
  2. checks that the trader already holds enough of the side being sold (balanceOf(trader) on that outcome token),
  3. prices the reverse inventory move ( C(q+Δ)C(q)C(q+\Delta)-C(q) with negative Δ\Delta),
  4. calls burn(trader, Δ) on that Yes or No OutcomeToken, removing the claim from the trader’s balance,
  5. sends PTE from the market’s balance back to the trader and updates markets[marketId].collateral.

So buy attributes outcome tokens to the trader; sell destroys them from that same address.

Claim flow

After the market is resolved, a holder of winning claims calls claim (or the equivalent payout entrypoint) on the venue:

  1. the venue checks resolution and which side won,
  2. burns the trader’s winning outcome tokens,
  3. pays PTE out from the market’s custody to the trader.

Peer-to-peer transfer of outcome tokens never touches the venue and does not change LMSR inventory via mint/burn — that is the composability hinge (and why a DEX can reallocate claims without buyShares).

Putting the flows on one diagram

The Architecture overview above shows the shared venue and factory versus per-market Yes/No tokens. The figure below is intentionally denser: it stacks the create path (market → factory → deploy both tokens) next to the trade path (market → mint/burn on those same tokens) and shows where PTE sits. It also makes the cardinality explicit: after NN markets there are 2N2N outcome-token contracts.

Contract architecture: Creator calls createMarket on PredictionMarketERC20.sol, which calls OutcomeTokenFactory.createOutcomePair to deploy Yes and No OutcomeToken.sol contracts for this marketId. Trader buyShares or sellShares on the market mint or burn on those tokens. Shared venue and factory; per marketId a fresh Yes/No pair. After N markets, 2N outcome-token contracts.
Create: market → factory → Yes and No (new addresses for this marketId). Trade: market → mint / burn on those same contracts. PTE collateral is held as an ERC-20 balance on the market address. The next marketId deploys another fresh Yes/No pair. Secondary transfers only reassign outcome-token balances; they do not change LMSR qq via mint/burn.

Interoperability

Claims outside the venue

Minting ERC-20 claims does more than price trades and track ownership: it puts a standard ERC-20 in the trader’s address. From there the claim can show up in a wallet, move peer-to-peer, or plug into other contracts — without calling the market again. That is full composability with other protocols.

Holding claims in a wallet

After a buy, the outcome tokens already sit on the trader’s address. Most wallets do not auto-list every newly deployed Yes/No pair, so balances only become visible once the tokens are added manually. The dapp surfaces that market’s tokens by symbol (e.g. E9S-38-Y / E9S-38-N — prefix, market id, side) and offers a one-click Add claim tokens to wallet flow that prompts MetaMask to import them.

Market card for “Will the demo work?” showing Yes and No at 50%, token chips E9S-38-Y and E9S-38-N with an Add to wallet tooltip, and an Add claim tokens to wallet section to import outcome ERC-20s into MetaMask.
Each market exposes its Yes/No tickers on the card; click to suggest the token to the connected wallet, or use Add claim tokens to wallet for the same import path.
MetaMask dialog titled Add suggested tokens, listing E9S-38-Y with balance 0, and Cancel or Add token actions.
MetaMask’s suggested-token prompt for E9S-38-Y. After adding, the claim balance is a normal wallet ERC-20 — transferable and approvable; only the market may mint or burn.

What you can do with an ERC-20 claim

Tokenization is useful for multiple reasons:

  • Peer-to-peer / OTC. Send Yes or No to a friend — or your grandma — for bilateral settlement or just as a gift. No contract required: a plain wallet-to-wallet transfer.
  • Custody and inventory. Hold exposure in a hot wallet, cold address, or operational account the same way you hold any other token.
  • Other applications. Approve and deposit into any contract that accepts ERC-20s: escrow, lending experiments, vaults, or a secondary market.

The last point is the real motivation. Outcome tokens should be usable wherever fungible tokens are usable in the broader crypto stack — not only inside the prediction-market dapp. A secondary-market DEX is one concrete instance of that idea, and one we implemented to demonstrate the property.

Composability, illustrated: a CFMM DEX

To make the external path tangible, we seed an xy = k CFMM against these outcome tokens (and alternatively PTE). Swaps on that pool are distinct from the LMSR contract’s logic and pricing and do not rewrite LMSR inventory — they only change who holds the claims. Primary pricing stays with the market maker; the DEX is a separate venue that moves ERC-20 balances. That setup also lets us study liquidity-provisioning incentives, arbitrage opportunities, and alternative trading mechanisms across primary and secondary markets — treated in a separate article.

That is enough to build intuition for what “composability” requires in practice: known token addresses, allowances, liquidity, and a contract that never needs to call buyShares. Design and UX for the pool live in the ERC-20 CFMM DEX article — this write-up only establishes why the tokens are worth routing there.

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.
DEX operator — create an xy = k pool linked to an existing LMSR market (here #38). The pool trades that market’s claim ERC-20s; it does not call buyShares. Fee is fixed at creation.

Limits

What this representation costs

Binary ERC-20 pairs prove composability but at the same time show why the same pattern does not scale to combinatorial claim spaces.

The cost of doing it this way

Every market ships two new ERC-20 contracts. Each claim the user cares about is another address to import, another allowance to manage, and — if you want secondary liquidity on the DEX — another pool to seed. A handful of example markets is manageable, but a production grade market place goes way beyond that.

That is a representation problem, not an LMSR problem. Why one-token-per- outcome breaks down for richer event spaces, and what Conditional Token / ERC-1155 architectures do instead, is the subject of Claim Representation and Implementation Standards. This article only flags the operational debt; that one develops the argument and the next implementation path ( ERC-1155 Prediction Market).

Takeaway

ERC-20 claims prove the composability thesis for binary markets: mint into a wallet, transfer peer-to-peer, route into other apps. The CFMM DEX is a working example of that last step (a secondary market that never touches the LMSR) and a template for using the same tokens elsewhere in the stack.

The representation does not scale to Hanson-style claim spaces without exploding contract and UX surface. We'll have to move on to claim representation standards and ERC-1155 for that.

Related