Overview
The CTFExchange contract is a core component of Polymarket's Conditional Token Framework (CTF), designed to facilitate the trading of conditional tokens (e.g., prediction market shares) and collateral. It operates as an order book-based exchange where users can create, fill, and match orders through an operator. Below is a detailed breakdown of its key functionalities:
1. Order Structure
Orders are defined by the
Order struct, which includes:- Maker/Signer: The account providing funds (
maker) and the signer authorizing the order (may differ for smart contract wallets).
- TokenId: The conditional token ID (e.g., "YES" or "NO" shares in a market).
- Side:
BUY(acquire conditional tokens) orSELL(sell conditional tokens).
- Amounts:
makerAmount(amount offered) andtakerAmount(amount requested).
- Fees:
feeRateBps(fee rate in basis points charged to the maker).
- Signature: Supports EOA, Polymarket Proxy, or Gnosis Safe signatures.
2. Core Functions
A. fillOrder
- Called by: Operator only.
- Purpose: Fill a single order (e.g., if the order is a
BUY, the operator sells tokens to the maker).
- Steps:
- Validate Order: Check expiration, signature, fees, and token validity.
- Calculate Fees: Fees are computed based on the order's probability (closer to 50% ⇒ lower fee).
- Transfer Assets:
- Transfer the "taker asset" (e.g., collateral for a
BUYorder) from the operator to the maker, minus fees. - Transfer the "maker asset" (e.g., shares) from the maker to the operator.
- Emit Event: Log the order fill details.
B. matchOrders
- Called by: Operator only.
- Purpose: Match a taker order against multiple maker orders, handling complementary scenarios (e.g., minting or merging shares).
- Steps:
- Validate Taker Order: Check order validity and compute required amounts.
- Transfer Taker Assets: Pull the taker’s offered assets into the exchange contract.
- Fill Maker Orders:
- For each maker order, validate and transfer assets.
- Handle minting (if two
BUYorders are matched) or merging (if twoSELLorders are matched) via the Conditional Token contract. - Settle Taker Order:
- Transfer the received assets to the taker, minus fees.
- Refund any unused assets to the taker.
- Emit Events: Log the match details.
3. Fee Mechanism
- Fees are charged in the received asset (e.g., shares for a
BUY, collateral for aSELL).
- The fee rate depends on the order's implied probability:
- Fees are minimized when the probability is near 50% and increase as it approaches 0% or 100%.
- Fees are collected by the operator implicitly (deducted from the transferred assets).
4. Signature Validation
Orders support three signature types:
- EOA: Signed by an externally-owned account.
- Polymarket Proxy: Signed by an EOA controlling a proxy wallet.
- Gnosis Safe: Signed by an EOA controlling a Gnosis Safe.
The contract ensures the signer is authorized to use the associated maker account.
5. Minting and Merging
- Minting: When two
BUYorders are matched, the contract deposits collateral into the Conditional Token contract to mint new shares.
- Merging: When two
SELLorders are matched, the contract burns shares via the Conditional Token contract to redeem collateral.
This mechanism ensures efficient matching without requiring external liquidity.
6. Order Cancellation
- Makers can cancel orders on-chain using
cancelOrderorcancelOrders.
- Cancellation marks the order as unfillable and emits an event.
7. Key Benefits
- Efficiency: Direct minting/merging reduces external market-making needs.
- Flexibility: Supports both simple fills and complex order matching.
- Fee Optimization: Dynamic fees incentivize balanced markets.
- User Safety: Nonce-based cancellation and signature options (including smart contracts).
The CTFExchange contract enables secure, efficient trading of conditional tokens while abstracting complexity for end-users. Its design aligns with Polymarket's goal of creating decentralized prediction markets with robust on-chain mechanics.
fillOrder
fillOrder can only be called by operator to fill certain order. For example, the order is to buy shares, then operator sells shares to order maker, vice versa.In the
_fillOrder function:- check order is valid and update order status
(_performOrderChecks)
- calculate fee
- transfer taking asset from operator to order maker
- transfer maker asset from order maker to specified
toaddress.
Note as the taking is transferred from operator to order maker, so operator implicitly charges fee.
/// --- polymarketContract/CTFExchange/src/exchange/CTFExchange.sol --- /// @notice Fills an order /// @param order - The order to be filled /// @param fillAmount - The amount to be filled, always in terms of the maker amount function fillOrder(Order memory order, uint256 fillAmount) external nonReentrant onlyOperator notPaused { _fillOrder(order, fillAmount, msg.sender); } /// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Fills an order against the caller /// @param order - The order to be filled /// @param fillAmount - The amount to be filled, always in terms of the maker amount /// @param to - The address to receive assets from filling the order function _fillOrder(Order memory order, uint256 fillAmount, address to) internal { uint256 making = fillAmount; (uint256 taking, bytes32 orderHash) = _performOrderChecks(order, making); uint256 fee = CalculatorHelper.calculateFee( order.feeRateBps, order.side == Side.BUY ? taking : making, order.makerAmount, order.takerAmount, order.side ); (uint256 makerAssetId, uint256 takerAssetId) = _deriveAssetIds(order); // Transfer order proceeds minus fees from msg.sender to order maker _transfer(msg.sender, order.maker, takerAssetId, taking - fee); // Transfer makingAmount from order maker to `to` _transfer(order.maker, to, makerAssetId, making); // NOTE: Fees are "collected" by the Operator implicitly, // since the fee is deducted from the assets paid by the Operator emit OrderFilled(orderHash, order.maker, msg.sender, makerAssetId, takerAssetId, making, taking, fee); }
matchOrders
matchOrders is called by operator to match oen taker order to multiple maker order. Unlike fillOrder function, matchOrders match users’ orders. And it can hanlde mint and merge situation such as two order makers both want to buy shares, one buys yes, the other buys no, so that their intention can both be satisfied by minting shares from CTF contract, vice versa.In
Order struct:
1. maker and signer can differ, for example, if user is using AA wallet, then maker is the AA contract which owns fund and shares, and signer is the AA wallet owner./// --- polymarketContract/CTFExchange/src/exchange/CTFExchange.sol --- /// @notice Matches a taker order against a list of maker orders /// @param takerOrder - The active order to be matched /// @param makerOrders - The array of maker orders to be matched against the active order /// @param takerFillAmount - The amount to fill on the taker order, always in terms of the maker amount /// @param makerFillAmounts - The array of amounts to fill on the maker orders, always in terms of the maker amount function matchOrders( Order memory takerOrder, Order[] memory makerOrders, uint256 takerFillAmount, uint256[] memory makerFillAmounts ) external nonReentrant onlyOperator notPaused { _matchOrders(takerOrder, makerOrders, takerFillAmount, makerFillAmounts); } /// --- polymarketContract/CTFExchange/src/exchange/libraries/OrderStructs.sol --- struct Order { /// @notice Unique salt to ensure entropy uint256 salt; /// @notice Maker of the order, i.e the source of funds for the order address maker; /// @notice Signer of the order address signer; /// @notice Address of the order taker. The zero address is used to indicate a public order address taker; /// @notice Token Id of the CTF ERC1155 asset to be bought or sold /// If BUY, this is the tokenId of the asset to be bought, i.e the makerAssetId /// If SELL, this is the tokenId of the asset to be sold, i.e the takerAssetId uint256 tokenId; /// @notice Maker amount, i.e the maximum amount of tokens to be sold uint256 makerAmount; /// @notice Taker amount, i.e the minimum amount of tokens to be received uint256 takerAmount; /// @notice Timestamp after which the order is expired uint256 expiration; /// @notice Nonce used for onchain cancellations uint256 nonce; /// @notice Fee rate, in basis points, charged to the order maker, charged on proceeds uint256 feeRateBps; /// @notice The side of the order: BUY or SELL Side side; /// @notice Signature type used by the Order: EOA, POLY_PROXY or POLY_GNOSIS_SAFE SignatureType signatureType; /// @notice The order signature bytes signature; }
_matchOrders
- check taker order is valid
- get taker order’s maker asset and taker asset ID
- transfer maker asset of taker order into exchange contract
- fill maker orders
- transfer maker asset of maker order into Exchange contract
- handle mint/merge operations with Conditional Token contract to get taker asset of maker order.
- transfer taker asset of maker order to maker of maker order
/// @notice Fills an order /// @param order - The order to be filled /// @param fillAmount - The amount to be filled, always in terms of the maker amount function fillOrder(Order memory order, uint256 fillAmount) external nonReentrant onlyOperator notPaused { _fillOrder(order, fillAmount, msg.sender); }
- update final taking the taker order receives (because the actual taking can be greator than the taking defined in order, this is to prevent erroneous taking amount set in order to protect order maker)
- transfer taking to taker order maker considering fee
- refund rest maker asset to taker order maker
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Matches orders against each other /// Matches a taker order against a list of maker orders /// @param takerOrder - The active order to be matched /// @param makerOrders - The array of passive orders to be matched against the active order /// @param takerFillAmount - The amount to fill on the taker order, in terms of the maker amount /// @param makerFillAmounts - The array of amounts to fill on the maker orders, in terms of the maker amount function _matchOrders( Order memory takerOrder, Order[] memory makerOrders, uint256 takerFillAmount, uint256[] memory makerFillAmounts ) internal { uint256 making = takerFillAmount; (uint256 taking, bytes32 orderHash) = _performOrderChecks(takerOrder, making); (uint256 makerAssetId, uint256 takerAssetId) = _deriveAssetIds(takerOrder); // Transfer takerOrder making amount from taker order to the Exchange _transfer(takerOrder.maker, address(this), makerAssetId, making); // Fill the maker orders _fillMakerOrders(takerOrder, makerOrders, makerFillAmounts); taking = _updateTakingWithSurplus(taking, takerAssetId); uint256 fee = CalculatorHelper.calculateFee( takerOrder.feeRateBps, takerOrder.side == Side.BUY ? taking : making, making, taking, takerOrder.side ); // Execute transfers // Transfer order proceeds post fees from the Exchange to the taker order maker _transfer(address(this), takerOrder.maker, takerAssetId, taking - fee); // Charge the fee to taker order maker, explicitly transferring the fee from the Exchange to the Operator _chargeFee(address(this), msg.sender, takerAssetId, fee); // Refund any leftover tokens pulled from the taker to the taker order uint256 refund = _getBalance(makerAssetId); if (refund > 0) _transfer(address(this), takerOrder.maker, makerAssetId, refund); emit OrderFilled( orderHash, takerOrder.maker, address(this), makerAssetId, takerAssetId, making, taking, fee ); emit OrdersMatched(orderHash, takerOrder.maker, makerAssetId, takerAssetId, making, taking); }
_performOrderChecks
_performOrderChecks checks and updates order status.- use
_validateTakerto check whether taker of the order is valid, either the order is permissionless ormsg.senderis the specified order taker.
- use
hashOrderto calculate order hash for signature verification
- use
calculateTakingAmountto calculate the taking amoung of taker of based on making amount.
- use
_updateOrderStatusto update order status (the remaining maker amount, and whether the order is fully filled)
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Performs common order computations and validation /// 1) Validates the order taker /// 2) Computes the order hash /// 3) Validates the order /// 4) Computes taking amount /// 5) Updates the order status in storage /// @param order - The order being prepared /// @param making - The amount of the order being filled, in terms of maker amount function _performOrderChecks(Order memory order, uint256 making) internal returns (uint256 takingAmount, bytes32 orderHash) { _validateTaker(order.taker); orderHash = hashOrder(order); // Validate order _validateOrder(orderHash, order); // Calculate taking amount takingAmount = CalculatorHelper.calculateTakingAmount(making, order.makerAmount, order.takerAmount); // Update the order status in storage _updateOrderStatus(orderHash, order, making); } /// --- polymarketContract/CTFExchange/src/exchange/libraries/CalculatorHelper.sol --- function calculateTakingAmount(uint256 makingAmount, uint256 makerAmount, uint256 takerAmount) internal pure returns (uint256) { if (makerAmount == 0) return 0; return makingAmount * takerAmount / makerAmount; } /// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- function _updateOrderStatus(bytes32 orderHash, Order memory order, uint256 makingAmount) internal returns (uint256 remaining) { OrderStatus storage status = orderStatus[orderHash]; // Fetch remaining amount from storage remaining = status.remaining; // Update remaining if the order is new/has not been filled remaining = remaining == 0 ? order.makerAmount : remaining; // Throw if the makingAmount(amount to be filled) is greater than the amount available if (makingAmount > remaining) revert MakingGtRemaining(); // Update remaining using the makingAmount remaining = remaining - makingAmount; // If order is completely filled, update isFilledOrCancelled in storage if (remaining == 0) status.isFilledOrCancelled = true; // Update remaining in storage status.remaining = remaining; }
_validateTaker
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- function _validateTaker(address taker) internal view { if (taker != address(0) && taker != msg.sender) revert NotTaker(); }
hashOrder
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Hashing.sol --- /// @notice Computes the hash for an order /// @param order - The order to be hashed function hashOrder(Order memory order) public view override returns (bytes32) { return _hashTypedDataV4( keccak256( abi.encode( ORDER_TYPEHASH, order.salt, order.maker, order.signer, order.taker, order.tokenId, order.makerAmount, order.takerAmount, order.expiration, order.nonce, order.feeRateBps, order.side, order.signatureType ) ) ); }
_validateOrder
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- function _validateOrder(bytes32 orderHash, Order memory order) internal view { // Validate order expiration if (order.expiration > 0 && order.expiration < block.timestamp) revert OrderExpired(); // Validate signature validateOrderSignature(orderHash, order); // Validate fee if (order.feeRateBps > getMaxFeeRate()) revert FeeTooHigh(); // Validate the token to be traded validateTokenId(order.tokenId); // Validate that the order can be filled if (orderStatus[orderHash].isFilledOrCancelled) revert OrderFilledOrCancelled(); // Validate nonce if (!isValidNonce(order.maker, order.nonce)) revert InvalidNonce(); } /// --- polymarketContract/CTFExchange/src/exchange/mixins/Fees.sol --- abstract contract Fees is IFees { /// @notice Maximum fee rate that can be signed into an Order uint256 internal constant MAX_FEE_RATE_BIPS = 1000; // 1000 bips or 10% /// @notice Returns the maximum fee rate for an order function getMaxFeeRate() public pure override returns (uint256) { return MAX_FEE_RATE_BIPS; } } /// --- polymarketContract/CTFExchange/src/exchange/mixins/Registry.sol --- /// @notice Validates that a tokenId is registered /// @param tokenId - The tokenId function validateTokenId(uint256 tokenId) public view override { if (registry[tokenId].complement == 0) revert InvalidTokenId(); } /// --- polymarketContract/CTFExchange/src/exchange/mixins/NonceManager.sol --- mapping(address => uint256) public nonces; function isValidNonce(address usr, uint256 nonce) public view override returns (bool) { return nonces[usr] == nonce; }
validateOrderSignature
validateOrderSignature has three modes:- EOA
- PolySafe
- PolyProxy
In EOA mode, order maker and signer should be same (the EOA account), and it verifies the signer has signed the order.
In PolySafe or PolyProxy mode, maker is AA wallet, and it requires the AA wallet owner (
signer) to sign the order. As each EOA corresponds to an AA wallet, signature from the EOA represents the authorization on the AA wallet usage. (It uses create2 opcode, and uses EOA address as salt to deploy AA wallet, so that each EOA address corresponds to only one AA wallet)./// --- polymarketContract/CTFExchange/src/exchange/mixins/Signatures.sol --- /// @notice Validates the signature of an order /// @param orderHash - The hash of the order /// @param order - The order function validateOrderSignature(bytes32 orderHash, Order memory order) public view override { if (!isValidSignature(order.signer, order.maker, orderHash, order.signature, order.signatureType)) { revert InvalidSignature(); } } /// @notice Verifies a signature for signed Order structs /// @param signer - Address of the signer /// @param associated - Address associated with the signer. /// For signature type EOA, this MUST be the same as the signer address. /// For signature types POLY_PROXY and POLY_GNOSIS_SAFE, this is the address of the proxy or the safe /// @param structHash - The hash of the struct being verified /// @param signature - The signature to be verified /// @param signatureType - The signature type to be verified function isValidSignature( address signer, address associated, bytes32 structHash, bytes memory signature, SignatureType signatureType ) internal view returns (bool) { if (signatureType == SignatureType.EOA) { return verifyEOASignature(signer, associated, structHash, signature); } else if (signatureType == SignatureType.POLY_GNOSIS_SAFE) { return verifyPolySafeSignature(signer, associated, structHash, signature); } else { // POLY_PROXY return verifyPolyProxySignature(signer, associated, structHash, signature); } } /// --- polymarketContract/CTFExchange/src/exchange/libraries/OrderStructs.sol --- enum SignatureType // 0: ECDSA EIP712 signatures signed by EOAs { EOA, // 1: EIP712 signatures signed by EOAs that own Polymarket Proxy wallets POLY_PROXY, // 2: EIP712 signatures signed by EOAs that own Polymarket Gnosis safes POLY_GNOSIS_SAFE }
verify EOA signature
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Signatures.sol --- /// @notice Verifies an EOA ECDSA signature /// Verifies that: /// 1) the signature is valid /// 2) the signer and maker are the same /// @param signer - The address of the signer /// @param maker - The address of the maker /// @param structHash - The hash of the struct being verified /// @param signature - The signature to be verified function verifyEOASignature(address signer, address maker, bytes32 structHash, bytes memory signature) internal pure returns (bool) { return (signer == maker) && verifyECDSASignature(signer, structHash, signature); } /// @notice Verifies an ECDSA signature /// @dev Reverts if the signature length is invalid or the recovered signer is the zero address /// @param signer - Address of the signer /// @param structHash - The hash of the struct being verified /// @param signature - The signature to be verified function verifyECDSASignature(address signer, bytes32 structHash, bytes memory signature) internal pure returns (bool) { return ECDSA.recover(structHash, signature) == signer; }
verify PolySafe signature
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Signatures.sol --- /// @notice Verifies a signature signed by a Polymarket Gnosis safe // Verifies that: // 1) the ECDSA signature is valid // 2) the Safe is owned by the signer /// @param signer - Address of the signer /// @param safeAddress - Address of the safe /// @param hash - Hash of the struct being verified /// @param signature - Signature to be verified function verifyPolySafeSignature(address signer, address safeAddress, bytes32 hash, bytes memory signature) internal view returns (bool) { return verifyECDSASignature(signer, hash, signature) && getSafeAddress(signer) == safeAddress; } /// --- polymarketContract/CTFExchange/src/exchange/mixins/PolyFactoryHelper.sol --- /// @notice Gets the Polymarket Gnosis Safe address for an address /// @param _addr - The address that owns the proxy wallet function getSafeAddress(address _addr) public view returns (address) { return PolySafeLib.getSafeAddress(_addr, getSafeFactoryImplementation(), safeFactory); } /// @notice Gets the Safe factory implementation address function getSafeFactoryImplementation() public view returns (address) { return IPolySafeFactory(safeFactory).masterCopy(); } /// --- polymarketContract/CTFExchange/src/exchange/libraries/PolySafeLib.sol --- /// @notice Gets the Polymarket Gnosis safe address for a signer /// @param signer - Address of the signer /// @param deployer - Address of the deployer contract function getSafeAddress(address signer, address implementation, address deployer) internal pure returns (address safe) { bytes32 bytecodeHash = keccak256(getContractBytecode(implementation)); bytes32 salt = keccak256(abi.encode(signer)); safe = _computeCreate2Address(deployer, bytecodeHash, salt); }
verify PolyProxy signature
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Signatures.sol --- /// @notice Verifies a signature signed by a Polymarket proxy wallet // Verifies that: // 1) the ECDSA signature is valid // 2) the Proxy wallet is owned by the signer /// @param signer - Address of the signer /// @param proxyWallet - Address of the poly proxy wallet /// @param structHash - Hash of the struct being verified /// @param signature - Signature to be verified function verifyPolyProxySignature(address signer, address proxyWallet, bytes32 structHash, bytes memory signature) internal view returns (bool) { return verifyECDSASignature(signer, structHash, signature) && getPolyProxyWalletAddress(signer) == proxyWallet; } /// --- polymarketContract/CTFExchange/src/exchange/mixins/PolyFactoryHelper.sol --- /// @notice Gets the Polymarket proxy wallet address for an address /// @param _addr - The address that owns the proxy wallet function getPolyProxyWalletAddress(address _addr) public view returns (address) { return PolyProxyLib.getProxyWalletAddress(_addr, getPolyProxyFactoryImplementation(), proxyFactory); } /// @notice Gets the Polymarket Proxy factory implementation address function getPolyProxyFactoryImplementation() public view returns (address) { return IPolyProxyFactory(proxyFactory).getImplementation(); } /// --- polymarketContract/CTFExchange/src/exchange/libraries/PolyProxyLib.sol --- /// @notice Gets the polymarket proxy address for a signer /// @param signer - Address of the signer function getProxyWalletAddress(address signer, address implementation, address deployer) internal pure returns (address proxyWallet) { return _computeCreate2Address(deployer, implementation, keccak256(abi.encodePacked(signer))); }
_deriveAssetIds
Buy order means buying share.
Sell order means selling share.
Zero represents token used to buy share.
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- function _deriveAssetIds(Order memory order) internal pure returns (uint256 makerAssetId, uint256 takerAssetId) { if (order.side == Side.BUY) return (0, order.tokenId); return (order.tokenId, 0); }
_transfer
_transfer function implement logic to transfer token./// --- polymarketContract/CTFExchange/src/exchange/mixins/AssetOperations.sol --- function _transfer(address from, address to, uint256 id, uint256 value) internal override { if (id == 0) return _transferCollateral(from, to, value); return _transferCTF(from, to, id, value); } function _transferCollateral(address from, address to, uint256 value) internal { address token = getCollateral(); if (from == address(this)) TransferHelper._transferERC20(token, to, value); else TransferHelper._transferFromERC20(token, from, to, value); } function _transferCTF(address from, address to, uint256 id, uint256 value) internal { TransferHelper._transferFromERC1155(getCtf(), from, to, id, value); } /// --- polymarketContract/CTFExchange/src/exchange/mixins/AssetOperations.sol --- address internal immutable collateral; address internal immutable ctf; function getCollateral() public view override returns (address) { return collateral; } function getCtf() public view override returns (address) { return ctf; }
_fillMakerOrders
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- function _fillMakerOrders(Order memory takerOrder, Order[] memory makerOrders, uint256[] memory makerFillAmounts) internal { uint256 length = makerOrders.length; uint256 i = 0; for (; i < length;) { _fillMakerOrder(takerOrder, makerOrders[i], makerFillAmounts[i]); unchecked { ++i; } } }
_fillMakerOrder
If both orders are buy order, they can be merged to ctf mint operation. (Deposit collateral to get shares)
If both orders are sell orders, they can be merged to ctf merge operation. (Burn share and get collateral)
If one order is buy and the other is sell, then they are counter party following orderbook exchange mechanism.
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Fills a Maker order /// @param takerOrder - The taker order /// @param makerOrder - The maker order /// @param fillAmount - The fill amount function _fillMakerOrder(Order memory takerOrder, Order memory makerOrder, uint256 fillAmount) internal { MatchType matchType = _deriveMatchType(takerOrder, makerOrder); // Ensure taker order and maker order match _validateTakerAndMaker(takerOrder, makerOrder, matchType); uint256 making = fillAmount; (uint256 taking, bytes32 orderHash) = _performOrderChecks(makerOrder, making); uint256 fee = CalculatorHelper.calculateFee( makerOrder.feeRateBps, makerOrder.side == Side.BUY ? taking : making, makerOrder.makerAmount, makerOrder.takerAmount, makerOrder.side ); (uint256 makerAssetId, uint256 takerAssetId) = _deriveAssetIds(makerOrder); _fillFacingExchange(making, taking, makerOrder.maker, makerAssetId, takerAssetId, matchType, fee); emit OrderFilled( orderHash, makerOrder.maker, takerOrder.maker, makerAssetId, takerAssetId, making, taking, fee ); } function _deriveMatchType(Order memory takerOrder, Order memory makerOrder) internal pure returns (MatchType) { if (takerOrder.side == Side.BUY && makerOrder.side == Side.BUY) return MatchType.MINT; if (takerOrder.side == Side.SELL && makerOrder.side == Side.SELL) return MatchType.MERGE; return MatchType.COMPLEMENTARY; }
_validateTakerAndMaker
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Ensures the taker and maker orders can be matched against each other /// @param takerOrder - The taker order /// @param makerOrder - The maker order function _validateTakerAndMaker(Order memory takerOrder, Order memory makerOrder, MatchType matchType) internal view { if (!CalculatorHelper.isCrossing(takerOrder, makerOrder)) revert NotCrossing(); // Ensure orders match if (matchType == MatchType.COMPLEMENTARY) { if (takerOrder.tokenId != makerOrder.tokenId) revert MismatchedTokenIds(); } else { // both bids or both asks validateComplement(takerOrder.tokenId, makerOrder.tokenId); } } /// --- polymarketContract/CTFExchange/src/exchange/libraries/CalculatorHelper.sol --- function isCrossing(Order memory a, Order memory b) internal pure returns (bool) { if (a.takerAmount == 0 || b.takerAmount == 0) return true; return _isCrossing(calculatePrice(a), calculatePrice(b), a.side, b.side); } uint256 internal constant ONE = 10 ** 18; function _isCrossing(uint256 priceA, uint256 priceB, Side sideA, Side sideB) internal pure returns (bool) { if (sideA == Side.BUY) { if (sideB == Side.BUY) { // if a and b are bids return priceA + priceB >= ONE; } // if a is bid and b is ask return priceA >= priceB; } if (sideB == Side.BUY) { // if a is ask and b is bid return priceB >= priceA; } // if a and b are asks return priceA + priceB <= ONE; } function calculatePrice(Order memory order) internal pure returns (uint256) { return _calculatePrice(order.makerAmount, order.takerAmount, order.side); } // for buy order, makerAmount is token, function _calculatePrice(uint256 makerAmount, uint256 takerAmount, Side side) internal pure returns (uint256) { if (side == Side.BUY) return takerAmount != 0 ? makerAmount * ONE / takerAmount : 0; return makerAmount != 0 ? takerAmount * ONE / makerAmount : 0; }
validateComplement
validateComplement checks two tokens are complementrary which means one is yes and the other is no.registry records token’s corresponding conditionId and complement token id. This record can only be registered by admin of CTFExchange contract. It needs manual operation as in the ConditionalToken contract, the token id is calculated using hash algorithm, so the id value is random./// --- polymarketContract/CTFExchange/src/exchange/mixins/Registry.sol --- /// @notice Validates that a tokenId is registered /// @param tokenId - The tokenId struct OutcomeToken { uint256 complement; bytes32 conditionId; } mapping(uint256 => OutcomeToken) public registry; function validateTokenId(uint256 tokenId) public view override { if (registry[tokenId].complement == 0) revert InvalidTokenId(); } /// @notice Gets the complement of a tokenId /// @param token - The token function getComplement( uint256 token ) public view override returns (uint256) { validateTokenId(token); return registry[token].complement; } /// @notice Validates the complement of a tokenId /// @param token - The tokenId /// @param complement - The complement to be validated function validateComplement( uint256 token, uint256 complement ) public view override { if (getComplement(token) != complement) revert InvalidComplement(); } /// @notice Gets the conditionId from a tokenId /// @param token - The token function getConditionId( uint256 token ) public view override returns (bytes32) { return registry[token].conditionId; }
registerToken
/// --- polymarketContract/CTFExchange/src/exchange/CTFExchange.sol --- /// @notice Registers a tokenId, its complement and its conditionId for trading on the Exchange /// @param token - The tokenId being registered /// @param complement - The complement of the tokenId /// @param conditionId - The CTF conditionId function registerToken(uint256 token, uint256 complement, bytes32 conditionId) external onlyAdmin { _registerToken(token, complement, conditionId); }
calculateFee
Based on
calculateFee function, closer the probability to 0.5, lower the fee.Fee unit is the receiving token of order.
- For share buyer, fee unit is share.
- For share seller, fee unit if quote token.
/// --- polymarketContract/CTFExchange/src/exchange/libraries/CalculatorHelper.sol --- /// @notice Calculates the fee for an order /// @dev Fees are calculated based on amount of outcome tokens and the order's feeRate /// @param feeRateBps - Fee rate, in basis points /// @param outcomeTokens - The number of outcome tokens /// @param makerAmount - The maker amount of the order /// @param takerAmount - The taker amount of the order /// @param side - The side of the order function calculateFee( uint256 feeRateBps, uint256 outcomeTokens, uint256 makerAmount, uint256 takerAmount, Side side ) internal pure returns (uint256 fee) { if (feeRateBps > 0) { uint256 price = _calculatePrice(makerAmount, takerAmount, side); if (price > 0 && price <= ONE) { if (side == Side.BUY) { // Fee charged on Token Proceeds: // baseRate * min(price, 1-price) * (outcomeTokens/price) fee = (feeRateBps * min(price, ONE - price) * outcomeTokens) / (price * BPS_DIVISOR); } else { // Fee charged on Collateral proceeds: // baseRate * min(price, 1-price) * outcomeTokens fee = feeRateBps * min(price, ONE - price) * outcomeTokens / (BPS_DIVISOR * ONE); } } } } function _calculatePrice(uint256 makerAmount, uint256 takerAmount, Side side) internal pure returns (uint256) { if (side == Side.BUY) return takerAmount != 0 ? makerAmount * ONE / takerAmount : 0; return makerAmount != 0 ? takerAmount * ONE / makerAmount : 0; }
_fillFacingExchange
_fillFacingExchange fill orders:- transfer maker order’s maker asset to itself
- use
_executeMatchCallto handle mint or merge situations to get enough taking asset for both maker and taker orders.
- transfer taking deducted by fee to maker order
- transfer fee to operator.
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Fills a maker order using the Exchange as the counterparty /// @param makingAmount - Amount to be filled in terms of maker amount /// @param takingAmount - Amount to be filled in terms of taker amount /// @param maker - The order maker /// @param makerAssetId - The Token Id of the Asset to be sold /// @param takerAssetId - The Token Id of the Asset to be received /// @param matchType - The match type /// @param fee - The fee charged to the Order maker function _fillFacingExchange( uint256 makingAmount, uint256 takingAmount, address maker, uint256 makerAssetId, uint256 takerAssetId, MatchType matchType, uint256 fee ) internal { // Transfer makingAmount tokens from order maker to Exchange _transfer(maker, address(this), makerAssetId, makingAmount); // Executes a match call based on match type _executeMatchCall(makingAmount, takingAmount, makerAssetId, takerAssetId, matchType); // Ensure match action generated enough tokens to fill the order if (_getBalance(takerAssetId) < takingAmount) revert TooLittleTokensReceived(); // Transfer order proceeds minus fees from the Exchange to the order maker _transfer(address(this), maker, takerAssetId, takingAmount - fee); // Transfer fees from Exchange to the Operator _chargeFee(address(this), msg.sender, takerAssetId, fee); } function _chargeFee(address payer, address receiver, uint256 tokenId, uint256 fee) internal { // Charge fee to the payer if any if (fee > 0) { _transfer(payer, receiver, tokenId, fee); emit FeeCharged(receiver, tokenId, fee); } } /// --- polymarketContract/CTFExchange/src/exchange/mixins/AssetOperations.sol --- function _getBalance(uint256 tokenId) internal override returns (uint256) { if (tokenId == 0) return IERC20(getCollateral()).balanceOf(address(this)); return IERC1155(getCtf()).balanceOf(address(this), tokenId); }
_executeMatchCall
_executeMatchCall is used to handle mint and merge situations to interact with CTF contract to get enough taking for both maker and taker orders.- When both orders purchases shares,
_executeMatchCallcalls CTF contract to deposit collateral and mint shares.
- When both orders sells shares,
_executeMatchCallcalls CTF contract to merge positions and get collateral back.
/// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Executes a CTF call to match orders by minting new Outcome tokens /// or merging Outcome tokens into collateral. /// @param makingAmount - Amount to be filled in terms of maker amount /// @param takingAmount - Amount to be filled in terms of taker amount /// @param makerAssetId - The Token Id of the Asset to be sold /// @param takerAssetId - The Token Id of the Asset to be received /// @param matchType - The match type function _executeMatchCall( uint256 makingAmount, uint256 takingAmount, uint256 makerAssetId, uint256 takerAssetId, MatchType matchType ) internal { if (matchType == MatchType.COMPLEMENTARY) { // Indicates a buy vs sell order // no match action needed return; } if (matchType == MatchType.MINT) { // Indicates matching 2 buy orders // Mint new Outcome tokens using Exchange collateral balance and fill buys return _mint(getConditionId(takerAssetId), takingAmount); } if (matchType == MatchType.MERGE) { // Indicates matching 2 sell orders // Merge the Exchange Outcome token balance into collateral and fill sells return _merge(getConditionId(makerAssetId), makingAmount); } } /// --- polymarketContract/CTFExchange/src/exchange/mixins/AssetOperations.sol --- bytes32 public constant parentCollectionId = bytes32(0); function _mint(bytes32 conditionId, uint256 amount) internal override { uint256[] memory partition = new uint256[](2); partition[0] = 1; partition[1] = 2; IConditionalTokens(getCtf()).splitPosition( IERC20(getCollateral()), parentCollectionId, conditionId, partition, amount ); } function _merge(bytes32 conditionId, uint256 amount) internal override { uint256[] memory partition = new uint256[](2); partition[0] = 1; partition[1] = 2; IConditionalTokens(getCtf()).mergePositions( IERC20(getCollateral()), parentCollectionId, conditionId, partition, amount ); }
_updateTakingWithSurplus
there is posibility that the taking got from filling of maker orders is bigger than the taker order requires, in that case, update the taking of taker order.
For example, taker order tries to pay 2 USDC to get 1 YES share. And maker order tries to pay 1.5 USDC to get 2 No. Function
_fillFacingExchange deposit 2 USDC to get 2 YES and 2 NO shares. Then update the taking of taker order to be 2. This mechanism is used to protect taker order in case of erroneous taking value setting: all taking got from filling maker orders is transferred to taker order’s maker.
function _updateTakingWithSurplus(uint256 minimumAmount, uint256 tokenId) internal returns (uint256) { uint256 actualAmount = _getBalance(tokenId); if (actualAmount < minimumAmount) revert TooLittleTokensReceived(); return actualAmount; }
Cancel Order
cancelOrder or cancelOrders functions can be called by order maker to cancel orders on-chain./// --- polymarketContract/CTFExchange/src/exchange/mixins/Trading.sol --- /// @notice Mapping of orders to their current status mapping(bytes32 => OrderStatus) public orderStatus; struct OrderStatus { bool isFilledOrCancelled; uint256 remaining; } /// @notice Cancels an order /// An order can only be cancelled by its maker, the address which holds funds for the order /// @notice order - The order to be cancelled function cancelOrder(Order memory order) external { _cancelOrder(order); } /// @notice Cancels a set of orders /// @notice orders - The set of orders to be cancelled function cancelOrders(Order[] memory orders) external { uint256 length = orders.length; uint256 i = 0; for (; i < length;) { _cancelOrder(orders[ i]); unchecked { ++i; } } } function _cancelOrder(Order memory order) internal { if (order.maker != msg.sender) revert NotOwner(); bytes32 orderHash = hashOrder(order); OrderStatus storage status = orderStatus[orderHash]; if (status.isFilledOrCancelled) revert OrderFilledOrCancelled(); status.isFilledOrCancelled = true; emit OrderCancelled(orderHash); }