| eip | title | description | author | status | type | category | created | requires |
|---|---|---|---|---|---|---|---|---|
9999 |
Conditional Tokens |
A standard for conditional tokens that split, merge and are redeemable based on oracle reported outcomes |
shafu (@shafu0x), Behzad (@bitnician), Sarvad (@serverConnectd), Ynyesto (@ynyesto), lajarre (@lajarre) |
Draft |
Standards Track |
ERC |
2025-12-16 |
1155 |
- Abstract
- Motivation
- Adoption and Usage
- Specification
- Example Lifecycle
- Security Considerations
- Copyright
This ERC extends ERC-1155 with conditional tokens that allow participants to create and settle positions on future outcomes.
It introduces three core operations. Splitting collateral into outcome positions, merging positions back into collateral and redeeming positions after oracle resolution.
Prediction markets have demonstrated product market fit through platforms like Polymarket. The Gnosis Conditional Tokens framework from 2019 pioneered the core primitives of splitting, merging, and redeeming positions based on oracle outcomes. But there is no formal ERC standard, limiting interoperability.
To enable a thriving ecosystem of prediction markets we need a standard interface. This ERC addresses this through three core operations:
- Condition Preparation: Registers a condition with an oracle, question identifier and outcome count.
- Position Splitting & Merging: Converts collateral into outcome tokens (split) or recombines them (merge).
- Redemptions: Token holders can claim collateral proportional to the reported payout weights after oracle resolution.
This ERC formalizes patterns that the prediction market industry has battle-tested for years. Providing one interface will accelerate adoption across chains and applications.
This section is non-normative.
- Polymarket: Prediction markets using CTF on Polygon
- Seer: Prediction and futarchy markets using CTF (Gnosis and Ethereum)
- Forkast: Sports/gaming prediction markets using CTF-style conditional tokens (Arbitrum)
- Omen: Early CTF + Reality.eth prediction markets (legacy frontend; onchain markets persist)
- Predict Fun: Prediction markets using conditional token contracts (BNB Chain and Blast)
- OPINION: Prediction exchange using a CTF-derived conditional tokens design (BNB Chain)
Nested positions (via parentCollectionId) allow conditioning a position on
multiple conditions. This is used for (i) decision markets / futarchy, where
downstream markets are only meaningful under a particular decision branch, and
(ii) outcome refinement, where an existing outcome set is further split by
introducing a child condition under that set (instead of mutating the parent
condition).
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Initialize a new condition with a fixed number of outcomes. The function generates a conditionId which is the keccak256(abi.encodePacked(oracle, questionId, outcomeSlotCount)) and initializes a payout vector associated to the conditionId
oracle: Account used to resolve a condition by reporting its result by callingreportPayouts. TheconditionIdis bound to the oracle address, so only that oracle can resolve the condition.questionId: Identifier for the question to be answered byoracleoutcomeSlotCount: Number of outcomes for a condition. MUST BE> 1and<= 256
function prepareCondition(address oracle, bytes32 questionId, uint outcomeSlotCount) externalOracle resolves a condition by calling this function and reports payouts for each outcome
questionId: Identifier for the question to be answered byoraclepayouts: Oracle reported payout numerators per outcome slot. MUST satisfypayouts.length == outcomeSlotCountandpayoutDenominator = Σ payouts[i] > 0. The payout fraction for outcome slotiispayouts[i] / payoutDenominator.
NOTE:
msg.sender is enforced as the oracle, because conditionId is derived from (msg.sender, questionId, payouts.length).
function reportPayouts(bytes32 questionId, uint[] calldata payouts) externalConvert one parent stake into multiple child outcome positions defined by partition. If parentCollectionId == bytes32(0) and indexSetUnion == fullIndexSet, transfers amount collateral from the message sender; otherwise, burns amount of the position being split. In both cases, mints amount of each child position defined by partition.
collateralToken: The address of the position's backing collateral tokenparentCollectionId: Outcome collection ID common to the position being split and the split target positions, orbytes32(0)if there's no parent outcome collection.conditionId: Condition being split on.partition: Array of disjoint index sets defining a non-trivial partition of anindexSetUnion, wherefullIndexSet = (1 << outcomeSlotCount) - 1andindexSetUnion = partition[0] | partition[1] | .... Each element MUST be> 0and< fullIndexSet, and elements MUST be pairwise disjoint.indexSetUnionMAY be a strict subset offullIndexSet.- Example (full): outcomeSlotCount=2,
A = 0b01,B = 0b10, partition[A, B]hasindexSetUnion = 0b11 (= fullIndexSet) - Example (partial): outcomeSlotCount=3,
B = 0b010,C = 0b100, partition[B, C]hasindexSetUnion = 0b110 (!= fullIndexSet)
- Example (full): outcomeSlotCount=2,
amount: Amount of collateral (only ifparentCollectionId == bytes32(0)andindexSetUnion == fullIndexSet) or position tokens to convert into the partitioned positions.
NOTE
bytes32(0) means “no parent outcome collection”. When indexSetUnion != fullIndexSet, the position being
split is the subset position for indexSetUnion (not collateral), even if parentCollectionId == bytes32(0).
A parent outcome collection represents a position already conditioned on prior outcomes, while a child outcome collection represents an additional condition on top of it.
E.g. Assume to condition statements C1 and C2 where C1 is the parent condition of C2 where:
- C1 is “ETH > $3k?”
- C2 is “ETH > $4k?”
The outcomes of C1 are prior outcomes for C2, because C2 is only evaluated within the branch where C1 is valid.
function splitPosition(
IERC20 collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint[] calldata partition,
uint amount
) externalThe inverse of splitPosition: burn multiple child positions to recreate a parent or subset position, or get back collateral.
collateralToken: The address of the position's backing collateral tokenparentCollectionId: Outcome collection ID common to the positions being merged and the merge result position, orbytes32(0)if there's no parent outcome collection.conditionId: Condition being split on.partition: Array of disjoint index sets defining a non-trivial partition of the outcome slots.amount: Burns amount of each child position defined by partition
NOTE
Let indexSetUnion = partition[0] | partition[1] | ... and fullIndexSet = (1 << outcomeSlotCount) - 1.
- If
indexSetUnion == fullIndexSet, either collateral is sent back to the caller (ifparentCollectionId == bytes32(0)) oramountof the parent position token is minted. - If
indexSetUnion != fullIndexSet,amountof the merged subset position token forindexSetUnionis minted (even ifparentCollectionId == bytes32(0)).
function mergePositions(
IERC20 collateralToken,
bytes32 parentCollectionId,
bytes32 conditionId,
uint[] calldata partition,
uint amount
) externalAfter a condition is resolved, redeem outcome position tokens for their payout share.
collateralToken: The address of the position's backing collateral tokenparentCollectionId: Eitherbytes32(0)for direct redemption for collateral or identifier of the parent collectionId for nested redemptionconditionId: resolved conditionindexSets: List of outcome collections (bitmasks) whose positions the caller wants to redeem.
FLOW
For each indexSet, computes the caller’s balance of the corresponding positionId, burns it, and adds
payout += stake * payoutNumerator(indexSet) / payoutDenominator. payoutNumerator(indexSet) is defined as
the sum of the per-outcome payouts[i] for which indexSet has the i-th bit set. Finally, transfers
collateral payout to the caller if (parentCollectionId == bytes32(0)) or mints the parent position token if
nested.
function redeemPositions(IERC20 collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint[] calldata indexSets) externalReturns outcome slot count of a conditionId
conditionId: ID of the condition
function getOutcomeSlotCount(bytes32 conditionId) external view returns (uint)Returns generated conditionId which is the keccak256(abi.encodePacked(oracle, questionId, outcomeSlotCount))
oracle: The account assigned to report the result for the prepared conditionquestionId: An identifier for the question to be answered by the oracleoutcomeSlotCount: The number of outcome slots which should be used for this condition. Must not exceed 256
function getConditionId(address oracle, bytes32 questionId, uint outcomeSlotCount) external pure returns (bytes32)Returns collectionId constructed by a parent collection and an outcome collection.
parentCollectionId: Collection ID of the parent outcome collection, or bytes32(0) if there's no parentconditionId: Condition ID of the outcome collection to combine with the parent outcome collectionindexSet: Index set of the outcome collection to combine with the parent outcome collection
function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint indexSet) external pure returns (bytes32)Returns positionID from collateral token and outcome collection associated to the position
collateralToken: Collateral token which backs the positioncollectionId: ID of the outcome collection associated with this position
function getPositionId(IERC20 collateralToken, bytes32 collectionId) external pure returns (uint)Emitted when a new condition is initialized
event ConditionPreparation(
bytes32 indexed conditionId,
address indexed oracle,
bytes32 indexed questionId,
uint outcomeSlotCount
)Emitted when oracle executes reportPayouts with payouts for a certain questionId
event ConditionResolution(
bytes32 indexed conditionId,
address indexed oracle,
bytes32 indexed questionId,
uint outcomeSlotCount,
uint[] payoutNumerators
)Emitted when a user splits collateral or position into multiple outcome positions
event PositionSplit(
address indexed stakeholder,
IERC20 collateralToken,
bytes32 indexed parentCollectionId,
bytes32 indexed conditionId,
uint[] partition,
uint amount
)Emitted when a user merges multiple positions back into a parent position or collateral
event PositionsMerge(
address indexed stakeholder,
IERC20 collateralToken,
bytes32 indexed parentCollectionId,
bytes32 indexed conditionId,
uint[] partition,
uint amount
)Emitted when a user redeems positions after resolution
event PayoutRedemption(
address indexed redeemer,
IERC20 indexed collateralToken,
bytes32 indexed parentCollectionId,
bytes32 conditionId,
uint[] indexSets,
uint payout
)This section illustrates the lifecycle of a simple binary conditional market, from condition preparation to redemption.
An oracle creates a condition for the question: "Will ETH trade above $3,000 on 2026-01-01?", with two possible outcomes: Yes and No.
The oracle calls prepareCondition with:
oracle = OquestionId = QoutcomeSlotCount = 2
This initializes a condition identified by: conditionId = getConditionId(O, Q, 2)
A participant deposits 100 units of collateral token C and splits it into outcome positions by calling splitPosition with:
collateralToken = CparentCollectionId = bytes32(0)conditionId = conditionIdpartition = [0b01, 0b10]amount = 100
This transfers 100 units of collateral C to the conditional tokens contract and mints two ERC-1155 outcome positions representing:
- outcome Yes (
indexSet = 0b01) - outcome No (
indexSet = 0b10)
Each position token represents a claim on the collateral conditional on the corresponding outcome. Each outcome position corresponds to positionId = getPositionId(C, getCollectionId(bytes32(0), conditionId, indexSet)).
After the resolution time, the oracle reports the result by calling reportPayouts with:
questionId = Qpayouts = [1, 0]
This assigns the full payout weight to the Yes outcome and zero to No.
A holder of the Yes position token calls redeemPositions with:
collateralToken = CparentCollectionId = bytes32(0)conditionId = conditionIdindexSets = [0b01]
The contract burns the redeemed position token and transfers 100 units of collateral C to the caller, proportional to the reported payout weights. Holders of the No position token receive no payout.
This example is illustrative and does not prescribe application-level behavior.
The oracle has absolute authority over payout distribution. A malicious or compromised oracle can direct all collateral to chosen outcomes with no on-chain dispute mechanism. Implementers SHOULD consider multi-sig oracles, time-locked reporting, or staking with slashing conditions.
Functions interacting with collateral tokens via transfer and transferFrom are susceptible to reentrancy if the token has callbacks (e.g., ERC-777). Implementations MUST follow checks-effects-interactions or use reentrancy guards.
Non-standard tokens (fee-on-transfer, rebasing) may cause accounting discrepancies. Implementations SHOULD document supported token types or measure actual balance changes.
Oracle resolution transactions are visible in the mempool. Attackers can front-run reportPayouts to acquire winning positions before resolution. Applications SHOULD consider commit-reveal schemes or private mempools.
prepareCondition is permissionless and allocates storage. Attackers can spam condition creation to bloat storage. Implementations MAY require deposits or restrict creation to authorized registries.
Copyright and related rights waived via CC0.