Trading

Polymarket's exchange contract is permissioned. You can't just trade any CTFs on the exchange. The token ID has to be whitelisted. Order matching is also permissioned, an exchange operator has to match and execute trades for traders.

Traders submit orders off-chain by signing the Order struct.

Terminologies

Complement: Two token IDs from an ERC-1155 contract are said to be complement of each other if they represent two sides of a condition.

Maker Order: A maker order is typically the order that first lands on the orderbook with a limit price.

Taker Order: A taker order is typically the order that lands on the orderbook after the maker order in a trade, but it does not necessarily mean it's a market order.

Maker Amount: If it's a buy, then it's the USDC amount. If it's a sell, then it's CTF amount.

Taker Amount: If it's a buy, then it's CTF amount. If it's a sell, then it's USDC amount.

Mint Trade: One side of the trade bids YES and the opposite side of the trade bids a complementary NO.

Merge Trade: One side of the trade sells YES and the opposite side of the trade sells a complementary NO.

Complementary Trade: One side of the trade wants to sell a YES or NO token to the opposite side of the trade.

Token Registration

Polymarket's admin whitelists a token by calling the function registerToken. token0 and token1 have to be the complement of each other (the YES and NO of the same question) but there is nothing stopping the admin from using the wrong token IDs.

Once the token ID is listed on the registry, both tokens become tradable on the CTF exchange.

function _registerToken(uint256 token0, uint256 token1, bytes32 conditionId) internal {
    if (token0 == token1 || (token0 == 0 || token1 == 0)) revert InvalidTokenId();
    if (registry[ token0].complement != 0 || registry[ token1].complement != 0) revert AlreadyRegistered();

    registry[ token0] = OutcomeToken({complement: token1, conditionId: conditionId});

    registry[ token1] = OutcomeToken({complement: token0, conditionId: conditionId});

    emit TokenRegistered(token0, token1, conditionId);
    emit TokenRegistered(token1, token0, conditionId);
}

The Order Struct

The gist is the tokenId is always going to be the CTF to be bought or sold depending on the order's side. If it's a buy order, the maker amount is the USDC amount the buyer is willing to pay and the taker amount is the CTF amount the buyer wants. If it's a sell order, the maker amount is the CTF amount the seller is willing to part with and the taker amount is the USDC amount the seller wants. There are match types and we will talk about them in upcoming pages.

Trading Logic

Validations

matchOrders is pretty much the only function used for trading. There is another function fillOrders but I don't ever see it being used.

The operator always matches a taker order against a list of maker orders and the operator has to specify the fill amount for each order. The fill amount is always in terms of the order's maker amount. If the taker order is a buy order, then takerFillAmount is the amount of USDC to fill. If a maker order is a sell order, then makerFillAmounts[i] is the amount of CTF to fill. It makes sense because you need to match USDC against CTF to create a trade (at least for complementary trades, there are other trade types and we will explore them in a bit).

matchOrders perform the following checks:

  1. Certain orders have a specified taker for private trades and it checks msg.sender is the taker (I don't think it's ever used)

  2. Order isn't expired

  3. Order signature is valid. Valid signers can be an EOA, Polymarket's proxy wallet or Polymarket's Gnosis safe

  4. Order's feeRateBps is not above the contract's max fee rate bps which is 10% (Polymarket doesn't charge a fee at the moment so it's also not going to hit)

  5. The token ID is registered

  6. The order is not fully filled or cancelled. Each fill increments the order's fill amount until it is fully filled. Orders can be cancelled on-chain but I also have not seen it being available as a feature from the UI (Pro-traders might use it). It isn't as important as a permissionless exchange to have on-chain cancellation if the traders trust the operator to not execute orders cancelled off-chain.

  7. The order's nonce is valid. Each order comes with a nonce and it has to be equal to the order maker's current nonce. Multiple orders can share the same nonce. This is also likely something to be used by pro-traders.

Remaining Liquidity Check

During order validation, the exchange verifies the order's maker amount is at least as much as the order's remaining amount. It reverts if there is insufficient liquidity and sets the order to be filled if after the match there is 0 remaining liquidity. The order's remaining liquidity is also decremented.

Taker Order's Taker Amount

The actual taker order's taking amount is calculated with the formula below. If takerFillAmount is 100% of the taker order's maker amount then it cancels out with the denominator and we end up with the full taker order's taker amount. If not it gives us a portion of the taker amount.

takingAmount=takerFillAmount×takerOrderTakerAmounttakerOrderMakerAmount\text{takingAmount} = \text{takerFillAmount} \times \frac{\text{takerOrderTakerAmount}}{\text{takerOrderMakerAmount}}

The maker and taker asset IDs are defined in terms of the taker order. If the taker order is a buy order, then the maker asset is USDC and the taker asset is CTF and vice versa. Polymarket uses 0 to represent USDC.

Fill Maker Orders

The exchange transfers the taker's maker asset to the exchange. It is all very confusing with the terminologies, but remember a maker asset is the asset the order maker has, and a taker asset is the asset the order maker wants.

Then it calls an internal function _fillMakerOrders with the arguments _takerOrder, makerOrders and makerFillAmounts. This function fills maker orders with the exchange acting as the counterparty.

There are 3 types of orders. A taker order does not necessarily have to be of opposite type of a maker order. They can both be buy or they can both be sell for the reasons below.

  1. If the trade is a complementary trade, then the taker order and the maker order must be of opposite types and the token ID must be the same. ie. The maker buys token 1 from the taker or sells token 1 to the taker.

  2. If the trade is mint trade, then the maker and the taker order must both be a buy. Their token IDs must be complement of each other. ie. The maker bids YES for 0.6c and the taker bids NO for 0.4c, the combined price is equal to $1 and it can deposit the $1 into the CTF contract via splitPosition to mint 100 YES shares for the maker and 100 NO shares for the taker.

  3. If the trade is merge trade, then the maker and the taker order must both be a sell. Their token IDs must be complement of each other. ie. The maker sells 1 YES for 0.6c and the taker sells 1 NO for 0.4c, the combined price is equal to $1 and it can merge 1 YES and 1 NO via the CTF's mergePositions to get back $1, with 0.6c going to the maker and 0.4c going to the taker.

_validateTakerAndMaker checks if the token IDs are equal or are complementary in the case of mint or merge.

Another validation is on the orders' prices. Orders cannot be matched against each other if their prices do not cross. This means

  1. For a mint order, the combined price has to be ≥ 1. They need to provide at least $1 in total to receive 1 YES and 1 NO.

  2. For a merge order, the combined price has to be <= 1. The CTF contract cannot give more than $1 in total to the order maker and taker.

  3. For a complementary trade, the bid price must be ≥ the ask price. The seller must get at least as much as what he asked for.

After validations, similar taking amount calculation is performed on each maker order. The fillAmount is the maker order's making amount.

takingAmount=makerFillAmounts[i]×makerOrders[i].takerAmountmakerOrders[i].makerAmount\text{takingAmount} = \text{makerFillAmounts[i]} \times \frac{\text{makerOrders[i].takerAmount}}{\text{makerOrders[i].makerAmount}}

There is a fee on the trade if the order's feeRateBps is nonzero. As far as I know Polymarket does not charge a fee and we will go further into details in the Fees page.

With all the information available, the exchange now fills the maker order against the exchange.

  1. It transfers the maker's maker asset to the exchange.

  2. _executeMatchCall does nothing if it's a complementary trade, splits position if it's a mint trade and merge positions if it's a merge trade.

  3. After executing the match call, the contract must be able to fill the maker order's amount of want tokens. If it is a complementary trade, _executeMatchCall does nothing but it should have already pulled enough tokens from the taker order to fill the maker order. If it's a mint trade, the position split should have minted sufficient CTFs to be sent to the maker. If it's a merge trade, the positions merge should have redeemed sufficient USDC to be sent to the maker.

  4. Finally, it sends the maker's "want" tokens to the maker after a fee (if any).

Execute Taker Order Transfer

After fulfilling all maker orders, the exchange still needs to fulfil the taker order as the taker hasn't received anything yet!

First, it checks if there is enough tokens to be sent to the order taker. If there is a surplus (more tokens than what the taker asked for), the exchange gives the taker the surplus. It does not capture the surplus.

Then the exchange calculates the taker order's fee (if any), and sends tokens to the order taker.

After sending the order taker the "want" tokens, the exchange checks if there it holds any remaining maker asset provided by the taker. If so, it refunds the taker.

The exchange should never hold any USDC or CTFs after each trade.

Last updated