Writing a Solidity Adapter for Polymarket CTF Positions
Polymarket's Conditional Token Framework uses ERC-1155 position tokens redeemable after resolution. This tutorial shows how to write a thin Solidity adapter contract that lets your EVM bot batch-buy, split, and redeem outcome tokens in a single transaction.
Polymarket runs on Gnosis's Conditional Token Framework, a battle-tested but non-trivial ERC-1155 standard. If your trading bot interacts with Polymarket via EOA calls, you are paying unnecessary gas on every leg and exposing yourself to partial-fill state that can lock collateral mid-trade. A thin adapter contract solves both problems by collapsing the buy-split-merge loop into a single atomic transaction.
How CTF Positions Actually Work
The CTF splits a collateral token (USDC.e on Polygon) into a set of conditional tokens, one per outcome. Each token is an ERC-1155 balance keyed by a positionId derived from the collateral token address, a condition ID, and an index set. For a binary market you get two position IDs: outcome A and outcome B.
The ConditionalTokens contract (deployed at 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 on Polygon) exposes three calls you care about:
splitPosition(collateral, parentCollection, conditionId, partition, amount)— burns USDC.e and mints the two outcome tokensmergePositions(...)— the inverse; burns outcome tokens and returns USDC.eredeemPositions(collateral, parentCollection, conditionId, indexSets)— post-resolution; burns winning tokens and pays out
The CLOB on Polymarket adds a layer on top: orders trade the ERC-1155 tokens directly, so your fill gives you a raw position balance, not a matched buy/sell in some internal ledger. That design is clean but it means your bot must manage split and merge explicitly.
Why a Single-Transaction Adapter Is Worth the Effort
Without an adapter, a typical buy-then-hedge cycle requires at least four separate transactions: approve USDC.e to the CTF, call splitPosition, approve the outcome token to the exchange, place the order. Each hop is a Polygon gas payment and a point of failure. On high-frequency strategies the round-trip latency to confirm each tx before sending the next is the binding constraint, not signal quality.
The adapter collapses this into one multicall-style entry point. The bot signs a single transaction, the adapter executes the full sequence atomically inside the EVM, and either everything settles or the whole thing reverts. No partial state, no orphaned USDC.e sitting in an approval limbo.
Adapter Contract Structure
The core of the adapter is straightforward. You need three pieces: a reentrancy guard, a reference to the CTF address, and an IERC1155Receiver implementation so the contract can hold position tokens between legs.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IConditionalTokens {
function splitPosition(
address collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint256[] calldata partition,
uint256 amount
) external;
function redeemPositions(
address collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint256[] calldata indexSets
) external;
}
contract CTFAdapter is IERC1155Receiver, ReentrancyGuard {
using SafeERC20 for IERC20;
address public immutable ctf;
address public immutable usdc;
address public immutable owner;
constructor(address _ctf, address _usdc) {
ctf = _ctf;
usdc = _usdc;
owner = msg.sender;
}
function splitAndTransfer(
bytes32 conditionId,
uint256 amount,
address recipient
) external nonReentrant {
require(msg.sender == owner, "not owner");
IERC20(usdc).safeTransferFrom(msg.sender, address(this), amount);
IERC20(usdc).approve(ctf, amount);
uint256[] memory partition = new uint256[](2);
partition[0] = 1; // outcome A index set
partition[1] = 2; // outcome B index set
IConditionalTokens(ctf).splitPosition(
usdc,
bytes32(0), // top-level, no parent
conditionId,
partition,
amount
);
// Transfer both position tokens to recipient (exchange or strategy contract)
// positionId derivation omitted here for brevity — compute off-chain and pass in
}
// Required by IERC1155Receiver
function onERC1155Received(address, address, uint256, uint256, bytes calldata)
external pure returns (bytes4) {
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
external pure returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId;
}
}
The IERC1155Receiver hooks are mandatory — the CTF will revert the splitPosition call if the recipient contract does not implement them. Missing this is the most common reason adapter deployments fail on first test.
Handling Redemption After Resolution
Resolution on Polymarket is two-phase. The oracle reports, then anyone can call redeemPositions for winning balances. Your adapter should expose a permissioned redeem function that accepts the resolved condition and the winning index set, calls the CTF, and sweeps USDC.e back to your treasury address. Do not rely on the bot's EOA to hold position tokens — keeping them inside the adapter lets you batch redemptions across multiple markets in one transaction, which matters when you are running dozens of positions simultaneously.
One important detail: payoutNumerators are set per-outcome by the oracle, and a market can resolve to fractional outcomes (e.g., 0.7/0.3 in a partial-resolution scenario). Build your redemption math to handle this rather than assuming binary 1/0.
Gas Costs and Practical Numbers
On Polygon PoS, a splitPosition costs roughly 90,000–110,000 gas depending on the number of outcomes and ERC-20 approval state. Wrapping this with a transfer and the batch redemption adds another 30–50k. At 100 Gwei (a reasonably busy Polygon block) that is about $0.01–$0.02 per full round-trip. That is cheap enough that the adapter overhead is irrelevant versus the gas you save by eliminating separate approval transactions.
Deploy behind a minimal proxy (ERC-1167 clone) if you are running the adapter per-strategy rather than sharing one instance across all markets. Clones cost around 45k gas to deploy, which you recover in the first split call.
Access Control and Upgrade Patterns
Keep the adapter as minimal as possible. It should not implement any trading logic — that belongs in your off-chain bot or a separate strategy contract. The adapter is purely a transaction batching layer. Lock it down with a single owner check (or a multi-sig if your setup warrants it) and resist the temptation to add an upgrade proxy. The CTF address is immutable on Polymarket's deployment; your adapter can be too.
If you need to rotate the adapter (new strategy, new collateral token), redeploy a fresh clone and update the bot's config. Immutable adapters are auditable adapters.
If you want to skip the integration work and run a fully managed Polymarket strategy, reach out to the TierZero team — we build and operate these systems end to end.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article