Skip to main content

Overview

Prophet’s AMM is a binary YES/NO automated market maker built around complete-set accounting. Unlike Uniswap-style constant-product AMMs that trade two existing tokens, Prophet’s AMM creates and destroys matched pairs of YES and NO shares to maintain solvency at all times. The invariant is simple: for every YES share outstanding, there is exactly one NO share outstanding. This means the pool can always pay $1.00 per winning share, regardless of which side wins.

Complete-Set Accounting

The fundamental building block is the complete set: one YES share + one NO share = $1.00 USDT (always). When liquidity enters the pool:
  • $1.00 USDT creates one YES share AND one NO share
  • The pool holds both — these are the reserves
When a market resolves YES:
  • Each YES share redeems for $1.00
  • Each NO share is worth $0.00
  • The total USDT in the pool exactly covers all YES share redemptions, because every YES share was matched with a NO share at creation
This is why the AMM is always solvent — the accounting is self-contained.

Pool State

The AMM tracks three values for each market:
The current YES price is computed from reserves:
Note that YES price + NO price = 1.00 always (they sum to 100% probability).

Seeding: How Liquidity Enters the Pool

When the market-maker agent calls seedLiquidity(collateralAmount):
  1. collateralAmount USDT enters the pool
  2. An equal number of YES shares and NO shares are minted and added to reserves
  3. Initial price is 50/50 — YES at 0.50,NOat0.50, NO at 0.50
Example: seed with 1,000 USDT
Seeding is neutral — it does not move prices. It just establishes the initial liquidity depth.

Buying YES Shares

When a trader calls buyShares(true, collateralAmount, minSharesOut):
  1. collateralAmount USDT enters the pool
  2. The same amount of YES shares AND NO shares are minted (complete-set creation)
  3. The newly minted YES shares are given to the trader
  4. The newly minted NO shares are added to the pool’s NO reserve
  5. A trading fee is taken from the collateral before share calculation
This is why buying YES increases the YES price: more NO shares are in the reserve relative to YES shares (which went to the trader), so the ratio shifts. Worked example: buy YES with 100 USDT
The trader receives 99 YES shares. If the market resolves YES, each is worth 1.00totalreturn1.00 — total return 99.00 on a 100.00investment,lessfees.Thepricemovedfrom100.00 investment, less fees. The price moved from 0.50 to $0.5233 — the larger the trade relative to pool depth, the more slippage.

Buying NO Shares

The mirror image: buyShares(false, collateralAmount, minSharesOut).
  1. collateralAmount USDT enters the pool
  2. YES and NO shares are minted
  3. The newly minted NO shares go to the trader
  4. The newly minted YES shares are added to the pool’s YES reserve
Buying NO increases the NO price (and decreases the YES price) — the YES reserve grows while NO shares leave the pool.

Selling Shares Back to the Pool

Selling is more complex because it is the reverse operation — shares re-enter the pool and USDT exits. The pool must ensure it remains solvent after the sale. The sell equation that governs sellShares(isYes, sharesIn, minCollateralOut):
Where:
  • sameReserve = reserve of the share type being sold (e.g., yesReserve when selling YES)
  • oppositeReserve = reserve of the other share type (e.g., noReserve when selling YES)
  • sharesIn = number of shares being sold
  • collateralOut = USDT to be returned to the seller (the unknown — solver must find this)
Why this formula? It preserves the constant-product relationship of the reserves after the complete-set destruction. When you sell YES shares back, the pool must destroy matched NO shares from its reserve to maintain the YES+NO = complete set invariant. The formula finds the maximum collateralOut such that the pool remains solvent. A trading fee is deducted from collateralOut. Slippage protection: The minCollateralOut parameter lets sellers specify the minimum USDT they accept. The transaction reverts if the AMM would return less — protecting against sandwich attacks and large price movements between tx submission and execution.

Price Discovery in Practice

The AMM price at any moment reflects the current buy/sell pressure. As traders buy YES, the YES price rises, attracting sellers and NO buyers — pushing it back toward equilibrium. This continuous process means the AMM price tracks the market consensus of probability in real time.

Trading Fees

Every buy and sell incurs a fee that accrues to the protocol. Fees are taken from the collateral input (for buys) or output (for sells). The fee breakdown:
  • Oracle fee: funds the AI oracle agent’s 0G Compute usage
  • Market-maker fee: compensates the LiquidityPool for providing initial liquidity
  • Protocol fee: goes to the treasury address
Fees are tracked in the contract and distributed via PayoutDistributor at market resolution.

View Functions

These read-only functions let the frontend display current state without a transaction:
The frontend calls getBuyAmount and getSellAmount before every trade to show the user exactly what they will receive, including the slippage from their specific trade size.

Slippage Protection

All trade functions accept a minimum output parameter:
The frontend sets these based on a configurable slippage tolerance (default 1%). Users can increase this tolerance for large trades where price impact is expected to be higher.

AMM Security Properties

  • Non-zero check: Trades with zero input revert immediately
  • ReentrancyGuard: All state-changing functions are protected against reentrancy attacks
  • SafeERC20: All token transfers use OpenZeppelin’s SafeERC20 — no raw transfer() calls
  • Solvency invariant: The sell equation is derived to maintain pool solvency — the pool can never be drained to insolvency by a single trade
  • 6-decimal USDT: All amounts use parseUnits("X", 6) — the contracts assume 6-decimal USDT throughout
All USDT amounts in the contracts use 6 decimal places. 100 USDT = 100_000_000 in contract units. If you interact directly with contracts, always use ethers.parseUnits("100", 6) — never ethers.parseEther("100").