PolymarketAdapter¶
The PolymarketAdapter is the concrete implementation of the IMarketAdapter interface for the Polymarket CLOB venue. It satisfies the interface ABI but diverges from the generic state variable model due to Polymarket's Conditional Token Framework (CTF/ERC-1155) architecture.
Interface compatibility
The vault interacts with PolymarketAdapter exclusively through IMarketAdapter. All function selectors are identical. The differences below are internal to the adapter and invisible to the vault.
3a.1 Constructor¶
constructor(
address _vault, // LiquidityVault. Immutable. onlyVault modifier target.
address _asset, // USDC token. Immutable.
address _exchange, // CTF Exchange (CLOB) address. Immutable.
address _ctf, // Conditional Token Framework (ERC-1155) address. Immutable.
bytes32 _conditionId, // Polymarket condition identifier. Immutable.
address _operatorAddr // Operator address authorised to push price and stage orders.
)
Differs from generic spec (§10.3): The generic interface constructor takes (vault, asset, noToken, marketId). The PolymarketAdapter replaces noToken and marketId with exchange, ctf, conditionId, and operatorAddr — required by Polymarket's CTF/ERC-1155 architecture.
NO token ID derivation: Polymarket NO tokens are ERC-1155 positions, not simple ERC-20 tokens. The NO token ID is derived on-chain:
collectionId = ctf.getCollectionId(bytes32(0), conditionId, noIndexSet)
noTokenId = ctf.getPositionId(asset, collectionId)
positionSize() returns IERC1155(ctf).balanceOf(address(this), noTokenId). Polymarket NO tokens are 1e18 precision, matching the spec's unit requirement.
3a.2 Price Feed — Operator Push Model¶
currentPrice() returns a stored value updated by the operator via updateCurrentPrice(uint256 price) — no on-chain price oracle is queried.
Why: Polymarket's CLOB has no on-chain price feed. The current best-bid must be read from the Polymarket API off-chain and pushed on-chain before each keeper run.
Price staleness risk
The operator must push a fresh price in the same block or transaction bundle as the keeper call to minimise staleness risk. If the operator pushes a stale price and the keeper processes withdrawals, the exit curve is computed against an inaccurate market NAV. This is the primary operational risk of this adapter.
3a.3 Buy and Sell — Signed Order Pre-Staging¶
Polymarket uses off-chain order matching. The vault cannot construct or sign a valid CLOB order on-chain. The flow is two-step:
Entry (buyNoShares):
- Operator constructs a signed counterparty order off-chain and calls
setPendingBuyOrder(SignedOrder, expectedPrice)to stage it in the adapter. - Vault calls
buyNoShares(assets)which fills the pre-staged signed order atomically via the CTF Exchange.
Emergency exit (sellNoShares):
- Operator constructs a signed sell order off-chain and calls
setPendingSellOrder(SignedOrder). - Vault calls
sellNoShares(shares)which caps at the actual ERC-1155 balance, fills the pre-staged signed order atomically, and returns(actualSharesSold, usdcReceived)computed via before/after balance diff.
Interface compliance: The staging step is adapter-internal and invisible to the vault. The vault still calls buyNoShares and sellNoShares with the standard IMarketAdapter signatures.
3a.4 Settlement Detection¶
isSettled() checks whether the CTF payoutDenominator for the condition is non-zero — which Polymarket sets when resolution is finalised after the UMA dispute window closes.
This satisfies the spec requirement that isSettled() returns false during any dispute window, because Polymarket does not set the denominator until resolution is complete and final.
The operator calls reportSettlement(uint256 finalPrice) to confirm finality. isSettled() returns the settled boolean set by that call, gated by the on-chain ctf.payoutDenominator(conditionId) > 0 check.
3a.5 Settlement Claim¶
claimSettlement() calls ctf.redeemPositions(asset, bytes32(0), conditionId, [2]) — the NO position index set. The CTF burns the NO ERC-1155 tokens and transfers the pro-rata USDC collateral to the adapter, which forwards it to the vault.
The [2] partition index corresponds to the NO outcome in a binary market (YES = 1, NO = 2).
Slippage Guards¶
| Operation | Max slippage | Enforced in |
|---|---|---|
buyNoShares |
50 bps | Compared against expectedPrice staged by operator |
sellNoShares |
200 bps | Compared against currentPrice() at call time |
Both match the spec requirements (§7.6 and §3.3).
Summary of Differences from Generic Spec¶
| Area | Generic spec (§3, §10.3) | PolymarketAdapter |
|---|---|---|
| Constructor | (vault, asset, noToken, marketId) |
(vault, asset, exchange, ctf, conditionId, operatorAddr) |
currentPrice() |
Queried from venue on-chain | Operator-pushed stored value |
buyNoShares / sellNoShares |
Direct execution on call | Two-step: operator stages order, vault fills |
isSettled() |
Oracle flag | CTF payoutDenominator > 0 + operator reportSettlement |
claimSettlement() |
Simple USDC transfer | ctf.redeemPositions burns ERC-1155, returns USDC |
positionSize() |
ERC-20 balance | ERC-1155 balance at derived noTokenId |