All articles
Smart Contracts·December 15, 2025·5 min read

Writing a Solidity CTF Adapter for Polymarket Automated Market Making

Polymarket's Conditional Token Framework uses ERC-1155 outcome shares settled through a UMA oracle — this tutorial shows how to write a Solidity adapter contract that wraps CTF splits and merges so a bot can atomically rebalance Yes/No positions in a single transaction. Includes Hardhat fork tests against Polygon mainnet state.

Polymarket's Conditional Token Framework (CTF) is not a normal AMM — you're trading ERC-1155 outcome shares, splitting and merging USDC collateral on-chain, and settling through a UMA oracle. Writing a Solidity CTF adapter for Polymarket automated market making means bridging that complexity so your off-chain bot can atomically rebalance Yes/No positions in a single transaction instead of firing three separate approvals and praying gas prices stay stable. This article walks through the exact mechanics, the adapter design, and the Hardhat fork test setup we use in production.

The CTF Mechanics You Actually Need to Understand

The CTF contract (0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 on Polygon) holds USDC as collateral and issues two ERC-1155 token IDs per condition: a YES position token and a NO position token. When you splitPosition, you deposit N USDC and receive N YES tokens and N NO tokens. When you mergePositions, you burn equal quantities of YES and NO and get your USDC back. Resolution collapses whichever side to zero and lets winners redeem 1:1.

For a market maker, the workflow is:

  • Buy YES: already in inventory, no issue.
  • Reduce YES, increase NO: the naive path is sell YES on the CLOB and buy NO separately — two CLOB fills, two price impacts, counterparty risk between legs.
  • Rebalance atomically: split more USDC into YES+NO, sell the unwanted leg, or merge a balanced quantity back to USDC.

The adapter exists to make that third path a single call with no intermediate state exposed on-chain.

Adapter Contract Design

The core insight is that USDC → split → sell-unwanted-leg is a pattern the adapter can execute atomically using a callback architecture. Here is the production-relevant interface:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IConditionalTokens {
    function splitPosition(
        address collateralToken,
        bytes32 parentCollectionId,
        bytes32 conditionId,
        uint256[] calldata partition,
        uint256 amount
    ) external;

    function mergePositions(
        address collateralToken,
        bytes32 parentCollectionId,
        bytes32 conditionId,
        uint256[] calldata partition,
        uint256 amount
    ) external;
}

contract CTFAdapter {
    IConditionalTokens public immutable ctf;
    IERC20 public immutable usdc;
    address public immutable clob; // Polymarket CLOB exchange

    bytes32 public immutable conditionId;
    uint256[] private PARTITION = [1, 2]; // YES=1, NO=2 in Polymarket's encoding

    constructor(
        address _ctf,
        address _usdc,
        address _clob,
        bytes32 _conditionId
    ) {
        ctf = IConditionalTokens(_ctf);
        usdc = IERC20(_usdc);
        clob = _clob;
        conditionId = _conditionId;
        usdc.approve(_ctf, type(uint256).max);
    }

    /// @notice Split USDC into YES+NO, sell the unwanted leg via CLOB
    function splitAndSell(
        uint256 usdcAmount,
        bool sellYes,
        uint256 minOut
    ) external {
        usdc.transferFrom(msg.sender, address(this), usdcAmount);
        ctf.splitPosition(address(usdc), bytes32(0), conditionId, PARTITION, usdcAmount);
        // approve CLOB to pull the outcome token and fill
        // ... CLOB fill logic here
    }

    /// @notice Merge equal YES+NO back to USDC
    function mergeToUsdc(uint256 amount) external {
        // pull YES and NO from caller, merge, return USDC
        ctf.mergePositions(address(usdc), bytes32(0), conditionId, PARTITION, amount);
        usdc.transfer(msg.sender, amount);
    }
}

A few non-obvious things: parentCollectionId is bytes32(0) for top-level positions (no nested conditions). The partition must exactly match the CTF's registered outcome slots — Polymarket uses [1, 2] for two-outcome markets. Get this wrong and splitPosition will revert with no useful error message.

The positionId Calculation

Every ERC-1155 token ID is deterministic. The YES token ID for a given condition is:

bytes32 collectionId = keccak256(abi.encodePacked(
    bytes32(0), // parentCollectionId
    conditionId,
    uint256(1)  // indexSet for YES
));
uint256 yesPositionId = uint256(
    keccak256(abi.encodePacked(address(usdc), collectionId))
);

Your bot needs these IDs to query balances and set ERC-1155 approvals on the CLOB. Compute them off-chain from the conditionId (which Polymarket publishes in its market API) and hardcode them per market or derive them dynamically in your adapter's constructor.

Hardhat Fork Tests Against Polygon Mainnet

Testing against a live fork is the only way to catch approval issues, token ID mismatches and CTF state that a local deployment will not replicate. The setup:

// hardhat.config.js
networks: {
  hardhat: {
    forking: {
      url: process.env.POLYGON_RPC,
      blockNumber: 58_000_000 // pin to a block where the market is open
    }
  }
}

In your test file, impersonate a whale holding USDC and a known YES/NO balance so you can run both legs without deploying fresh collateral:

const whale = "0x..."; // real Polygon address with USDC
await network.provider.request({
  method: "hardhat_impersonateAccount",
  params: [whale]
});
const signer = await ethers.getSigner(whale);

// deploy adapter
const Adapter = await ethers.getContractFactory("CTFAdapter", signer);
const adapter = await Adapter.deploy(CTF_ADDR, USDC_ADDR, CLOB_ADDR, CONDITION_ID);

// approve adapter to spend USDC
await usdc.connect(signer).approve(adapter.address, ethers.constants.MaxUint256);

// test split: 100 USDC in, 100 YES + 100 NO out
await adapter.connect(signer).splitAndSell(parseUnits("100", 6), true, parseUnits("97", 6));
const yesBalance = await ctf.balanceOf(adapter.address, YES_POSITION_ID);
expect(yesBalance).to.equal(0); // sold

Pin the block number. Floating forks break nondeterministically when mainnet state changes under the test — a lesson that costs exactly one production outage to learn.

Gas and Atomicity Trade-offs

A combined splitAndSell call on Polygon costs roughly 180,000–220,000 gas depending on CLOB fill complexity. That is three to four times a simple ERC-20 transfer, but it is still under $0.01 at normal Polygon gas prices. The real cost is revert risk: if the CLOB fill fails after the split, you are holding YES+NO with no easy recovery path in the same transaction unless you catch the revert and branch to a merge. We handle this with a try/catch pattern that falls back to merging if the CLOB fill reverts, returning USDC minus gas to the caller instead of leaving stranded position tokens.

The alternative — two separate transactions — exposes you to the Polymarket spread bot pattern where someone sees your YES sell pending in the mempool and front-runs the NO side before your second transaction lands. Atomic execution eliminates that attack surface entirely.

Integration Points for an Off-Chain Bot

The adapter contract is the on-chain half; the off-chain bot (see our smart contract and bot development services) handles:

  • Pricing: computing fair YES/NO probabilities from your model, deciding when the current CLOB spread justifies a split-and-sell.
  • Position tracking: querying ctf.balanceOf for both outcome tokens and net USDC exposed per condition.
  • Delta targeting: deciding how much USDC to split each cycle to hit the target YES/NO inventory ratio.
  • Resolution monitoring: watching UMA oracle events to merge or redeem positions before the window closes.

The adapter call becomes a single ethers.js transaction with encoded calldata — no multi-step approval orchestration in the hot path.


If you want a production-ready CTF adapter, fork-tested and integrated with a live Polymarket market-making strategy, get in touch — we ship these end to end.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Smart Contracts#Polymarket#Solidity#ERC-1155#Market Making#DeFi#Hardhat