Packages
polymarket_sdk
0.2.0
Higher-level Elixir SDK for Polymarket: Gamma market discovery, Data API, CTF/pUSD helpers, and convenience facades. Depends on polymarket_clob for the low-level CLOB surface.
Current section
Files
Jump to
Current section
Files
lib/polymarket/ctf.ex
defmodule Polymarket.CTF do
@moduledoc """
Gnosis Conditional Tokens Framework (CTF) operations Polymarket
relies on.
Three functions are exposed in two flavors each — a pure calldata
builder and a submit wrapper. The pair always shares the same
function-specific arguments; only the wrapper layers signing and
broadcasting on top.
## Two surfaces
* **Calldata builders** — `redeem_positions_calldata/4`,
`merge_positions_calldata/5`, `split_position_calldata/5`.
Produce the raw function-call bytes. Use when you want the
bytes for inspection, off-line signing, or submission through
your own Polygon RPC client.
* **Submit wrappers** — `redeem_positions/2`, `merge_positions/2`,
`split_position/2`. Build, sign, and broadcast in one call.
Compose the calldata builder + `Polymarket.Tx` +
`Polymarket.RPC`. Caller provides every signing field
(`:nonce`, `:gas_price`, `:gas_limit`, `:chain_id`,
`:private_key`); the wrappers do not auto-fetch from RPC.
## What each function does
* `redeem_positions[_calldata]` — claim payouts for resolved
outcome tokens. Per-outcome amounts are derived from the
reported payout numerators on-chain; the caller only specifies
the partition.
* `merge_positions[_calldata]` — combine outcome tokens back into
collateral. Useful for wallet hygiene or when liquidating an
entire spread.
* `split_position[_calldata]` — mint outcome tokens from
collateral. Rare in normal trading; included for completeness.
CTF function ABI (from the Gnosis spec):
* `redeemPositions(address collateralToken, bytes32 parentCollectionId,
bytes32 conditionId, uint256[] indexSets)`
* `mergePositions(address collateralToken, bytes32 parentCollectionId,
bytes32 conditionId, uint256[] partition, uint256 amount)`
* `splitPosition(address collateralToken, bytes32 parentCollectionId,
bytes32 conditionId, uint256[] partition, uint256 amount)`
## Partition / index sets
For Polymarket binary YES/NO markets, pass `[1, 2]`. For an N-outcome
market, pass `[1, 2, 4, ..., 2^(N-1)]` — each integer is a bitmask
selecting one outcome slot.
## Static-vs-dynamic ABI layout
All three functions have exactly one dynamic parameter (`uint256[]`).
Per the Solidity ABI, every argument occupies one 32-byte word in
the head — static args inline their value, dynamic args inline an
offset pointer to their tail.
`redeemPositions` (`address, bytes32, bytes32, uint256[]`) has
4 head words = `4 * 32 = 128` bytes:
1 address + 2 bytes32 + 1 dynamic-array offset.
`mergePositions` and `splitPosition`
(`address, bytes32, bytes32, uint256[], uint256`) have 5 head words
= `5 * 32 = 160` bytes:
1 address + 2 bytes32 + 1 dynamic-array offset + 1 uint256.
The dynamic-array offset placed in the head equals the head size
(i.e. the byte position of the array tail relative to the start of
the args, post-selector).
Calldata bytes are validated against the canonical ethers.js v6
encoder in `test/fixtures/calldata.json`. The submit-wrapper
composition (calldata → tx → signed bytes) is verified against a
manually-composed reference in
`test/polymarket/submit_test.exs`.
"""
alias Polymarket.ABI
@redeem_signature "redeemPositions(address,bytes32,bytes32,uint256[])"
@merge_signature "mergePositions(address,bytes32,bytes32,uint256[],uint256)"
@split_signature "splitPosition(address,bytes32,bytes32,uint256[],uint256)"
# redeem head: 1 address + 2 bytes32 + 1 dynamic-array offset = 4 words.
@redeem_head_size 4 * 32
# merge/split head: 1 address + 2 bytes32 + 1 dynamic-array offset
# + 1 uint256 = 5 words.
@merge_split_head_size 5 * 32
@doc """
Builds calldata for `redeemPositions(address, bytes32, bytes32, uint256[])`.
`index_sets` is the partition over outcome slots, expressed as
bitmasks. For Polymarket binary YES/NO markets pass `[1, 2]`.
"""
@spec redeem_positions_calldata(
String.t(),
String.t(),
String.t(),
[non_neg_integer()]
) :: binary()
def redeem_positions_calldata(
collateral_token,
parent_collection_id,
condition_id,
index_sets
)
when is_list(index_sets) do
head =
ABI.encode_address(collateral_token) <>
ABI.encode_bytes32(parent_collection_id) <>
ABI.encode_bytes32(condition_id) <>
ABI.encode_uint256(@redeem_head_size)
tail = ABI.encode_uint256_array_tail(index_sets)
ABI.function_selector(@redeem_signature) <> head <> tail
end
@doc """
Builds calldata for `mergePositions(address, bytes32, bytes32,
uint256[], uint256)`.
"""
@spec merge_positions_calldata(
String.t(),
String.t(),
String.t(),
[non_neg_integer()],
non_neg_integer()
) :: binary()
def merge_positions_calldata(
collateral_token,
parent_collection_id,
condition_id,
partition,
amount
)
when is_list(partition) and is_integer(amount) and amount >= 0 do
head =
ABI.encode_address(collateral_token) <>
ABI.encode_bytes32(parent_collection_id) <>
ABI.encode_bytes32(condition_id) <>
ABI.encode_uint256(@merge_split_head_size) <>
ABI.encode_uint256(amount)
tail = ABI.encode_uint256_array_tail(partition)
ABI.function_selector(@merge_signature) <> head <> tail
end
@doc """
Builds calldata for `splitPosition(address, bytes32, bytes32,
uint256[], uint256)`.
"""
@spec split_position_calldata(
String.t(),
String.t(),
String.t(),
[non_neg_integer()],
non_neg_integer()
) :: binary()
def split_position_calldata(
collateral_token,
parent_collection_id,
condition_id,
partition,
amount
)
when is_list(partition) and is_integer(amount) and amount >= 0 do
head =
ABI.encode_address(collateral_token) <>
ABI.encode_bytes32(parent_collection_id) <>
ABI.encode_bytes32(condition_id) <>
ABI.encode_uint256(@merge_split_head_size) <>
ABI.encode_uint256(amount)
tail = ABI.encode_uint256_array_tail(partition)
ABI.function_selector(@split_signature) <> head <> tail
end
# ── Submit wrappers ──
@doc """
Builds, signs, and broadcasts a `redeemPositions(...)` transaction to
the CTF contract at `:contract`. Returns the on-chain transaction
hash on success.
Required `opts`: `:contract`, `:collateral_token`,
`:parent_collection_id`, `:condition_id`, `:index_sets`, plus the
common signing fields `:nonce`, `:gas_price`, `:gas_limit`,
`:chain_id`, `:private_key`. See `redeem_positions_calldata/4` for
argument shapes.
"""
@spec redeem_positions(Polymarket.RPC.t(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def redeem_positions(%Polymarket.RPC{} = rpc, opts) when is_list(opts) do
required = [:contract, :collateral_token, :parent_collection_id, :condition_id, :index_sets]
with :ok <- Polymarket.Submit.require_opts(opts, required) do
calldata =
redeem_positions_calldata(
Keyword.fetch!(opts, :collateral_token),
Keyword.fetch!(opts, :parent_collection_id),
Keyword.fetch!(opts, :condition_id),
Keyword.fetch!(opts, :index_sets)
)
Polymarket.Submit.send_call(rpc, Keyword.fetch!(opts, :contract), calldata, opts)
end
end
@doc """
Builds, signs, and broadcasts a `mergePositions(...)` transaction to
the CTF contract at `:contract`.
Required `opts`: `:contract`, `:collateral_token`,
`:parent_collection_id`, `:condition_id`, `:partition`, `:amount`,
plus `:nonce`, `:gas_price`, `:gas_limit`, `:chain_id`,
`:private_key`.
"""
@spec merge_positions(Polymarket.RPC.t(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def merge_positions(%Polymarket.RPC{} = rpc, opts) when is_list(opts) do
required =
[:contract, :collateral_token, :parent_collection_id, :condition_id, :partition, :amount]
with :ok <- Polymarket.Submit.require_opts(opts, required) do
calldata =
merge_positions_calldata(
Keyword.fetch!(opts, :collateral_token),
Keyword.fetch!(opts, :parent_collection_id),
Keyword.fetch!(opts, :condition_id),
Keyword.fetch!(opts, :partition),
Keyword.fetch!(opts, :amount)
)
Polymarket.Submit.send_call(rpc, Keyword.fetch!(opts, :contract), calldata, opts)
end
end
@doc """
Builds, signs, and broadcasts a `splitPosition(...)` transaction to
the CTF contract at `:contract`.
Required `opts`: same as `merge_positions/2`.
"""
@spec split_position(Polymarket.RPC.t(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def split_position(%Polymarket.RPC{} = rpc, opts) when is_list(opts) do
required =
[:contract, :collateral_token, :parent_collection_id, :condition_id, :partition, :amount]
with :ok <- Polymarket.Submit.require_opts(opts, required) do
calldata =
split_position_calldata(
Keyword.fetch!(opts, :collateral_token),
Keyword.fetch!(opts, :parent_collection_id),
Keyword.fetch!(opts, :condition_id),
Keyword.fetch!(opts, :partition),
Keyword.fetch!(opts, :amount)
)
Polymarket.Submit.send_call(rpc, Keyword.fetch!(opts, :contract), calldata, opts)
end
end
end