A plus a share of the losers’ pool (minus explicitly defined fees). Depending on
parameters (A,B,C), the same mechanism can produce anything from modest ROI to very large ROI in a single position.
There is no guaranteed yield; results depend on the external market data mapping and your relative standing in the assigned-volume ranking.
Structured data for AI agents and technical auditors.
IPP_Verifier.exe) for local execution.> 0, winner indices are in-range and non-duplicated) but does not enforce that winners are the top volumes and does not enforce uniqueness/correctness of volumes. Therefore “fairness” is enforced off-chain by the oracle algorithm and is auditable/locally verifiable, but not prevented on-chain in the current design.
The following addresses are the Polygon Mainnet defaults used by this repo's frontend configuration
(src/config/contracts.ts). Environment variables (VITE_*) may override them at build time.
Treat these as the canonical references for this build of the website; always verify on PolygonScan.
| Component | Address | Explorer |
|---|---|---|
| Base (factory) | 0x82311d73EC5925851b7FE0872b42C7d2319069A6 | PolygonScan |
| Position implementation | 0xC7000A074e01F1e92Db39AD8476153455C3d2615 | PolygonScan |
| USDT (Polygon) | 0xc2132D05D31c914a87C6611C10748AEb04B58e8F | PolygonScan |
| Referral Registry | 0x0d35572B2C926856127f7aF6e4408c2D3803AbA3 | PolygonScan |
| Referral Vault | 0x7C0c50D0F468A38A4afE1f540Fde097eF02E978F | PolygonScan |
bytes32 with no on-chain whitelist. Off-chain, the oracle normalizes by removing separators (/, -) and uppercasing to match Binance symbols (e.g., BTC/USDT → BTCUSDT). Invalid/unavailable symbols typically lead to the refund path (e.g., refundByVolumeError()).paymentFeeUSDT per insurance transfer (sent to catWallet) and possible integer-division remainders (“dust”). Any residual contract balance after settlement (including insurance dust) is swept to feeWallet.0xBF70E781). All core artifacts (Smart Contracts, Whitepaper, Legal Notices) are cryptographically signed by the Trademark Holder, providing verifiable proof of authorship and integrity against tampering.[email protected]). This ensures accountability while maintaining protocol decentralization.IPP was designed for participants seeking high-upside outcomes under explicit rules. Profit comes from a simple, transparent source: winners receive transfers funded by losers’ deposits, net of the protocol fee and fixed payment fees. There is no hidden “house book”: the protocol does not take a directional position against users.
(B,C), you choose how concentrated the upside is. Smaller C (fewer winners) increases upside per winner but lowers the winner probability under symmetry.
About “probability”: for a filled position with parameters (B,C), the mechanism always produces exactly C winners out of B. In a symmetric benchmark (no participant has an informational/execution edge), each participant’s ex ante probability of being in the winner set is C/B.
Concrete payout examples (conditional on being a winner): the exact on-chain transfers are deterministic once the position is resolved. The examples below illustrate the mechanical upside if you end up in the winner set; they are not a promise that you will win.
A = 100, B = 20, C = 1, the losers’ pool is 1900. With a 5% protocol fee, winners’ pool is 1805, so the winner receives approximately 100 + 1805 − 0.10 = 1904.90 USDT (before minor dust/rounding nuances).A = 100, B = 10, C = 3, the losers’ pool is 700. With a 5% protocol fee, winners’ pool is 665, so each winner receives approximately 100 + (665/3) − 0.10 ≈ 321.57 USDT (with integer-division dust added to winners[0]).Invariant Parameters Protocol (IPP) is a blockchain‑based mechanism that redistributes capital between participants according to a deterministic function of realized trading volumes on an external exchange. It does not inject additional randomness into outcomes: there is no pseudo‑random generator, no hidden seed, and no internal oracle entropy. Instead, outcomes are intended to be determined by publicly observable market data and a fixed algorithm; the on-chain settlement uses inputs submitted by an authorized off-chain oracle (CAT).
Economically, IPP belongs to the same formal class as futures and other zero‑sum derivatives: the aggregate gain of some participants equals the aggregate loss of others, net of explicitly defined fees. The underlying variable is not the price of an asset, but the realized trading volume of a chosen trading pair over short time windows after participant entry. This article presents the formal structure of IPP, its parameterization, and its risk properties, with particular emphasis on a systematic comparison to futures markets. For terminology and classification criteria, see Section 1.1.
In everyday language, the term "lottery" is often applied to any activity with uncertain outcomes and negative expected value after fees. From a scientific perspective this is too coarse: the same description would apply to most traded derivatives once transaction costs are taken into account. Empirical concentration of edge in a small fraction of participants (where a small minority achieves large gains while the majority incurs losses) is a generic property of zero‑sum competition with heterogeneous skill and information, not a defining feature of lotteries. This phenomenology is observed in most speculative derivatives markets, including leveraged futures, once transaction costs, funding, and price impact are taken into account.
In this article we reserve "lottery" for mechanisms in which outcomes are generated by an explicit randomization device (physical or algorithmic) that is independent of the economic processes participants may attempt to forecast, and in which participants have no levers beyond the size of their stake and, possibly, a choice among symmetric tickets.
Under this definition, both IPP and futures markets are not lotteries. In both cases outcomes are functions of endogenous market variables (price paths or volume profiles), and participants may attempt to obtain a systematic advantage through information, models, or execution. The appropriate classification for both is "zero‑sum derivative mechanisms with fees", differing not in kind but in which market variable they target and in how risk is bounded and operationalized.
If one insists on describing IPP as a "lottery" solely because most participants are unlikely to achieve positive long‑run P&L after fees, then the same label would also apply to leveraged futures and other speculative derivatives that combine uncertainty with leverage, complex liquidation rules, and opaque venue policies, especially when they are built on lower-liquidity markets (with higher manipulation susceptibility). Any concern that volume‑based mechanisms on relatively illiquid pairs might be distorted by wash trading or other forms of manipulation applies at least as strongly to price‑based leveraged products on the same pairs, where engineered price moves can produce non-linear losses via liquidation mechanics and feedback effects (e.g., cascades) depending on venue microstructure.
Invariant Parameters Protocol includes an optional deposit insurance mechanism that allows participants to mitigate downside risk while preserving the zero-sum economic structure of the protocol. This peer-to-peer (P2P) insurance system is implemented entirely on-chain and operates without external insurance providers or centralized risk pools. The mechanism is unique among DeFi protocols of this class, offering participants a transparent, deterministic way to protect their deposits while maintaining the protocol's core economic properties.
When creating or joining a position, participants may optionally purchase insurance by paying an additional insurance premium alongside their deposit A. The premium is calculated deterministically based on the position parameters using the formula:
R = A × (B − C) / B
where R is the insurance premium, A is the deposit amount, B is the total number of participants, and C is the number of payout receivers. This formula ensures that the premium is proportional to the expected loss probability (the ratio of non-receivers to total participants) and scales linearly with the deposit amount.
The insurance premium has several key properties:
All insurance premiums paid by participants in a position form an insurance pool. This pool is maintained separately from the main deposit pool (A × B) and is used exclusively for insurance payouts. The pool size equals the sum of all premiums paid: if N participants purchase insurance, the pool contains N × R USDT.
The insurance pool operates as a peer-to-peer risk-sharing mechanism: premiums from insured participants who become winners may be used to compensate insured participants who become losers. This creates a mutual insurance structure where participants collectively bear risk, rather than relying on external capital or protocol reserves.
Insurance settlement occurs automatically when the position is resolved (after volumes are submitted and winners are determined). The settlement logic follows two distinct paths depending on whether there are insured participants among the losers:
Case 1: No Insured Losers
If all insured participants are among the C winners, there are no insured losers to compensate. In this case, the insurance pool is returned to the insured participants. Each insured participant receives their premium back minus a fixed payment fee of 0.1 USDT (R − 0.1 USDT). This fee covers the gas costs for the insurance payout transaction. The protocol retains only this minimal fee; no additional charges are applied to insurance premiums.
Case 2: Insured Losers Present
If one or more insured participants are among the B−C losers, the insurance pool is distributed among the insured losers to compensate for their deposit loss. The distribution follows these rules:
Because the pool is split using integer division, a remainder (“dust”) may remain undistributed. After settlement, any remaining USDT balance in the Position (including insurance dust) is swept to feeWallet as residual dust.
This mechanism ensures that insurance premiums are either returned to participants (when no claims occur) or used to compensate losers (when claims occur). The protocol itself does not profit from insurance; it only collects the 0.1 USDT payment fee per insurance transaction to cover gas costs. This makes the insurance mechanism effectively free from a net cost perspective: premiums are either returned or redistributed among participants, with only minimal transaction fees retained by the protocol.
Winning Outcome: If an insured participant becomes a winner, they receive their standard position payout (A plus their share of the winners' pool, minus protocol fees and payment fees). Their insurance premium is handled according to the settlement rules above: if there are no insured losers, the premium is returned (minus 0.1 USDT); if there are insured losers, the premium is not returned and contributes to the compensation pool.
Losing Outcome: If an insured participant becomes a loser, they lose their deposit A (which is redistributed to winners). However, they receive compensation from the insurance pool according to the settlement rules. This compensation may fully or partially offset their deposit loss, depending on the size of the insurance pool and the number of insured losers. Without insurance, a losing participant would lose their entire deposit A; with insurance, they receive compensation that mitigates this loss.
Position Not Filled (Refund): If a position does not fill with B participants within the 24-hour deadline, all participants receive refunds. Insured participants receive both their deposit A and their insurance premium R, minus a single payment fee of 0.1 USDT (A + R − 0.1 USDT). This ensures that insurance premiums are fully refundable if the position does not proceed to resolution.
Early Exit: Participants can exit a position early (before it fills) by calling leavePosition().
Insured participants who exit early receive both their deposit A and their insurance premium R, minus a single payment
fee of 0.1 USDT (A + R − 0.1 USDT). This allows participants to recover their insurance premium if they choose to exit
before the position resolves.
The insurance mechanism in IPP exhibits several properties that distinguish it from traditional insurance models and other DeFi protocols:
paymentFeeUSDT paid per insurance transfer (sent to catWallet). Additionally, small residual amounts (“dust”) may remain due to integer division and are swept to feeWallet as contract balance remainder.This P2P insurance model is unique among DeFi protocols of this class. Most DeFi insurance mechanisms rely on external capital providers, centralized risk pools, or complex tokenomics. IPP's insurance operates as a transparent, on-chain mutual insurance system where participants collectively share risk without intermediaries or protocol profit extraction.
The insurance mechanism provides several advantages for participants:
feeWallet.The insurance mechanism opens new possibilities for participants:
The implementation of insurance in IPP demonstrates how peer-to-peer risk sharing can be achieved in a decentralized, transparent, and cost-effective manner, without requiring external capital providers or complex tokenomics. This innovation expands the protocol's utility while maintaining its core economic properties and deterministic settlement mechanisms.
This section provides a comprehensive, plain-language explanation of how IPP operates, from position creation to settlement, without requiring prior knowledge of blockchain technology or financial derivatives. The description covers the complete lifecycle of a position, the roles of each system component, and the deterministic mechanisms intended to define a reproducible mapping from participation records to outcomes.
A new position is created on the blockchain by calling createPositionAndJoin(...) on the Base contract (which creates or reuses an open position and immediately joins it). The creator specifies four core parameters that define the position's structure:
bytes32. The contracts do not enforce a whitelist on-chain. The oracle expects a Binance-compatible trading pair (e.g., BTCUSDT) and attempts to resolve volumes for it; if the symbol is invalid/unavailable, the position will typically end up in the refund path (e.g., via refundByVolumeError()).
When a position is created, the Base contract deploys a new Position contract instance using the
minimal proxy pattern (EIP-1167). This cloned contract becomes the custodian for all participant funds under fixed on-chain rules and implements the
complete position logic. The position also has a fixed safety deadline T = 24 hours from creation. If
the position does not fill with B participants within this time, deposits become refundable under the on-chain rules; after the deadline, anyone can call finalize() to trigger refunds (deposit A minus the fixed payment fee).
The total possible parameter combinations per trading pair is approximately 398 million (20,000 values for A times 19,900 combinations of (B,C)), allowing for a rich spectrum of risk-return profiles, from nearly symmetric allocations (C ≈ B/2) to highly selective ones (C ≪ B).
Other participants can join an open position via Base.createPositionAndJoin which transfers A USDT directly to the Position, then calls Position.joinFromBaseFunded(participant). The deposit is exactly A USDT; the fixed payment fee is applied on refunds/early exits and on winner payouts (see contract logic). The contract uses the ERC-20 transferFrom
mechanism to transfer USDT from the participant's wallet to the Position contract. Each join transaction is recorded on-chain with a second-resolution timestamp (block.timestamp). This matches the 1-second kline binning used by the oracle, but the timestamp is a consensus field (not an exchange timestamp) and is treated as part of the deterministic on-chain input.
Participants can join in any order, and the position fills when the B-th participant joins. At this moment, the Position
contract emits a PositionFull event, which triggers the CAT bot to begin the volume calculation process.
The contract also records the firstJoinTimestamp (when the first participant joined) and
positionFullTimestamp (when the B-th participant joined), which define the time window for volume assignment.
Until the position is full and volumes are submitted, any participant can exit early by calling
leavePosition() (before the position is full). This returns A − paymentFeeUSDT, and transfers paymentFeeUSDT to catWallet. Early exit allows participants
to abandon positions that are not filling or that they no longer wish to participate in. Once volumes are submitted to the
contract, early exit is no longer possible, and the position proceeds to settlement.
The Position contract maintains an on-chain array of participants, each with their wallet address, join timestamp, and an on-chain index (0 to B−1) that determines the order in which volumes will be assigned. This index is assigned based on the order of joining, not the join timestamp, ensuring deterministic ordering for the volume assignment algorithm.
Note: the on-chain index is the participant’s current index in the participants[] array. Before the position becomes full, leavePosition() uses swap-and-pop, so the indices (and the array order) of remaining participants may change. After PositionFull, exits are disabled and the indices become stable for resolution.
Once B participants have joined and the PositionFull event is emitted, an off-chain service called the
CAT bot (Contract Automation Tool) automatically begins processing the position. The CAT bot is a Python
application that runs continuously, monitoring the blockchain for events and performing deterministic volume calculations.
It is the only authorized entity (via the authorizedCat address in the Base contract) that can submit volume
results to positions.
The CAT bot performs the following sequence of operations:
getAllData() on the Position contract.
This returns all participant addresses, their exact join timestamps (in seconds), and their on-chain indices (0 to B−1).
The bot also retrieves the firstJoinTimestamp and positionFullTimestamp to define the time
window for volume data retrieval.T_first (minimum join timestamp) to T_last + MAX_SEARCH_SEC (maximum join timestamp plus search window, typically 300 seconds by default). The data is converted to a second-precision dictionary mapping each timestamp
(in seconds) to the quote volume (in USDT) from 1-second klines.MAX_SEARCH_SEC seconds (typically 300 seconds by default, configurable). The search begins at the join timestamp itself (dt_sec=0), but typically finds a volume in a subsequent moment.volume_int = floor(volume_decimal × 106).k (0..4) it uses MAX_SEARCH_SEC + 60*k as the forward-search bound. With default MAX_SEARCH_SEC = 300, the maximum expansion is 540 seconds after T_last (300 + 240).submitResults(volumes[], winnerIndices[]) on
the Position contract. This transaction must be signed by the authorized CAT wallet and requires POL (Polygon's native
token) for gas fees.The volume assignment algorithm is deterministic: given the same join timestamps, the same on-chain indices, and the same Binance data, the algorithm is deterministic given the same inputs; the on-chain outcome is deterministic given the submitted (volumes, winnerIndices). This makes the computation externally auditable (recomputable) and allows post-hoc verification against on-chain submissions. The algorithm's determinism is guaranteed by its use of fixed ordering (on-chain index), fixed search windows (MAX_SEARCH_SEC seconds forward from join time, typically 300 seconds), and fixed conversion rules (floor of decimal × 106).
If the CAT bot cannot assign unique volumes to all participants (e.g., due to insufficient trading volume in the time window
or too many duplicate volumes), the position is declared unresolvable. In this case, the bot calls
refundByVolumeError() on the Position contract, which refunds all deposits to participants. This is intended to avoid settlement under missing/failed volume assignments; in such cases the refund path is used instead.
Once volumes are submitted to the smart contract via submitResults(), the settlement process is fully automatic
and irreversible. The Position contract performs the following calculations and transfers:
submitResults() checks only: position is full, array lengths match, each submitted volume is > 0, and winnerIndices are in-range and non-duplicated. It does not verify that winnerIndices correspond to the top volumes, and it does not enforce volume uniqueness/correctness on-chain.Pool = A × BLosersPool = A × (B − C) (the deposits from participants who did not win)F_prot = LosersPool × X, where X is the protocol fee rate (currently 5% or 0.05)WinnersPool = LosersPool − F_prot (the amount available for redistribution to winners)winners[0], where winners[] is formed exactly in the order of winnerIndices[] submitted by the oracle (the contract does not sort winners on-chain). Therefore, dust attribution is deterministic given the submitted ordering, but the ordering itself is not enforced on-chain and must be verified off-chain. Each winner receives:
Payout_per_winner = WinnersPool / C (with dust handling).Π_win = A + (WinnersPool / C) − paymentFeeUSDT.Π_lose = −A.F_prot is transferred to the feeWallet address (set in the Base contract).catWallet address
(the CAT bot operator's wallet).
All transfers are executed atomically within the submitResults() transaction using the SafeERC20 library for
secure token transfers. The contract uses ReentrancyGuard to prevent reentrancy attacks. There is no manual intervention in on-chain settlement after submitResults() is mined; the on-chain execution is irreversible for that position. The Position contract marks itself as
closed and emits a PositionClosed event, which notifies the Base contract and any monitoring systems.
Payout is executed inside submitResults() (atomic settlement); there is no separate on-chain claim step for winners. This transfers their winnings to their wallet. Losers have no payout to claim; their funds have already been redistributed.
The IPP design yields several fundamental properties of the mechanism:
authorizedCat, which affects who may submit resolution inputs.These properties define a zero-sum mechanism with fees and uncertain outcomes, while bounding maximum loss per position and bounding maximum lock time, and enabling post-hoc verification of oracle submissions.
The CAT (Contract Automation Tool) bot is a critical off-chain component that bridges real-world market data with on-chain smart contracts. It serves as a deterministic oracle that calculates and submits trading volumes for IPP positions. The bot is implemented in Python and runs continuously as a systemd service, monitoring the blockchain and processing positions as they become eligible for resolution.
The CAT bot is responsible for the following critical functions:
PositionCreated, ParticipantJoined, PositionFull, and other position lifecycle
events in real-time.submitResults() function, managing gas prices, transaction simulation, and confirmation handling.finalize() on expired positions that
have not been resolved, triggering automatic refunds to participants.
The bot operates continuously, processing positions as they become full. It is the only authorized entity
(via the authorizedCat address in the Base contract) that can submit volumes to positions. This authorization
is enforced on-chain, preventing unauthorized submissions; correctness of the submitted (volumes, winnerIndices) relative to the published algorithm is intended to be verifiable post-hoc and is not fully enforced on-chain in the current version.
The CAT bot is implemented as a modular Python application with the following core components:
main.py: Entry point that initializes the system, performs bootstrap synchronization,
and starts all background workers (event subscriptions, resolution worker, finalization worker, periodic sync worker,
WebSocket collector, metrics worker).events.py: Handles blockchain event subscriptions via WebSocket, processing
PositionCreated and PositionFull events, triggering WebSocket collection for new positions,
and scheduling resolution tasks when positions become full.resolver.py: Core resolution orchestrator that processes filled positions, coordinates
volume calculation, performs safety checks, and submits results to smart contracts. Implements a priority queue system
for efficient processing of multiple positions.volume.py: Implements the deterministic volume assignment algorithm over 1-second kline data (timestamps in seconds), attempting to assign positive, unique volumes in the correct order (by on-chain index).binance_client.py: Fetches trading data from Binance REST API, converting kline data to
second-precision time-volume dictionaries from 1s klines. Handles rate limiting, retries, and data normalization.binance_ws_collector.py: Maintains real-time WebSocket connections to Binance, collecting
aggregate trade data as positions are created and caching it in the database. Improves performance (especially for frequently-used symbols) by reducing repeated REST API calls during volume calculation iterations; the magnitude of improvement depends on symbol activity and cache hit rate.chain.py: Provides Web3 interface for reading contract state (getAllData())
and submitting transactions (submitResults(), refundByVolumeError()). Handles RPC failover,
transaction simulation, and gas management.db.py: Database access layer for PostgreSQL, providing functions for storing and retrieving
position state, participant data, volume assignments, and audit trails. The database serves as a cache and historical
record, but smart contracts remain the source of truth.sync.py: Performs synchronization between on-chain contract state and off-chain database,
including bootstrap sync on startup and periodic sync operations to catch any missed events or state changes.finalizer.py: Handles timeout scenarios by scanning for expired positions that have not
been resolved and calling finalize() to trigger automatic refunds.metrics.py: Collects and reports system metrics for monitoring, including processing
times, success rates, queue sizes, and error counts.The complete workflow from position creation to resolution proceeds as follows:
PositionCreated event. The CAT bot's event subscription handler detects this event and:
ParticipantJoined
events. The bot tracks these events and updates the database, but does not take action until the position is full.PositionFull event. The bot's event handler:
getAllData() on the Position contract to retrieve the complete on-chain stateT_first (minimum join timestamp) to
T_last + MAX_SEARCH_SEC (maximum join timestamp plus search window, typically 300 seconds)submitResults(volumes[], winnerIndices[]) transactionPositionClosed event. The bot:
The core volume assignment algorithm is implemented in volume.py and operates with 1-second precision using Binance 1s klines.
The algorithm is specified to be deterministic and to enforce uniqueness/nonzero constraints; if assignment fails for any participant, the position is refunded (Section 3.3):
Input Data:
participants: List of Participant objects, each containing:
address: Wallet addressjoinTimestamp: Join timestamp in seconds (from blockchain)index_onchain: On-chain index (0 to B−1), determining processing ordertime_volume: Dictionary mapping timestamp in seconds to quote volume (Decimal) from Binance 1s klinesmax_search_sec: Maximum search window in seconds (typically 300)Algorithm Steps:
index_onchain (0 to B−1) and that all
indices are unique. Verify that time_volume contains sufficient nonzero volumes.index_onchain, not join time):
join_ts_sec = joinTimestampmax_search_sec = MAX_SEARCH_SEC (typically 300 seconds by default, configurable via environment variable)dt_sec = 0dt_sec ≤ max_search_sec:
t_sec = join_ts_sec + dt_sect_sec already occupied by another participant (collision handling)time_volume[t_sec] (1s kline quote volume)volume_int = floor(volume × 106)volume_int has not been assigned to any previous participant (checked against
global set of used volumes):volume_int to participant ivolume_int to the set of used volumest_sec as occupieddt_sec by 1 (search with 1-second precision)CannotAssignVolumeForParticipant exceptionindex_onchain)
and detailed assignment results for audit purposes.Key Properties:
index_onchain, not join time.
This ensures that earlier joiners (with lower indices) have priority in volume assignment, but the order is fixed
and not dependent on timing of algorithm execution
Public Availability: The volume assignment algorithm logic is publicly available and implemented in the
open-source verification script ipp_volume_verifier.py, which can be downloaded from the IPP website. This
script contains an identical implementation of the CAT bot's volume assignment algorithm, enabling independent verification and external review of the volume assignment process.
Open Source Code: The complete CAT bot code responsible for volume assignment and participant ranking is available in open access and is presented below in this file. Similarly, smart contract codes are also available in open access and accessible for review and audit. This ensures full transparency of the algorithm and allows anyone to verify the correctness of the system's operation.
CAT Bot Source Code (Volume Assignment and Ranking):
"""
Volume assignment module for CAT bot.
ROLLBACK TO 1-SECOND PRECISION with collision handling.
History:
- Originally: second_volume: Dict[t_sec -> Decimal(quoteVolume)]
- Updated to: time_volume: Dict[t_ms -> Decimal(quoteVolume)] (milliseconds)
- Now (rollback): time_volume: Dict[t_sec -> Decimal(quoteVolume)] (seconds)
NEW: Collision Handling
- For each participant i we search in [join_ts_sec; join_ts_sec + max_search_sec]
- If a second is already occupied by another participant → skip to next second
- If volume is 0 → skip to next second
- If Volume_Int is duplicate → skip to next second
- This handles queue: multiple participants entering in same block/second
Key algorithm:
- For each participant i (index_onchain 0..B-1):
- Search forward from join_ts_sec in 1-second steps (up to max_search_sec)
- Find first nonzero, unique, and UNOCCUPIED volume
- volume_int = floor(volume_dec * 10^6)
- If any participant cannot be assigned -> position is unresolvable
Requirements:
- All volumes must be > 0
- All volumes must be unique (globally for the position)
- All volume_int must be unique (no duplicates)
- Order by index_onchain (NOT by join time)
"""
from __future__ import annotations
from decimal import Decimal, ROUND_FLOOR
from typing import Dict, List, Set, Tuple
from dataclasses import dataclass
from .logging import get_logger
logger = get_logger(__name__)
class CannotAssignVolumeForParticipant(Exception):
"""Raised when volume cannot be assigned to a participant."""
def __init__(self, participant_index: int, reason: str = "No unique nonzero volume found"):
self.participant_index = participant_index
self.reason = reason
super().__init__(f"Cannot assign volume for participant {participant_index}: {reason}")
@dataclass
class Participant:
"""Participant data for volume assignment."""
index_onchain: int
address: str
join_ts: int # Join timestamp in seconds
@dataclass
class ParticipantResult:
"""Result of volume assignment for a participant."""
index_onchain: int
address: str
join_ts: int # Join timestamp in seconds
assigned_sec: int # Timestamp of assigned volume (seconds) - open_time_sec of kline
volume_dec: Decimal # Assigned volume (Decimal)
volume_int: int # Assigned volume (integer, as in contract)
search_iterations: int # Number of seconds searched before finding
def assign_volumes_for_round(
participants: List[Participant],
time_volume: Dict[int, Decimal],
max_search_sec: int,
) -> Tuple[List[int], Dict[int, ParticipantResult]]:
"""
Assign unique volumes to participants, using second-level time grid with collision handling.
Algorithm:
1. For each participant i (by index_onchain 0..B-1):
- Search in [join_ts_sec; join_ts_sec + max_search_sec]
- For each t_sec:
- Skip if second is already occupied (by previous participant)
- Skip if volume_dec <= 0
- Compute volume_int = floor(volume_dec * 10^6)
- Skip if volume_int <= 0
- Skip if volume_int is already used (duplicate)
- Otherwise: assign to participant i, mark second as occupied
- If no volume found -> raise CannotAssignVolumeForParticipant
2. Collision handling (NEW):
- Multiple participants can have same join_ts_sec (entered in same block)
- They get consecutive seconds: first gets t, second gets t+1, etc.
- Each second can only be assigned once
Args:
participants: List of Participant objects, ordered by index_onchain
time_volume: Dict mapping t_sec -> Decimal(quoteVolume)
max_search_sec: Δ (seconds) - max seconds to search for volume
Returns:
(volumes, results) where:
- volumes: List[int] of length B, volumes[i] for participant i (index_onchain)
- results: Dict[index_onchain, ParticipantResult] with assignment details
Raises:
CannotAssignVolumeForParticipant: If volume cannot be assigned to any participant
Note:
Participants MUST be ordered by index_onchain (0..B-1)!
Do NOT reorder participants array.
"""
B = len(participants)
logger.info("🎯 Starting volume assignment for %d participants (1s precision with collision handling)", B)
logger.debug(" Search window: Δ = %d seconds (per participant)", max_search_sec)
logger.debug(" timeVolume data points (second buckets): %d", len(time_volume))
# Validate participants are ordered by index_onchain
for i, participant in enumerate(participants):
if participant.index_onchain != i:
raise ValueError(
f"Participants must be ordered by index_onchain. "
f"Expected index {i}, got {participant.index_onchain}"
)
# Track used values globally
used_volumes: Set[int] = set() # volume_int values already assigned
occupied_seconds: Set[int] = set() # seconds already assigned to participants
# Results
volumes: List[int] = [0] * B
results: Dict[int, ParticipantResult] = {}
# Assign volumes sequentially by index_onchain
for i, participant in enumerate(participants):
join_ts_sec = participant.join_ts
logger.debug(
"🔍 Participant %d/%d: index=%d, addr=%s, join_ts=%d sec",
i + 1,
B,
participant.index_onchain,
participant.address,
join_ts_sec,
)
found = False
search_iterations = 0
# Search in second window [join_ts_sec; join_ts_sec + max_search_sec]
for dt_sec in range(0, max_search_sec + 1):
t_sec = join_ts_sec + dt_sec
search_iterations += 1
# Skip if this second is already occupied by another participant
if t_sec in occupied_seconds:
continue
# Get volume at this second
vol_dec = time_volume.get(t_sec, Decimal("0"))
# Skip if zero or negative
if vol_dec <= 0:
continue
# Convert to uint256 (6 decimals for USDT)
vol_int = int(
(vol_dec * Decimal(10**6)).to_integral_value(rounding=ROUND_FLOOR)
)
# Skip if zero after conversion
if vol_int <= 0:
continue
# Skip if already used (must be globally unique)
if vol_int in used_volumes:
continue
# Found! Assign volume and mark second as occupied
used_volumes.add(vol_int)
occupied_seconds.add(t_sec)
volumes[i] = vol_int
results[i] = ParticipantResult(
index_onchain=participant.index_onchain,
address=participant.address,
join_ts=join_ts_sec,
assigned_sec=t_sec,
volume_dec=vol_dec,
volume_int=vol_int,
search_iterations=search_iterations,
)
logger.debug(
" ✅ Found at t_sec=%d (+%d sec): vol_dec=%s, vol_int=%d",
t_sec,
dt_sec,
vol_dec,
vol_int,
)
found = True
break
# If not found - position is unresolvable
if not found:
logger.error(
"❌ Participant %d (index=%d, addr=%s): No unique nonzero volume found in [%d; %d] sec window",
i,
participant.index_onchain,
participant.address,
join_ts_sec,
join_ts_sec + max_search_sec,
)
raise CannotAssignVolumeForParticipant(
participant_index=i,
reason=f"No unique nonzero volume in {max_search_sec}s window starting from {join_ts_sec}",
)
# Log summary
logger.info("✅ Volume assignment complete for %d participants", B)
logger.debug(" Used seconds: %d unique seconds occupied", len(occupied_seconds))
logger.debug(" Used volumes: %d unique volume_int values", len(used_volumes))
for i in range(B):
result = results[i]
logger.debug(
" Participant %d: vol=%d, assigned_at=t+%d sec (from join_ts_sec)",
i,
result.volume_int,
result.assigned_sec - result.join_ts,
)
return volumes, results
def compute_winner_indices(
participants: List[Participant],
volumes: List[int],
C: int,
) -> List[int]:
"""
Select top C winners from participants based on volumes.
Sorting rules:
1. Primary: volume (descending - higher is better)
2. Tie-break: join_ts (ascending - earlier is better)
3. Final tie-break: index_onchain (ascending)
Args:
participants: List of Participant objects
volumes: List of volume_int values (parallel to participants)
C: Number of winners to select
Returns:
List of winner indices (index_onchain), sorted by volume descending
"""
B = len(participants)
# Create list of (index_onchain, volume, join_ts) for sorting
ranked = [
(
participants[i].index_onchain,
volumes[i],
participants[i].join_ts,
)
for i in range(B)
]
# Sort by: volume DESC, join_ts ASC, index_onchain ASC
ranked.sort(key=lambda x: (-x[1], x[2], x[0]))
# Take top C and extract indices
winner_indices = [ranked[i][0] for i in range(min(C, len(ranked)))]
return winner_indices
__all__ = [
"Participant",
"ParticipantResult",
"assign_volumes_for_round",
"compute_winner_indices",
"CannotAssignVolumeForParticipant",
]
Iterative Window Expansion: If the initial search window (MAX_SEARCH_SEC, typically 300 seconds) does not yield sufficient unique volumes, the resolution orchestrator expands the window iteratively. The expansion adds 60 seconds to the search window on each retry attempt (up to 5 attempts total), continuing until all volumes are assigned or the maximum number of attempts is reached, or the safe submission deadline is approached.
To optimize performance and reduce Binance API rate limit issues, the CAT bot implements a real-time WebSocket trade data collector. This system:
This system is intended to reduce redundant REST calls and latency in repeated computations; performance is deployment-specific and should be quoted only with benchmark methodology.
Before submitting volumes to a smart contract, the CAT bot performs comprehensive safety checks:
closed == false)volumesSubmitted == false)index_onchain)resolveDeadline − SAFETY_GAP_SEC)submitResults() transaction to ensure it will
succeed on-chain before sending
If any check fails, the bot logs the error and does not submit the transaction. For unresolvable positions (insufficient
unique volumes), the bot calls refundByVolumeError() to refund all participants.
The CAT bot uses PostgreSQL as a cache and audit trail. Key tables include:
positions: Position metadata (A, B, C, symbol, deadlines, status, on-chain address)round_participants: Participant data (address, join timestamp, on-chain index, assigned
volume, winner status)binance_klines_1s_cache: Cached 1s kline data from Binance (symbol, open_time_sec, quote_volume,
agg_trade_id) with indexes for fast retrievalbinance_ws_subscriptions: Active WebSocket subscriptions per trading pairvolume_oracle_proofs: Complete audit trail of volume assignments, including all inputs
and outputs for reproducibilityposition_logs: Log of all CAT bot actions (resolution attempts, submissions, errors)global_state: Last processed blockchain block number for event synchronizationThe database serves as a cache only; smart contracts remain the source of truth. The bot always verifies on-chain state before making decisions and synchronizes the database with on-chain data regularly.
The CAT bot handles various edge cases and error scenarios:
refundByVolumeError() to refund all
participants.finalize() to trigger automatic refunds.
If the oracle cannot assign unique volumes for all participants before the on-chain deadline, it calls refundByVolumeError() (oracle-only) to refund all participants. Separately, finalize() is permissionless: after the deadline, anyone can call it; if volumes were not submitted, the contract refunds all participants via the timeout path. In both refund paths, the contract refunds A + premium − paymentFeeUSDT per participant (when a premium exists), and sweeps any leftover dust to feeWallet.
All errors are logged with detailed context, enabling debugging and audit trails. The bot is designed to avoid invalid submissions via pre-checks and simulation; failures fall back to retry and/or refund paths.
The CAT bot is designed to be deterministic and reproducible (conditional on inputs and code version):
getAllData())ipp_volume_verifier.py, which implements the identical algorithm used by
the CAT bot. This script can be downloaded from the IPP website and allows anyone to independently verify volume
assignments for any position. The public availability of this algorithm enhances transparency and enables independent
verification; however, correctness is not enforced on-chain and therefore assumes an honest authorized oracle (or a future dispute/decentralization layer).Code version note (current state). The protocol does not pin the oracle build/version on-chain. Reproducibility therefore assumes the same published algorithm and an equivalent oracle implementation. For stronger provenance, publish an official release manifest (commit hash + SHA-256 hashes of verifier binaries/scripts) and sign it (e.g., GPG), so third parties can verify they are running an authentic build.
This determinism supports externally verifiable operation and post-hoc detection of oracle deviations; it does not remove the trust assumption in the authorized oracle under the current on-chain design.
IPP is implemented as a system of two smart contracts deployed on the Polygon blockchain. These contracts are verified on PolygonScan, making their source code publicly auditable. The contracts use OpenZeppelin libraries for security (SafeERC20, ReentrancyGuard) and follow best practices for gas efficiency and security.
The Base.sol contract serves as a factory for creating positions and stores global protocol parameters.
It is deployed once and manages the entire protocol lifecycle.
Key Functions:
createPositionAndJoin(usdt, A, B, C, symbol): Creates a new Position contract instance using the minimal proxy
pattern (EIP-1167). This clones a fixed Position implementation, ensuring all positions use the same immutable
logic. Base tracks an open position per parameter key (openPositionByKey) and may reuse it until it closes. Participants
join with only their deposit A (no insurance).createPositionAndJoinWithInsurance(usdt, A, B, C, symbol, insure): Similar to createPositionAndJoin,
but allows participants to optionally purchase insurance by setting insure to true. If insurance
is enabled, the participant pays both deposit A and insurance premium R, which is calculated automatically based on
position parameters.setOwner(newOwner): Allows the current owner to transfer ownership to a new address. This is an owner-only function.setFeeWallet(newFeeWallet): Allows the owner to change the address that receives protocol fees. This is an owner-only function.setCatWallet(newCatWallet): Allows the owner to change the address that receives payment fees (0.1 USDT per transaction). This is an owner-only function.setAuthorizedCat(newCat): Sets the address authorized to submit volumes to positions. Only this address
can call submitResults() on Position contracts. Note that each Position contract fixes its authorizedCat at initialization, so this change only affects newly created positions.setFeeBps(newFeeBps): Allows the owner to decrease the protocol fee rate (monotonic non-increasing; cannot increase beyond the current value; bounded by MAX_FEE_BPS = 5%).
This one-way restriction protects participants from fee increases.setPaymentFeeUSDT(newFee): Allows the owner to decrease the payment fee (monotonic non-increasing; cannot increase beyond 0.10 USDT).
This one-way restriction protects participants from fee increases.rescueTokenFromPosition(position, token): Allows the owner to rescue non-USDT tokens that may be
accidentally sent to a Position contract. This function can only be called after the position is closed and cannot
access USDT funds, which are locked according to Position contract rules.Owner Capabilities and Restrictions:
authorizedCat changes who is permitted to submit submitResults() / call refundByVolumeError() for unresolved positions. Note that each Position contract fixes its authorizedCat at initialization, so this change only affects newly created positions.rescueTokenFromPosition)finalize() refunds after the deadlineGlobal Parameters:
feeWallet: Address that receives protocol fees (5% of losers' pool)catWallet: Address that receives payment fees (0.10 USDT per transaction)authorizedCat: Address authorized to submit volumes (intended to correspond to the oracle/CAT wallet)feeBps: Protocol fee rate in basis points (currently 500 = 5%)paymentFeeUSDT: Fixed payment fee in USDT (currently 0.10 USDT, with 6 decimals)ALLOWED_USDT: Hardcoded USDT token address on Polygon (0xc2132D05D31c914a87C6611C10748AEb04B58e8F)POSITION_DURATION: Fixed at 24 hours (86,400 seconds)
Each position is implemented as a cloned instance of the Position.sol contract. The contract uses the minimal
proxy pattern (EIP-1167), which means all positions share the same immutable implementation code, but each has its own
storage. This ensures gas efficiency (deployment costs ~45,000 gas vs. ~2,000,000 for full deployment) while maintaining
immutability and security.
Key Functions:
joinFromBaseFunded(participant) (onlyBase): Allows a participant to join the position; public join() is removed. Records the participant's
address, join timestamp, and assigns an on-chain index (0 to B−1). Emits ParticipantJoined event. When
the B-th participant joins, emits PositionFull event.leavePosition() (before full): Allows a participant to exit early (before position is full and volumes are submitted). Returns
deposit A minus payment fee. Emits ParticipantLeft event.submitResults(volumes[], winnerIndices[]): Called only by the authorized CAT address. Submits volume
assignments and winner indices. The contract verifies that volumes are positive; verifies winnerIndices are unique and in-range; it does not verify that winnerIndices correspond to top volumes. Then it calculates payouts, transfers funds to winners, collects protocol fee, and marks the position as
closed. Payout is executed inside submitResults() (atomic settlement); there is no separate on-chain claim step for winners. Emits VolumesSubmitted and PositionClosed events.finalize(): Can be called by anyone after the 24-hour deadline if the position has not been resolved.
Triggers automatic refunds to all participants (deposit A minus payment fee). This ensures capital cannot be locked
indefinitely if the CAT bot fails.refundByVolumeError(): Called by the authorized CAT address if volumes cannot be assigned (unresolvable
position). Refunds all participants.getAllData(): Returns complete position state in a single call, including all participants, timestamps,
volumes, status flags, and balances. This is the single source of truth for position state and is used by the CAT
bot for synchronization.Security Features:
submitResults() and
refundByVolumeError(). This is enforced via require(msg.sender == base.authorizedCat()).finalize() for refunds).Autonomous Operation:
Once a Position contract is created and initialized, its parameters and implementation are immutable, and funds can only move according to the Position rules. The protocol owner cannot:
The protocol owner can change authorizedCat at the Base level, which changes who may submit submitResults() or call refundByVolumeError() for unresolved positions. Therefore, outcomes are determined by on-chain logic applied to oracle-submitted inputs; correctness relative to the published algorithm is verifiable post-hoc but not enforced on-chain in the current version.
Both contracts are verified on PolygonScan, making their source code publicly visible and auditable. The contracts use standard Solidity patterns and OpenZeppelin libraries; nevertheless, protocol-specific logic should be audited independently. The Position implementation is immutable after deployment (via the minimal proxy pattern), ensuring that the logic cannot be changed for existing positions.
Verification Process:
GPG Notarization: The contracts are also GPG-signed with fingerprint
3022A4B79A1E62BB6C201C412FBA74B1BF70E781, providing a cryptographic signature over specific published artifacts (integrity relative to the signed artifact).
Source Code Availability: The complete source code of both Base.sol and Position.sol
contracts is included in this document (see Appendix: Smart Contract Source Code below). This supports transparency and facilitates independent security review. The contracts are open-source, publicly auditable, and can be independently
verified against the deployed contracts on PolygonScan.
IPP implements multiple layers of security to protect participant funds and ensure protocol integrity:
Smart Contract Security:
authorizedCat can submit; authorizedCat is owner-configurable, so correctness relies on oracle honesty and is verifiable post-hoc rather than enforced on-chain.authorizedCat, which affects oracle identity and therefore affects who can submit resolution inputs. Oracle correctness and external market manipulation remain separate risk categories.Volume Assignment Security:
Data Source Security:
block.timestamp) to align on-chain
join times with external market data. This reduces ambiguity in mapping join times to external 1-second bins; it does not prevent exchange-level manipulation, validator timestamp freedom within chain rules, or oracle misreporting.Operational Security:
finalize() to trigger refunds, preventing indefinite fund locking.Limitations and Residual Risks:
finalize() to refund participants under the predefined rules.In addition to the core position mechanism, IPP implements a referral (partner) program that redistributes a configurable portion of protocol cash flows to referrers, while preserving the non‑custodial structure of positions. Conceptually, the referral program is not a separate market mechanism: it is a fee routing and accounting layer that operates on top of the protocol fee already defined in the base payoff model.
The primary design goals are: (i) on-chain immutability of referral relationships, (ii) pull‑payment claims by recipients, (iii) strict separation between user‑owed balances and withdrawable protocol profit, and (iv) idempotent, auditable accrual logic that is triggered only when a position has actually paid out on-chain.
Referral relationships are recorded in a dedicated on‑chain registry contract as a mapping \( R(u) \mapsto r \), where \( u \) is a user address and \( r \) is the referrer address. The mapping is write‑once: once a referrer is registered for a user, it cannot be modified or deleted. This immutability eliminates retroactive reassignment of referrers and makes the relationship fully auditable on-chain.
The registry enforces minimal integrity constraints:
Referral rewards are paid from a dedicated vault contract that holds USDT and tracks accrued balances per recipient using the pull‑payment pattern. The vault maintains two key state variables: individual accrued balances \( \text{balances}[x] \) and an aggregate liability \( \text{totalOwed} = \sum_x \text{balances}[x] \). The critical invariant enforced at accrual time is that the vault remains fully collateralized: \[ \text{USDTBalance}_{\mathrm{vault}} \ge \text{totalOwed}. \]
At the protocol level, the standard position contract already computes a protocol fee \( F_{\mathrm{prot}} \) at payout time.
To enable the referral program, the Base contract’s feeWallet is configured to point to the ReferralVault. As a consequence, when a position pays out, the on-chain protocol fee is transferred directly into the vault.
The vault exposes three relevant flows:
addRewards(recipients[], amounts[]) to increase recipients’ balances. The function is bounded by a maximum batch size for gas safety and rejects accrual that would violate collateralization.claim() to withdraw their own accrued balance. Claims are protected by reentrancy guards and are not blocked by pause (pause applies to accrual, not to withdrawals of already‑accrued balances).profitRecipient on successful accrual, reducing the amount of idle funds exposed in the vault.
Referral rewards are computed off-chain by the CAT service immediately after a position closes successfully (i.e., after on-chain winner payouts occur). A hard requirement is that accrual is performed only when the close transaction contains the WinnersPaid event.
Concretely, the bot parses the WinnersPaid event from the specific payout transaction receipt rather than scanning broad block ranges. This anchors the computation to an auditable on-chain artifact and eliminates ambiguity about whether payouts actually happened.
Let \( F_{\mathrm{prot}} \) denote the protocol fee emitted in WinnersPaid.protocolFee for a given position, and let \( B \) denote the participant count (for a filled position). The computation defines a per‑participant unit fee:
\[
f_{\mathrm{unit}} = \left\lfloor \frac{F_{\mathrm{prot}}}{B} \right\rfloor,
\]
with the remainder \( F_{\mathrm{prot}} \bmod B \) retained as protocol profit (and ultimately swept as excess).
For each participant \( p \), the bot determines a referrer \( R(p) \) from the on-chain registry (with optional caching in PostgreSQL for efficiency). If \( R(p) \) exists, the referrer reward for that participant is:
\[
\text{reward}(p) = \left\lfloor f_{\mathrm{unit}} \cdot \rho(R(p)) \right\rfloor,
\]
where \( \rho(\cdot) \in [0,1] \) is a configurable rate (default 10%) that may be overridden for VIP partners via an off-chain database field (users.personal_rate), guarded by admin authentication.
Rewards are aggregated by recipient and accrued to the vault via batched addRewards calls.
To prevent double accrual on restarts or retries, the bot enforces idempotency in the database by recording each position’s payout transaction hash in a dedicated table (referral_rewards_applied) and skipping if already applied.
As an additional safety check, the bot verifies that the total sum of all computed rewards does not exceed \( F_{\mathrm{prot}} \) before sending any on-chain accrual transaction.
Optional cashback trial. The same vault mechanism can be used for a limited-time cashback campaign: for eligible positions (a specific \( A \) value) and within a per-user trial window, the per‑participant unit fee is accrued directly to the participant (cashback) and the corresponding referrer reward is suppressed. Trial eligibility is evaluated using the block timestamp associated with the payout transaction and is tracked idempotently in the database to avoid double payment.
The referral program introduces an explicit trust boundary: a bot (or owner) is authorized to write accrual entries into the vault. However, the vault’s accounting design limits the damage of erroneous accrual attempts:
(i) accrual cannot exceed the vault’s available USDT (collateralization check),
(ii) user claims are pull-based and isolated per recipient,
(iii) administrative withdrawals are limited to excess above totalOwed, preventing the owner from withdrawing partner balances.
In this sense, the vault acts as a segregated ledger with on-chain enforceable solvency constraints.
IPP organizes capital flows into discrete positions. Each position is an autonomous contract specifying: a per‑participant deposit, a finite set of participants, a subset of participants who will receive a positive outcome, and a maximum temporal horizon. Participants join a position by contributing a fixed amount of stablecoin (USDT); when the position is sufficiently populated, an off‑chain oracle evaluates realized trading volumes for a selected trading pair and computes deterministic outcomes.
Formally, for each position one can define a finite set of participants \( P = \{1, \dots, B\} \), a deposit amount \( A > 0 \), and an integer \( C \) with \( 1 \leq C \leq B-1 \). The total pool is \( A \cdot B \). After observing an external market data stream and on‑chain join times, the mechanism partitions \( P \) into a positive‑outcome set of size \( C \) and a negative‑outcome set of size \( B-C \). Transfers are then executed from the negative‑outcome group to the positive‑outcome group, minus a fixed protocol fee.
Formally, let \( m \) be the oracle message containing \( (\text{volumes}, \text{winnerIndices}) \); on-chain settlement is a deterministic function of \( (\text{state}, m) \).
Each position is fully determined by four core parameters \( (A, B, C, T) \) and the choice of trading pair:
| Symbol | Meaning | Typical Range | Example |
|---|---|---|---|
| A | Deposit amount per participant (USDT) | 5 – 100,000 (step 5) | 100 |
| B | Total number of participants required | 2 – 200 | 10 |
| C | Number of positive‑outcome participants | 1 to B − 1 | 3 |
| T | Maximum lifetime of the position (safety horizon) | 24 hours (fixed) | 24 h |
The parameter T plays the role of a safety deadline rather than a holding requirement. Positions may fill and resolve faster than the maximum horizon depending on demand, oracle submission latency, and chain conditions; faster resolution is not guaranteed. The 24‑hour bound guarantees that capital cannot remain in a partially filled or non‑resolvable state indefinitely. If a position does not reach \( B \) participants or deterministic settlement cannot be achieved within \( T \), deposits are returned according to predefined rules.
For a fixed trading pair, the space of possible parameter quadruples is large: in the current configuration, there are approximately \( 3.98 \times 10^8 \) distinct combinations per pair. This allows for a rich spectrum of risk‑return profiles, from nearly symmetric allocations (\( C \approx B/2 \)) to highly selective ones (\( C \ll B \)).
IPP links outcomes to the realized trading volume of a chosen trading pair on a major exchange (currently Binance). For each participant, the mechanism considers a short interval immediately following the participant’s on‑chain entry and maps this interval to a unique, non‑zero volume value extracted from the external data stream.
Crucially, the external data are not used as a random number generator. They are market observables with known statistical structure: intraday seasonality, regime shifts, and responses to news can be studied and, in principle, modeled. The protocol treats these observables as inputs to a deterministic function, not as a source of entropy.
Let \( t_i \) denote the join timestamp of participant \( i \) and let \( V(\tau) \) denote the realized trading volume at time \( \tau \) for the chosen pair (with 1-second resolution from klines). For each participant \( i \), the mechanism searches forward from \( t_i \) over a finite window (up to MAX_SEARCH_SEC seconds, typically 300 seconds) to find the first volume value that is both strictly positive and whose integer representation has not yet been assigned to any previous participant. Denoting the original decimal volume by \( v_i^{\mathrm{dec}} \), the assigned integer volume is \[ v_i = \Big\lfloor v_i^{\mathrm{dec}} \cdot 10^6 \Big\rfloor, \] Under the published oracle algorithm, this construction yields \( v_i > 0 \) and (by design) \( v_i \neq v_j \) for \( i \neq j \), thereby eliminating ties *in the computed assignment*. The current on-chain contract checks positivity but does not enforce the full mapping/uniqueness constraints.
If no such unique, strictly positive volume can be found for some participant within the window, the position is declared unresolvable and deposits are refunded rather than forcing an artificial outcome. This guarantees that, whenever settlement occurs, the mapping from \(\{t_i\}\) and the external volume series \( V(\cdot) \) to the integer volumes \(\{v_i\}\) is well‑defined, strictly positive, and pairwise distinct.
Once each participant \( i \in P \) has an associated integer volume \( v_i \) computed by the oracle algorithm, the oracle constructs a ranking: an index array \([0, 1, \dots, B-1]\) sorted in strictly decreasing order of \( v_i \). Because all contract volumes \( v_i \) differ, this single key already defines a complete strict ranking of all participants, and no further tie‑breaking criteria are required at the level of the protocol description.
The first \( C \) indices in this sorted list form the positive‑outcome set. The oracle submits these indices as winnerIndices; the on-chain contract validates index uniqueness/range but does not recompute the ranking from submitted volumes. Let the total pool be
\[
\text{Pool} = A \cdot B, \quad
\text{LosersPool} = A \cdot (B - C).
\]
A fixed proportion of the negative‑outcome pool (currently 5%) is collected as a protocol fee, and the remainder is
redistributed among the positive‑outcome participants according to a predefined sharing rule (equal split up to integer division; remainder dust is assigned to the first winner).
At the level of transfers between participants, and abstracting from explicitly defined protocol and transaction fees and possible rounding effects, the aggregate profit of the positive‑outcome set equals the aggregate loss of the negative‑outcome set. In this sense, the mechanism realizes a zero‑sum transfer conditioned on the observed market volumes and participation times.
For any individual position, the maximum capital at risk for a participant is known ex ante and equal to the deposit \( A \), plus small fixed transactional fees associated with optional actions such as early exit and payout. There is no leverage, no margin, and no possibility of losses exceeding the initial position size. In the worst case, a participant loses at most \( A \) in a given position; there is no mechanism by which a position can expand or "grow" beyond its initial stake.
This differs from leveraged futures, where path‑dependent liquidations can generate losses disproportionate to the initial margin, and where the effective loss bound is a stochastic function of the price path, margin rules, and venue‑specific liquidation procedures.
Until a position is irreversibly locked for settlement, a participant may request to withdraw from the position:
The early‑exit option serves two distinct purposes:
1. Protection against "dead" configurations.
If a particular configuration \((A,B,C)\) fails to attract sufficient interest (e.g., a niche trading pair or an asymmetric risk profile that other participants find unappealing), a participant is not forced to keep capital immobilized indefinitely. The position can be abandoned before it reaches \( B \) entries or the safety deadline \( T \).
2. Correction of subjective misjudgment.
Participants may revise their view of the market, their own risk tolerance, or the attractiveness of a given position. Early exit allows them to fully reverse a decision that, in hindsight, they no longer endorse.
The mechanism thereby avoids immediate irreversibility at entry (a common feature of many gambling setups), because exposure remains under participant control until a well-defined settlement phase begins. Once the position is filled (\( B \) participants) and the settlement process is initiated, the risk profile and potential payoff for each participant are fixed.
The parameter \( T = 24 \) hours serves as a hard upper bound on the lifetime of a position:
In normal operation, positions may fill and resolve on timescales much shorter than \( T \), but this depends on participant demand, oracle polling configuration, exchange data availability, and blockchain confirmation times. The mapping from join times to volume values is often computable quickly once \( B \) is reached, but specific latencies are not guaranteed.
It is therefore important to distinguish:
The former is short and operational; the latter is a guardrail against infrastructure failures and forgotten positions.
Like any zero‑sum market mechanism, IPP admits heterogeneous participant types: some may have faster access to data, superior models of short‑horizon volume dynamics, or better execution infrastructure. The same is true, a fortiori, in futures markets, where high‑frequency traders and market‑makers exploit microstructure patterns and latency advantages.
These asymmetries are not unique to IPP and do not transform the protocol into a lottery (as defined in Section 1.1). They describe the competitive landscape within which strategies interact. IPP's contribution is that it makes the payoff function, risk bounds, and data dependence explicit and auditable, so that any analysis of strategic advantage can proceed from a fully specified mechanism rather than from undocumented exchange behaviour.
A structural feature of IPP is its dependence on trading volumes reported by external venues (e.g., centralized exchanges). To the extent that these venues are affected by manipulation patterns such as wash trading, spoofing, or artificial volume inflation, the resulting distortions will propagate into the ranking of participants and the distribution of outcomes.
The protocol does not and cannot certify that the external market is free of such behavior; it can only make its own transformation of publicly observable data explicit. For a typical participant without specialized models of short‑horizon volume dynamics, this means that outcomes at the level of individual positions will often be difficult to forecast ex ante, despite the formal determinism of the mechanism.
The interaction of intraday seasonality, news flow, and other agents' strategies generally produces a degree of effective unpredictability: in any given position there is a non‑trivial probability of losing the entire stake \( A \) (plus fees) while winners can realize a large upside. IPP does not present itself as a source of guaranteed or mechanically generated yield. For participants without a demonstrable informational or strategic edge, the symmetric baseline has non‑positive expected value once protocol and transaction fees are taken into account, in the same way that symmetric traders in fee‑bearing futures markets face negative expectation.
Any use of the mechanism should be based on explicit recognition of this risk profile and limited to capital that participants can afford to lose in a speculative, zero‑sum setting.
Let:
Then, for a fully filled position with no early exits:
1. Total pool:
\[ \text{Pool} = A B. \]
2. Pool of negative‑outcome participants:
\[ \text{LosersPool} = A (B - C). \]
3. Protocol fee:
\[ F_{\mathrm{prot}} = X \cdot \text{LosersPool}. \]
4. Pool available for redistribution to positive‑outcome participants:
\[ \text{WinnersPool} = \text{LosersPool} - F_{\mathrm{prot}}. \]
If the sharing rule is symmetric (for example, equal split among the \( C \) positive‑outcome participants), then the gross payout to a winner is approximately:
\[ \text{Payout} \approx A + \frac{\text{WinnersPool}}{C} - f_{\mathrm{pay}}. \]
The corresponding net profit for a winner in that position is
\[ \Pi_{\mathrm{win}} \approx \frac{A (B - C) (1 - X)}{C} - f_{\mathrm{pay}}, \]
while each loser incurs a net loss of approximately
\[ \Pi_{\mathrm{lose}} \approx -A - f_{\mathrm{tx,join}}, \]
where \( f_{\mathrm{tx,join}} \) collects fixed transactional costs associated with joining and (if applicable) optional exit and payout.
If all participants are ex ante symmetric (no informational or execution advantage) and the ranking induced by volume values is, from their point of view, effectively random, then each participant faces probability \( C/B \) of being in the positive‑outcome set and \( (B-C)/B \) of being in the negative‑outcome set. In this symmetric benchmark, the expected value of a single participation is:
\[ \mathbb{E}[\Pi] \approx \frac{C}{B} \cdot \Pi_{\mathrm{win}} + \frac{B - C}{B} \cdot \Pi_{\mathrm{lose}}. \]
Because \( F_{\mathrm{prot}} \) is a fixed fraction of the negative‑outcome pool, and fixed transaction fees are small relative to \( A \), this expected value is approximately equal to \(-X\) times the average stake, i.e., the fee-induced negative expectation is of order \( X \leq 5\% \) of the at‑risk capital. Unlike in many other markets, this edge is transparent, mechanically defined, and does not depend on hidden venue policies, funding flows, or proprietary risk engines.
As an illustrative example, consider the configuration
\[ A = 100,\quad B = 20,\quad C = 1,\quad X = 0.05,\quad f_{\mathrm{pay}} = 0.10\ \text{USDT}. \]
Then:
With \( C = 1 \), the unique winner receives approximately
\[ \text{Payout} \approx 100 + 1805 - 0.10 = 1904.90\ \text{USDT}, \]
yielding
\[ \Pi_{\mathrm{win}} \approx 1804.90\ \text{USDT},\quad \text{ROI} \approx 1804.9\%. \]
The ex ante probability of being the unique winner in a symmetric benchmark is \(1/20 = 5\%\), while the probability of losing the entire stake is \(95\%\). The expected profit per 100 USDT stake in this configuration is therefore approximately
\[ \mathbb{E}[\Pi] \approx 0.05 \cdot 1804.9 + 0.95 \cdot (-100) \approx -4.755\ \text{USDT}, \]
corresponding to an expected loss of about \(4.8\%\). This value is fully explained by the protocol fee and fixed payment cost; there is no additional hidden erosion due to venue‑specific mechanisms.
From a behavioural perspective, IPP differs from leveraged futures not only in its mathematical loss distribution but also in the way it exposes participants to psychological stress:
For some participants (especially those without professional trading infrastructure), this structure may reduce the need for continuous monitoring relative to liquidation-based products: instead of continuously managing margin and leverage, participants make discrete decisions about entry and stake \( A \). These effects are participant- and strategy-dependent and are not guaranteed.
This does not make IPP "safe" or "low risk" in any absolute sense: participants can still incur repeated losses, and the mechanism remains speculative and zero‑sum. It does, however, align the shape of risk more directly with human factors such as:
Because downside per position is fully specified by \( A \), the protocol effectively quantizes risk into discrete units that participants can align with their own self‑assessment. An individual who is prepared to lose, for example, 100 USDT on a speculative experiment can implement exactly that exposure by choosing \( A = 100 \) and participating in a single position, rather than constructing a leveraged futures portfolio whose effective exposure is unclear ex ante.
In this sense, IPP externalizes a significant portion of risk management from the venue's opaque risk engine into the participant's explicit choice of \( A \) and of which \((A,B,C)\) configurations to engage with. The mechanism does not prevent poor choices, but it makes the consequences of each choice mechanically bounded and analytically tractable.
This section provides a comprehensive comparison between IPP and leveraged crypto futures, highlighting structural differences in mechanics, risk profiles, operational characteristics, and participant experience. Understanding these differences is essential for participants to make informed decisions about which mechanism aligns with their risk tolerance, capital constraints, and strategic goals.
In both mechanisms, uncertainty originates from the behaviour of the same underlying market: order flow, news, liquidity regimes, and the interaction of other agents' strategies. Neither IPP nor futures injects exogenous randomness in the form of an internal random-number generator; both are deterministic transformations of market observables.
Consequently, any argument that labels IPP "lottery-like" (in the informal sense) solely because realized volumes are difficult to predict would, by parity of reasoning, label futures "lottery-like" because realized prices are difficult to predict. The difference lies not in the presence or absence of uncertainty, but in which market variable is used and how risk is bounded. For a formal definition of "lottery" and why IPP does not qualify, see Section 1.1.
Leverage and margin in futures. Perpetual futures allow participants to control notional exposures that far exceed their initial capital. Losses can be amplified by leverage, and path-dependent liquidations can crystallize large losses on short time scales. Effective loss bounds are stochastic and depend on:
In extreme cases, traders can lose their entire account balance or more relative to initial margin, even if the underlying asset does not undergo an extreme move in absolute terms.
Bounded loss in IPP. By contrast, IPP implements:
For a given position, the maximum loss per participant is bounded above by the stake \( A \) (plus small fixed fees). The loss distribution for a single participation is fully described by:
From a risk-theoretic standpoint, IPP is a fixed-stake, bounded-loss mechanism. Futures are variable-stake, path-dependent mechanisms in which effective risk can grow endogenously.
If one uses "lottery-like" in the broad informal sense (see Section 1.1), then leveraged futures — where both gains and losses can be leveraged and where liquidation cascades exist — fit that description at least as strongly as IPP.
Futures.
IPP.
Thus, in IPP each exposure is bounded in both amount and time; there is no concept of a "forever-open" position. In futures, risk can persist indefinitely, and the transition from "safe" to "catastrophic" can occur discontinuously via margin events at unpredictable times.
Centralized futures venues.
In such environments, the line between "platform", "market-maker", and "counterparty" can be blurred, and the effective payoff structure of a given contract can evolve over time as rules are adjusted.
IPP as a neutral mechanism.
Manipulation of underlying exchange volumes, where economically feasible, affects IPP only through the same data that any third party can observe and analyze ex post. The protocol does not hide, filter, or re-weight these volumes in an opaque manner. In practical terms:
For a detailed discussion of terminology and classification criteria, including formal and informal definitions of "lottery", see Section 1.1. Here we summarize the key points in the context of comparing IPP with futures:
Under the informal definition (any activity with uncertain outcomes, negative expected value, and concentration of gains in a small minority), many speculative derivatives — including leveraged futures — satisfy "lottery-like" phenomenology. IPP also exhibits this pattern when viewed through this lens.
Under the formal definition (mechanisms driven by explicit randomization devices independent of economic processes), both futures and IPP are not lotteries. Both are zero-sum derivative mechanisms with fees, in which outcomes are functions of market observables (prices or volumes), and participants can attempt to obtain a systematic advantage through information, modelling, or execution.
If one chooses to use "lottery" in the broad informal sense, consistency demands that the same label apply to futures as to IPP. If one uses it in the narrow formal sense (as defined in Section 1.1), neither qualifies. What distinguishes IPP from futures is not whether it is "lottery-like", but how transparently it encodes risk bounds, payoff functions, and the link to observable market data.
Leveraged futures trading typically places a high cognitive and emotional burden on non-professional participants:
This environment encourages:
IPP alters this pattern in several structural ways:
This does not make IPP psychologically neutral: losing the full stake \( A \) can still be emotionally impactful, especially if repeated. However, it reduces the specific stressors associated with leverage, liquidations, and 24/7 surveillance of the market. For many individuals, particularly those without professional trading infrastructure or risk-management tools, bounded, discrete exposures are easier to integrate into everyday life than open-ended leveraged positions.
Around futures markets, especially in retail-facing segments, a large ecosystem of:
has emerged. In many such setups, the revenue of signal providers comes primarily from selling access, not from trading performance. The economic incentives can therefore become decoupled from actual skill; "success stories" may serve more as marketing material than as evidence of systematic edge.
IPP, by design, does not embed or require such an ecosystem:
This does not prevent the emergence of low-quality or promotional commentary around IPP, but it reduces the structural dependence on opaque "gurus". The mechanism itself remains indifferent to narratives; it enforces only the payoff function encoded in its contracts.
Futures trading mechanics:
IPP mechanics:
These differences make IPP suitable for discrete, bounded-risk experiments but unsuitable for dynamic portfolio management or sophisticated hedging strategies that require continuous position adjustment.
Futures leverage and capital efficiency:
IPP capital efficiency:
IPP prioritizes risk clarity (knowing exactly how much can be lost) over capital amplification (controlling large notional positions with small capital). This trade-off makes IPP unsuitable for participants seeking maximum leverage or notional exposure, but suitable for those prioritizing bounded risk and transparent outcomes.
Futures market access:
IPP market access:
IPP's dependence on underlying market liquidity creates a natural selection mechanism: positions on illiquid pairs may fail to resolve, while positions on liquid pairs proceed normally. This protects participants from outcomes based on insufficient or manipulated data, but limits the range of trading pairs that can be reliably used.
From a structural and behavioural standpoint, IPP offers the following advantages relative to leveraged futures:
At the same time, IPP has clear limitations relative to futures:
IPP should therefore be understood not as a drop-in replacement for futures, but as a complementary derivative mechanism that prioritizes bounded loss, determinism, and verifiability over maximal flexibility and leverage. It is suitable for participants who value:
It is not suitable for participants who need:
A central property of IPP is determinism: given
there exists exactly one outcome consistent with the mechanism's rules. The protocol itself introduces no additional randomness and no discretionary choices.
For a fixed configuration \((A,B,C,T)\) and a chosen trading pair, the mechanism:
At each step, the rules are fixed and public. Given the same input data \((\{t_i\}, V(\cdot))\), any independent implementation of the algorithm will produce the same partition of participants and the same set of transfers, up to rounding effects.
Determinism has several direct consequences for auditability:
In practice, these properties are realized to the extent that:
Without these elements, IPP remains formally deterministic but less transparent to external observers.
IPP is intentionally and tightly linked to real markets:
This linkage has two complementary implications:
1. Alignment with real economic activity.
IPP does not fabricate an independent randomness source; it reuses the same data (volume) that drive trading and risk-management systems in the broader ecosystem.
2. Inheritance of external vulnerabilities.
The protocol inherits any flaws, pathologies, or manipulations in the underlying venue's volume reports:
IPP cannot, by itself, guarantee that the external market is "clean". It can only make its own transformation of observable data explicit and reproducible, so that any investigation of anomalies has a well-defined object: the external data and their mapping, not internal discretionary decisions.
Accordingly, part of IPP's practical risk surface lies outside the core contracts:
These are implementation and governance questions layered on top of a deterministic core.
The current implementation does not enforce correctness of oracle-submitted winners/volumes on-chain; verification is post-hoc.
In addition to determinism of outcomes, IPP is non-custodial by design:
As a consequence:
Generic smart-contract risks remain relevant:
These risks must be addressed through careful engineering, formal verification where possible, and independent audits. The contracts aim to eliminate discretionary admin withdrawal paths over user funds; however, oracle-key security and oracle correctness are still critical assumptions in the current design.
From the perspective of an external observer (whether human or machine), trust in IPP as a mechanism depends on more than its abstract specification:
When these conditions hold, the combination of:
produces a mechanism in which:
IPP can then be analysed and classified as a bounded-risk, market-linked derivative protocol whose residual risks are primarily those of its external dependencies and its users' strategic decisions, rather than of internal discretion or hidden randomness.
From a regulatory perspective, it is useful to distinguish IPP from two broad classes of financial products: (i) interest‑bearing or time‑based deposit accounts, in which a centralized intermediary accepts funds onto its balance sheet and promises a return over time, and (ii) lotteries and house‑bank gambling, in which a central operator designs payoffs, may act as the counterparty, and often controls or seeds the randomization device that determines outcomes.
IPP may be characterized as not operating interest‑bearing or savings‑type deposits under certain definitions. Participant funds are contributed as fixed‑size stakes into autonomous smart contracts representing specific positions; they are not lent out, re‑invested, or pooled into open‑ended balance‑sheet products. There is no promise of yield as a function of time held. Instead, capital is redistributed within each position exclusively according to the on‑chain payoff rules and observable market data, with bounded loss per position and a fixed safety horizon \(T\).
Likewise, IPP may be characterized as not operating a "house book" in the sense of a casino or dealer under certain definitions. The protocol never takes directional exposure against participants and does not choose outcomes; it only enforces a public, deterministic function of participants' actions and external volumes. To the extent that some participants achieve persistent positive P&L while others incur losses, this reflects competition in a zero‑sum environment with heterogeneous skill and information, not discretionary decisions by a central operator. Any supervisory assessment of IPP as a financial product should therefore focus on its nature as a non‑custodial, bounded‑risk, market‑linked derivative mechanism, rather than on analogies to deposit‑taking or operator‑driven gambling schemes.
The preceding sections describe structural risk in IPP: bounded loss per position, deterministic payoffs, explicit fee structure, and dependence on external market volumes. These properties are intrinsic to the protocol and do not change from one participant to another.
In contrast, behavioral risk arises from how individuals choose to interact with the mechanism:
IPP can reduce certain classes of structural and psychological stress relative to leveraged futures (e.g., margin calls, continuous PnL swings), but it does not remove the possibility that participants engage in impulsive or compulsive risk-taking. The protocol eliminates many avenues for technical or institutional abuse; it does not eliminate human error or problematic participation patterns.
Accordingly, risk assessment for IPP must separate:
A key behavioral distinction between IPP and leveraged futures lies in their temporal and experiential structure.
Continuous exposure in futures.
Discrete experiments in IPP.
From a behavioral viewpoint, IPP encourages a stepwise pattern: think → decide → accept bounded risk → observe outcome → reassess. This reduces the number of moments at which impulsive reactions can alter exposure. It does not guarantee rational behaviour, but it makes the decision points discrete and easier to isolate analytically.
IPP does not attempt to be a low-risk or conservative product; it is a zero-sum speculative mechanism. However, the shape of psychological load differs from that of leveraged futures:
For many individuals, this can lead to:
These effects are not guaranteed; participants can still choose to monitor markets obsessively or to over-allocate to IPP. The point is that the protocol's mechanics do not require continuous vigilance for basic risk control.
IPP is structurally aligned with participants who:
Examples of suitable goals include:
The protocol is particularly aligned with individuals who prefer to reason in terms of:
"How much am I willing to risk on this specific idea?"
rather than:
"How much leverage can I apply before liquidation becomes too likely?"
Conversely, IPP is not designed for:
Linear price hedging.
It does not provide a straightforward long/short exposure to asset prices and is not a drop-in replacement for futures used to hedge spot portfolios or corporate exposures.
Leveraged capital amplification.
It does not offer arbitrary leverage; capital at risk is limited to \( A \). Participants seeking to transform small balances into very large notional exposures via leverage will not find equivalent functionality in IPP.
Fine-grained intraday risk management.
There are no built-in stop-loss or take-profit orders, no partial closes, no reversals within a position. Positions are atomic; intrapositional risk management is intentionally minimal.
Stable yield or income generation.
IPP is a zero-sum system with fees. For a symmetric participant without structural edge, repeated participation has negative expected value. It is therefore unsuitable as a primary source of income or as a "yield product".
Recognizing these boundaries is essential: misuse of IPP as a substitute for instruments it is not designed to replace can lead to inappropriate expectations and disappointment.
Like any speculative mechanism with discrete wins and losses, IPP can become a vehicle for addictive behavior if participants:
Bounded loss per position does not imply bounded loss over time when stakes and frequency are uncontrolled. The protocol is neutral with respect to participant psychology; it does not enforce:
From a behavioral standpoint, responsible use requires self-imposed rules such as:
These constraints are external to the protocol but crucial to keeping speculative activity within tolerable bounds.
In IPP, the parameter \( A \) acts as a quantized risk unit. The participant chooses not only whether to engage, but at what level of potential loss per position. This introduces an explicit self-knowledge component:
The choice of \( A \) encodes an implicit answer to the question:
"What is the maximum loss on a single idea that I can accept without destabilizing my finances or behaviour?"
The choice of \((A,B,C)\) encodes a view on:
From the standpoint of a machine analyzing participant behaviour, IPP exposes a clearer mapping between:
This makes IPP a useful environment for studying how humans interact with bounded-risk mechanisms, where:
In summary, from a behavioural and practical perspective:
IPP is a bounded-risk, volume-linked derivative mechanism suitable for:
IPP is not a replacement for:
Used within these boundaries, IPP offers a distinctive combination of:
Invariant Parameters Protocol can be characterized as a deterministic, volume-based derivative mechanism with bounded downside per position and a fixed safety horizon. In the current implementation, settlement relies on inputs submitted by an authorized off-chain oracle (CAT); correctness relative to the published algorithm is therefore primarily a matter of transparency and post-hoc verification.
Compared with leveraged futures, IPP offers a simpler risk envelope at the position level: no leverage, no margin calls, and no path-dependent liquidations, with an explicit maximum stake \(A\) and a bounded time horizon \(T\). At the same time, outcomes remain linked to real market data (external volumes) and thus inherit both statistical unpredictability and external-dependency risks.
These properties make IPP suitable as an object of academic study in mechanism design, and as a candidate primitive for transparent, bounded-risk on-chain derivatives—provided oracle assumptions, data availability, and governance constraints are stated explicitly.
An IPP position on a given trading pair can be modeled as a finite game \[ \mathcal{G}_{\mathrm{IPP}}(A,B,C,T) = \langle P, \mathcal{S}, \mathcal{O}, u \rangle, \] where:
For a single, fully filled position without early exits, the payoff function can be written as:
where \(A\) is the deposit, \(f_{\mathrm{tx}}\) is the sum of fixed transactional fees incurred by the participant (join, optional exit, payout), and \(\pi_i\) is the share of the redistributable pool assigned to participant \(i\) according to the deterministic ranking on integer volumes \(\{v_i\}\). The mapping \((V(\cdot), \{t_i\}) \mapsto \{v_i\} \mapsto \pi_i\) is fully specified and deterministic.
At the level of transfers between participants, abstracting from fees and rounding, one has \(\sum_i u_i = 0\). Including protocol and transaction fees \(\sum_i u_i = -F\), where \(F\) is the total fee collected in the position. This is formally identical to fee‑bearing futures markets, where the net sum of trader P&L equals the negative of aggregate fees.
A leveraged futures position on an underlying asset with price process \(S_t\) can be modeled as a game \(\mathcal{G}_{\mathrm{Fut}}\) with:
Ignoring funding and fees for a moment, the terminal P&L of a single directional position can be approximated as \[ \mathrm{P\&L}_{\mathrm{fut}} \approx \ell \cdot (S_{T'} - S_{t_0}), \] where \(\ell\) is effective leveraged exposure, \(t_0\) is entry time, and \(T'\) is the random exit or liquidation time. In reality, transaction fees, spreads, funding payments, and forced liquidations make the mapping from \(\{S_t\}\) to trader P&L path‑dependent and sensitive to exchange‑specific rules.
As in IPP, the aggregate P&L of all traders in a futures market, net of exchange fees and spreads captured by liquidity providers, sums to a negative amount equal to total fees. Uncertainty in both settings arises from market dynamics; neither mechanism is driven by exogenous random draws in the lottery sense.
Consider again an IPP position with parameters
\[ A = 100,\quad B = 20,\quad C = 1,\quad X = 0.05,\quad f_{\mathrm{pay}} = 0.10. \]
As shown in Section 10.7, the unique winner receives approximately 1904.90 USDT, corresponding to a net profit
\[ \Pi_{\mathrm{win}} \approx 1804.90\ \text{USDT}, \]
while each loser incurs a loss of \(-100\) USDT.
In the symmetric benchmark where each participant has probability \(1/20\) of being the winner, the expected profit for a single 100 USDT stake is:
\[ \mathbb{E}[\Pi] = \frac{1}{20} \cdot 1804.90 + \frac{19}{20} \cdot (-100) \approx -4.755\ \text{USDT}. \]
This calculation makes explicit that:
In this sense, IPP delivers, in a formally transparent way, a profile that is often only implicitly present in leveraged futures trading: a small minority of large winners and a majority of net losers, with the aggregate shortfall accounted for by fees. The difference is that in IPP this profile is governed by explicit, immutable formulas rather than by a complex interaction of leverage, liquidation rules, and venue-specific microstructure.
Consider a “symmetric” participant with no informational or execution advantage over the population, who selects entries randomly among admissible times and positions and does not condition on private signals. In such a case, both IPP and futures markets share a structural property: the expected value of a marginal trade for this participant is non‑positive once fees are taken into account.
In IPP, conditioning on a fully filled position, let \(\mathbb{P}(i \in \text{positive})\) denote the probability that a randomly drawn participant belongs to the positive‑outcome set under some prior over join times and volume scenarios. Then the expected payoff of a symmetric participant can be written as:
\mathbb{E}[u_i]
= \mathbb{P}(i \in \text{positive}) \cdot \mathbb{E}[A + \pi_i - f_{\mathrm{tx}} \mid i \in \text{positive}]
+ \mathbb{P}(i \in \text{negative}) \cdot \mathbb{E}[-A - f_{\mathrm{tx}} \mid i \in \text{negative}].
Because the net transfers between participants sum to \(-F\) (total fees), the aggregation of \(\mathbb{E}[u_i]\) across all participants equals \(-F\). For a symmetric participant, this implies \(\mathbb{E}[u_i] \leq 0\), with strict inequality whenever fees are strictly positive. This is not a property of “lottery‑likeness”, but a direct consequence of operating in a fee‑bearing zero‑sum mechanism.
In leveraged futures, under a symmetric prior and ignoring funding asymmetries, the expected P&L of a marginal trade before fees is approximately zero in a frictionless model, but after accounting for trading fees, spreads, and other transaction costs it becomes strictly negative. Empirical studies and exchange statistics consistently report that a small minority of traders capture most of the positive P&L, while the majority experience net losses once costs are included. This pattern is entirely consistent with a zero‑sum game with fees and does not, by itself, distinguish futures from IPP.
A salient difference between IPP and futures markets lies not in the presence or absence of uncertainty, but in the transparency of the risk profile at the moment of entry. In IPP, a participant who joins a position with parameters \( (A,B,C,T) \) knows ex ante that:
In leveraged futures, by contrast, the effective risk at entry depends on a combination of leverage, margin rules, liquidation thresholds, volatility regimes, and potential rule changes by the exchange. While traders can compute nominal liquidation levels under current conditions, the true tail risk — including gaps, cascading liquidations, and forced deleveraging — is difficult to quantify and is often underestimated in practice. From the participant’s perspective, the mapping from “I opened a position of size X with leverage L” to “my maximum possible loss” is therefore considerably less transparent than in IPP.
Finally, in IPP participants have full discretion over whether to join any given position in light of perceived manipulation risks or liquidity conditions of the underlying pair. Positions on illiquid or easily manipulated pairs can simply be ignored. This freedom to condition participation on an ex ante assessment of risk and market quality, together with bounded loss and a fixed safety horizon, differentiates IPP from many existing derivatives venues where complex and adaptive infrastructure makes effective risk assessment by non‑experts substantially harder.
Empirical studies of speculative trading in futures and other derivatives commonly report that a relatively small fraction of participants — on the order of a few percent — accounts for a large share of positive P&L, while the vast majority either breaks even before costs or incurs net losses once transaction costs are included. This pattern is often summarized colloquially as “2–5% of traders consistently win, 95% lose” and is frequently cited in discussions of speculatively oriented markets.
From the standpoint of game theory, such concentration of edge is a natural consequence of three facts:
In a zero‑sum setting with heterogeneous skill, it is mathematically impossible for a majority of participants to achieve above‑average returns simultaneously; any persistent edge must, by definition, be concentrated in a minority. This is true whether the underlying game is built on prices (as in futures) or on volumes (as in IPP). Observing that only a small subset of agents systematically outperforms therefore does not distinguish a “lottery” from a competitive market; it simply reflects the basic geometry of zero‑sum competition under heterogeneous capabilities.
In IPP, as in futures, it is reasonable to expect that a small set of participants with better models of volume dynamics, faster data, or more disciplined risk management will achieve positive long‑run P&L, while many casual participants will not. This empirical asymmetry says little about the underlying mechanism and much about the distribution of skill and discipline in the population. The structural questions remain: how clearly are risk and payoff specified, how bounded are losses, and how neutral is the protocol itself with respect to participant strategies. On these axes, IPP is designed to make risk and rules as explicit and verifiable as possible.
The complete source code of IPP smart contracts is provided below for full transparency and independent auditability. Both contracts are verified on PolygonScan and can be independently verified against the deployed bytecode.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";
interface IPosition {
function initialize(
address _usdt,
uint256 _A,
uint256 _B,
uint256 _C,
address _base,
bytes32 _symbol,
uint256 _deadline
) external;
// old join (no insurance) — MUST remain
function joinFromBaseFunded(address participant) external;
// new join (with insurance premium)
function joinFromBaseFunded(address participant, uint256 insurancePremium) external;
// insurance premium (R = A*(B-C)/B)
function insurancePremium() external view returns (uint256);
function rescueToken(address token) external;
function isOpen() external view returns (bool);
}
/**
* @title Base
* @notice Position factory and global protocol parameters storage.
*
* No on-chain stopping/decommissioning. If emergency happens, UI can be disabled off-chain.
*/
contract Base {
using SafeERC20 for IERC20;
// Limits (synced with Position)
uint256 public constant MIN_A = 5_000_000; // 5 USDT (6 decimals) + step = 5 USDT
uint256 public constant MAX_A = 100_000_000_000; // 100000 USDT (6 decimals)
uint256 public constant MIN_B = 2;
uint256 public constant MAX_B = 200;
uint256 public constant MAX_FEE_BPS = 500; // 5%
uint256 public constant MAX_PAYMENT_FEE = 100_000; // 0.1 USDT
// Hardcoded ALLOWED_USDT (Polygon USDT)
address public constant ALLOWED_USDT = 0xc2132D05D31c914a87C6611C10748AEb04B58e8F;
// Position duration (T = 24 hours)
uint256 public constant POSITION_DURATION = 24 hours;
address public owner;
address public feeWallet;
address public catWallet;
address public authorizedCat; // affects only NEW positions (Position fixes CAT at init)
address public immutable implementation;
uint256 public feeBps; // only down
uint256 public paymentFeeUSDT; // only down
uint256 public totalPositionsCreated;
// Open position by key (usdt, A, B, C, symbol)
mapping(bytes32 => address) public openPositionByKey;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event FeeWalletSet(address indexed feeWallet);
event CatWalletSet(address indexed catWallet);
event AuthorizedCatSet(address indexed authorizedCat);
event FeeBpsSet(uint256 feeBps);
event PaymentFeeUSDTSet(uint256 paymentFeeUSDT);
event PositionCreated(
address indexed position,
bytes32 indexed symbol,
uint256 A,
uint256 B,
uint256 C,
uint256 createdAt,
uint256 deadline
);
event PositionJoined(
address indexed position,
address indexed participant,
uint256 A,
bool isCreator,
uint256 joinedAt
);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor(
address _owner,
address _feeWallet,
address _catWallet,
address _authorizedCat,
address _implementation
) {
require(_owner != address(0), "Invalid owner");
require(_feeWallet != address(0), "Invalid fee wallet");
require(_catWallet != address(0), "Invalid cat wallet");
require(_authorizedCat != address(0), "Invalid cat");
require(_implementation != address(0) && _implementation.code.length > 0, "Bad implementation");
owner = _owner;
feeWallet = _feeWallet;
catWallet = _catWallet;
authorizedCat = _authorizedCat;
implementation = _implementation;
feeBps = MAX_FEE_BPS;
paymentFeeUSDT = MAX_PAYMENT_FEE;
emit OwnershipTransferred(address(0), _owner);
emit FeeWalletSet(_feeWallet);
emit CatWalletSet(_catWallet);
emit AuthorizedCatSet(_authorizedCat);
emit FeeBpsSet(feeBps);
emit PaymentFeeUSDTSet(paymentFeeUSDT);
}
function allowedUsdt() external pure returns (address) {
return ALLOWED_USDT;
}
function _positionKey(
address usdt,
uint256 A,
uint256 B,
uint256 C,
bytes32 symbol
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(usdt, A, B, C, symbol));
}
function getOpenPosition(
address usdt,
uint256 A,
uint256 B,
uint256 C,
bytes32 symbol
) external view returns (address) {
bytes32 key = _positionKey(usdt, A, B, C, symbol);
return openPositionByKey[key];
}
// ===== Admin =====
function setOwner(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Invalid owner");
emit OwnershipTransferred(owner, _newOwner);
owner = _newOwner;
}
function setFeeWallet(address _feeWallet) external onlyOwner {
require(_feeWallet != address(0), "Invalid fee wallet");
feeWallet = _feeWallet;
emit FeeWalletSet(_feeWallet);
}
function setCatWallet(address _catWallet) external onlyOwner {
require(_catWallet != address(0), "Invalid cat wallet");
catWallet = _catWallet;
emit CatWalletSet(_catWallet);
}
function setAuthorizedCat(address _authorizedCat) external onlyOwner {
require(_authorizedCat != address(0), "Invalid cat");
authorizedCat = _authorizedCat;
emit AuthorizedCatSet(_authorizedCat);
}
function setFeeBps(uint256 _feeBps) external onlyOwner {
require(_feeBps <= feeBps, "Cannot increase fee");
require(_feeBps <= MAX_FEE_BPS, "Fee too high");
feeBps = _feeBps;
emit FeeBpsSet(_feeBps);
}
function setPaymentFeeUSDT(uint256 _paymentFeeUSDT) external onlyOwner {
require(_paymentFeeUSDT <= paymentFeeUSDT, "Cannot increase fee");
require(_paymentFeeUSDT <= MAX_PAYMENT_FEE, "Fee too high");
paymentFeeUSDT = _paymentFeeUSDT;
emit PaymentFeeUSDTSet(_paymentFeeUSDT);
}
// ===== Internal: create or get open position =====
function _createOrGetOpenPosition(
address usdt,
uint256 A,
uint256 B,
uint256 C,
bytes32 symbol
) internal returns (address position, bool isNew) {
require(usdt == ALLOWED_USDT, "Only USDT");
require(implementation.code.length > 0, "No implementation");
require(feeWallet != address(0), "Fee wallet not set");
require(catWallet != address(0), "Cat wallet not set");
require(authorizedCat != address(0), "Cat not set");
require(A >= MIN_A && A <= MAX_A && A % MIN_A == 0, "Bad A");
require(A > paymentFeeUSDT, "A <= payment fee");
require(B >= MIN_B && B <= MAX_B, "Bad B");
require(C >= 1 && C < B, "Bad C");
bytes32 key = _positionKey(usdt, A, B, C, symbol);
address current = openPositionByKey[key];
if (current != address(0)) {
bool open = false;
// (2) Harden: if current has no code OR isOpen() reverts => treat as closed and clear mapping
if (current.code.length > 0) {
try IPosition(current).isOpen() returns (bool v) {
open = v;
} catch {
open = false;
}
}
if (open) {
return (current, false);
} else {
openPositionByKey[key] = address(0);
}
}
totalPositionsCreated++;
bytes32 salt = keccak256(abi.encodePacked(totalPositionsCreated, A, B, C, symbol));
position = Clones.cloneDeterministic(implementation, salt);
uint256 createdAt = block.timestamp;
uint256 deadline = createdAt + POSITION_DURATION;
IPosition(position).initialize(ALLOWED_USDT, A, B, C, address(this), symbol, deadline);
openPositionByKey[key] = position;
emit PositionCreated(position, symbol, A, B, C, createdAt, deadline);
isNew = true;
}
// ===== OLD PATH — unchanged =====
function createPositionAndJoin(
address usdt,
uint256 A,
uint256 B,
uint256 C,
bytes32 symbol
) external returns (address position) {
bool isNew;
(position, isNew) = _createOrGetOpenPosition(usdt, A, B, C, symbol);
IERC20 token = IERC20(ALLOWED_USDT);
uint256 beforeBal = token.balanceOf(position);
token.safeTransferFrom(msg.sender, position, A);
uint256 delta = token.balanceOf(position) - beforeBal;
require(delta == A, "Bad token: net < A");
IPosition(position).joinFromBaseFunded(msg.sender);
emit PositionJoined(position, msg.sender, A, isNew, block.timestamp);
}
// ===== NEW: optional insurance =====
function createPositionAndJoinWithInsurance(
address usdt,
uint256 A,
uint256 B,
uint256 C,
bytes32 symbol,
bool insure
) external returns (address position) {
bool isNew;
(position, isNew) = _createOrGetOpenPosition(usdt, A, B, C, symbol);
IERC20 token = IERC20(ALLOWED_USDT);
uint256 premium = 0;
if (insure) {
premium = IPosition(position).insurancePremium();
require(premium > paymentFeeUSDT, "Premium <= payment fee");
}
uint256 total = A + premium;
uint256 beforeBal = token.balanceOf(position);
token.safeTransferFrom(msg.sender, position, total);
uint256 delta = token.balanceOf(position) - beforeBal;
require(delta == total, "Bad token: net < total");
if (premium == 0) {
IPosition(position).joinFromBaseFunded(msg.sender);
} else {
IPosition(position).joinFromBaseFunded(msg.sender, premium);
}
emit PositionJoined(position, msg.sender, A, isNew, block.timestamp);
}
// ===== Callbacks / Rescue =====
function onPositionClosed(
address usdt,
uint256 A,
uint256 B,
uint256 C,
bytes32 symbol
) external {
bytes32 key = _positionKey(usdt, A, B, C, symbol);
if (openPositionByKey[key] == msg.sender) {
openPositionByKey[key] = address(0);
}
}
function rescueTokenFromPosition(address position, address token) external onlyOwner {
IPosition(position).rescueToken(token);
}
receive() external payable { revert("No ETH"); }
fallback() external payable { revert("No ETH"); }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IBase {
function feeWallet() external view returns (address);
function catWallet() external view returns (address);
function authorizedCat() external view returns (address);
function feeBps() external view returns (uint256);
function paymentFeeUSDT() external view returns (uint256);
function allowedUsdt() external view returns (address);
// Callback from position to base
function onPositionClosed(
address usdt,
uint256 A,
uint256 B,
uint256 C,
bytes32 symbol
) external;
}
struct Participant {
address addr;
uint64 joinTimestamp;
uint64 volumeTimestamp;
uint256 volume;
}
/**
* @dev All position data for reading in a single request.
*/
struct PositionData {
// Basic parameters
address usdt;
address base;
bytes32 symbol;
uint256 A;
uint256 B;
uint256 C;
uint256 createdAt;
uint256 deadline;
// Status
bool initialized;
bool closed;
bool volumesSubmitted;
string closeReason;
// Timestamps
uint256 firstJoinTimestamp;
uint256 positionFullTimestamp;
// Participants
address[] participants;
uint256[] joinTimestamps;
uint256[] volumeTimestamps;
uint256[] volumes;
// Balance
uint256 usdtBalance;
// Check status
bool shouldFinalize;
string statusMessage;
}
/**
* @title Position
* @notice OLD deposit economy preserved 1:1. Added optional insurance.
*
* CAT is FIXED at initialization (variant A).
*
* Insurance rules:
* - Premium R is fixed per position: R = A*(B-C)/B
* - Premium is independent of how many users buy it.
* - Winner payouts (from A) are unchanged.
* - Insurance payouts are separate:
* - If there are insured losers: pool is split among them equally, each pays paymentFee.
* - If there are NO insured losers: each insured participant gets premium back minus paymentFee.
* - Early leave / refund scenarios return A(+premium) minus paymentFee.
*/
contract Position is ReentrancyGuard {
using SafeERC20 for IERC20;
uint256 public constant MIN_A = 5_000_000;
uint256 public constant MAX_A = 100_000_000_000;
uint256 public constant MIN_B = 2;
uint256 public constant MAX_B = 200;
uint256 public constant MAX_FEE_BPS = 500;
uint256 public constant MAX_PAYMENT_FEE = 100_000;
uint256 public constant POSITION_DURATION = 24 hours;
IERC20 public usdt;
address public base;
bytes32 public symbol;
uint256 public A;
uint256 public B;
uint256 public C;
uint64 public createdAt;
uint64 public deadline;
uint64 public firstJoinTimestamp;
uint64 public positionFullTimestamp;
Participant[] public participants;
mapping(address => uint256) public participantIndex; // index+1, 0 = not participating
bool public initialized;
bool public closed;
bool public volumesSubmitted;
string public closeReason;
// ===== Fixed CAT (variant A) =====
address public authorizedCatFixed;
// ===== Insurance =====
mapping(address => uint256) public insurancePremiumPaid; // 0 => not insured
uint256 public insurancePool; // sum of premiums locked (gross)
uint256 public insuredCount; // number of insured participants
event Initialized(
uint256 A,
uint256 B,
uint256 C,
uint256 createdAt,
uint256 deadline,
address base,
bytes32 symbol
);
event ParticipantJoined(
address indexed participant,
uint256 index,
uint256 totalParticipants,
uint256 joinTimestamp
);
event ParticipantLeft(
address indexed participant,
uint256 refundedAmount,
uint256 feeAmount
);
event PositionFull(uint256 timestamp, uint256 totalParticipants);
event PositionClosed(uint256 timestamp, string reason);
event VolumesSubmitted(address indexed cat);
event WinnersPaid(
address[] winners,
uint256[] prizes,
uint256 protocolFee,
uint256 totalPaymentFee
);
// New finalization event (by deadline / volume error)
event Finalize(address indexed caller, string reason);
event AuthorizedCatFixed(address indexed cat);
event InsuranceLocked(address indexed participant, uint256 premium);
event InsurancePayout(address indexed participant, uint256 gross, uint256 net, uint256 paymentFee);
event InsurancePremiumReturned(address indexed participant, uint256 gross, uint256 net, uint256 paymentFee);
event InsuranceSettled(
uint256 poolGross,
uint256 insuredParticipants,
uint256 insuredLosers,
uint256 perLoserGross,
uint256 paymentFeeToCat,
uint256 dustLeftInContract
);
modifier onlyBase() {
require(msg.sender == base, "Not base");
_;
}
modifier onlyCat() {
require(msg.sender == authorizedCatFixed, "Not cat");
_;
}
function initialize(
address _usdt,
uint256 _A,
uint256 _B,
uint256 _C,
address _base,
bytes32 _symbol,
uint256 _deadline
) external {
require(!initialized, "Already initialized");
initialized = true;
require(_base != address(0), "Invalid base");
require(msg.sender == _base, "Only base can init");
base = _base;
// FIX CAT at init (variant A)
address catNow = IBase(base).authorizedCat();
require(catNow != address(0), "Cat not set");
authorizedCatFixed = catNow;
emit AuthorizedCatFixed(catNow);
require(_usdt != address(0), "Invalid USDT");
require(_usdt == IBase(base).allowedUsdt(), "Bad USDT");
usdt = IERC20(_usdt);
symbol = _symbol;
require(_A >= MIN_A && _A <= MAX_A && _A % MIN_A == 0, "Bad A");
require(_B >= MIN_B && _B <= MAX_B, "Bad B");
require(_C >= 1 && _C < _B, "Bad C");
uint256 feeBps_ = IBase(base).feeBps();
require(feeBps_ <= MAX_FEE_BPS, "FeeBps too high");
uint256 paymentFee = IBase(base).paymentFeeUSDT();
require(paymentFee <= MAX_PAYMENT_FEE, "Payment fee too high");
require(_A > paymentFee, "A <= payment fee");
require(IBase(base).feeWallet() != address(0), "Fee wallet not set");
require(IBase(base).catWallet() != address(0), "Cat wallet not set");
A = _A;
B = _B;
C = _C;
createdAt = uint64(block.timestamp);
uint256 expectedDeadline = createdAt + POSITION_DURATION;
require(_deadline == expectedDeadline, "Bad deadline");
deadline = uint64(_deadline);
emit Initialized(A, B, C, createdAt, deadline, base, symbol);
}
/// @notice R = A*(B-C)/B
function insurancePremium() public view returns (uint256) {
return (A * (B - C)) / B;
}
/// @notice Open strictly BEFORE deadline (at exact deadline => closed for joining)
function isOpen() external view returns (bool) {
if (!initialized || closed) return false;
if (block.timestamp >= deadline) return false;
if (participants.length >= B) return false;
return true;
}
/**
* @notice Returns all position data in one request, including check status.
*/
function getAllData() external view returns (PositionData memory data) {
uint256 count = participants.length;
// Basic parameters
data.usdt = address(usdt);
data.base = base;
data.symbol = symbol;
data.A = A;
data.B = B;
data.C = C;
data.createdAt = uint256(createdAt);
data.deadline = uint256(deadline);
// Status
data.initialized = initialized;
data.closed = closed;
data.volumesSubmitted = volumesSubmitted;
data.closeReason = closeReason;
// Timestamps
data.firstJoinTimestamp = uint256(firstJoinTimestamp);
data.positionFullTimestamp = uint256(positionFullTimestamp);
// Participants
data.participants = new address[](count);
data.joinTimestamps = new uint256[](count);
data.volumeTimestamps = new uint256[](count);
data.volumes = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
Participant memory p = participants[i];
data.participants[i] = p.addr;
data.joinTimestamps[i] = uint256(p.joinTimestamp);
data.volumeTimestamps[i] = uint256(p.volumeTimestamp);
data.volumes[i] = p.volume;
}
// USDT Balance
data.usdtBalance = usdt.balanceOf(address(this));
// Check status (logic from statusCheck)
if (!initialized) {
data.shouldFinalize = false;
data.statusMessage = "Not initialized";
} else if (closed) {
data.shouldFinalize = false;
data.statusMessage = "Already closed";
} else {
uint256 nowTs = block.timestamp;
if (nowTs < deadline) {
if (!volumesSubmitted) {
if (count < B) {
data.shouldFinalize = false;
data.statusMessage = "Waiting: collecting participants";
} else if (count == B) {
data.shouldFinalize = false;
data.statusMessage = "Waiting: volumes from cat";
} else {
data.shouldFinalize = false;
data.statusMessage = "Invalid state: participants > B";
}
} else {
data.shouldFinalize = false;
data.statusMessage = "Volumes submitted (no finalize needed)";
}
} else {
// nowTs >= deadline
if (volumesSubmitted) {
data.shouldFinalize = false;
data.statusMessage = "Volumes submitted (no finalize needed)";
} else {
data.shouldFinalize = true;
data.statusMessage = "Timeout: 24h exceeded, should finalize";
}
}
}
}
// ===== Join =====
// Public join() removed. Only possible via Base -> joinFromBaseFunded.
function joinFromBaseFunded(address participant)
external
nonReentrant
onlyBase
{
_joinFromBaseFunded(participant, 0);
}
function joinFromBaseFunded(address participant, uint256 premium)
external
nonReentrant
onlyBase
{
_joinFromBaseFunded(participant, premium);
}
function _joinFromBaseFunded(address participant, uint256 premium) internal {
require(!closed, "Closed");
require(participant != address(0), "Invalid participant");
require(block.timestamp < deadline, "After deadline");
uint256 count = participants.length;
require(count < B, "Position full");
require(participantIndex[participant] == 0, "Already joined");
if (premium > 0) {
uint256 expected = insurancePremium();
require(premium == expected, "Bad premium");
uint256 paymentFee = IBase(base).paymentFeeUSDT();
require(paymentFee <= MAX_PAYMENT_FEE, "Payment fee too high");
require(premium > paymentFee, "Premium <= payment fee");
}
// Tolerate donations: only require "at least" expected balance
uint256 requiredBalance = (A * (count + 1)) + insurancePool + premium;
require(usdt.balanceOf(address(this)) >= requiredBalance, "Prefund missing");
_addParticipant(participant);
if (premium > 0) {
insurancePremiumPaid[participant] = premium;
insurancePool += premium;
insuredCount += 1;
emit InsuranceLocked(participant, premium);
}
}
function _addParticipant(address participant) internal {
uint64 ts = uint64(block.timestamp);
if (firstJoinTimestamp == 0) {
firstJoinTimestamp = ts;
}
participants.push(
Participant({
addr: participant,
joinTimestamp: ts,
volumeTimestamp: 0,
volume: 0
})
);
uint256 idx1 = participants.length; // 1-based
participantIndex[participant] = idx1;
emit ParticipantJoined(participant, idx1 - 1, participants.length, ts);
if (participants.length == B) {
positionFullTimestamp = uint64(block.timestamp);
emit PositionFull(positionFullTimestamp, participants.length);
}
}
// ===== Leave before full =====
function leavePosition() external nonReentrant {
require(!closed, "Closed");
require(block.timestamp < deadline, "After deadline");
uint256 idx1 = participantIndex[msg.sender];
require(idx1 != 0, "Not joined");
require(participants.length < B, "Position full");
uint256 idx = idx1 - 1;
uint256 lastIdx = participants.length - 1;
if (idx != lastIdx) {
Participant memory last = participants[lastIdx];
participants[idx] = last;
participantIndex[last.addr] = idx + 1;
}
participants.pop();
delete participantIndex[msg.sender];
uint256 paymentFee = IBase(base).paymentFeeUSDT();
require(paymentFee <= MAX_PAYMENT_FEE, "Payment fee too high");
require(A > paymentFee, "A <= fee");
uint256 prem = insurancePremiumPaid[msg.sender];
if (prem > 0) {
delete insurancePremiumPaid[msg.sender];
insurancePool -= prem;
insuredCount -= 1;
}
uint256 gross = A + prem;
require(gross > paymentFee, "Refund <= fee");
uint256 refundAmount = gross - paymentFee;
if (refundAmount > 0) usdt.safeTransfer(msg.sender, refundAmount);
if (paymentFee > 0) usdt.safeTransfer(IBase(base).catWallet(), paymentFee);
emit ParticipantLeft(msg.sender, refundAmount, paymentFee);
if (participants.length == 0) {
closed = true;
closeReason = "All participants left";
emit PositionClosed(block.timestamp, closeReason);
IBase(base).onPositionClosed(address(usdt), A, B, C, symbol);
}
}
// ===== Submit results from CAT =====
/**
* @notice CAT submits volumes and winners list.
* volumes[i] — volume for i-th participant in participants[i].
* winnerIndices[j] — index of winner (0..B-1).
*/
function submitResults(
uint256[] calldata volumes,
uint256[] calldata winnerIndices
) external nonReentrant onlyCat {
require(!closed, "Closed");
require(!volumesSubmitted, "Already submitted");
require(block.timestamp <= deadline, "After deadline");
uint256 count = participants.length;
require(count == B, "Not full");
require(volumes.length == count, "Wrong volumes length");
require(winnerIndices.length == C, "Wrong winners length");
uint64 ts = uint64(block.timestamp);
// 1) Record volumes
for (uint256 i = 0; i < count; i++) {
uint256 v = volumes[i];
require(v > 0, "Zero volume");
participants[i].volume = v;
participants[i].volumeTimestamp = ts;
}
// 2) Verify winner indices and form winners array
bool[] memory used = new bool[](count);
bool[] memory isWinner = new bool[](count);
address[] memory winners = new address[](C);
for (uint256 j = 0; j < C; j++) {
uint256 idx = winnerIndices[j];
require(idx < count, "Winner index out of range");
require(!used[idx], "Winner index duplicate");
used[idx] = true;
isWinner[idx] = true;
winners[j] = participants[idx].addr;
}
volumesSubmitted = true;
emit VolumesSubmitted(msg.sender);
_payoutWinners(winners, isWinner, "Resolved by CAT");
}
// ===== Finalize by timeout T=24h =====
function finalize() external nonReentrant {
if (closed) {
return;
}
// Nothing to finalize before deadline
if (block.timestamp < deadline) {
return;
}
// If volumes already submitted (and position should be closed) — do nothing
if (volumesSubmitted) {
return;
}
// 24h passed, no volumes — always refund all
emit Finalize(msg.sender, "Timeout: 24h exceeded");
_refundAll("Timeout: 24h exceeded");
}
/**
* @notice Explicit refund by CAT if volumes cannot be calculated correctly.
*/
function refundByVolumeError() external nonReentrant onlyCat {
require(!closed, "Closed");
require(!volumesSubmitted, "Already submitted");
emit Finalize(msg.sender, "Volume error by CAT");
_refundAll("Volume error by CAT");
}
// ===== Internal: Refund =====
function _refundAll(string memory reason) internal {
require(!closed, "Already closed");
closed = true;
closeReason = reason;
uint256 count = participants.length;
uint256 paymentFee = IBase(base).paymentFeeUSDT();
require(paymentFee <= MAX_PAYMENT_FEE, "Payment fee too high");
require(A > paymentFee, "A <= fee");
address catWallet = IBase(base).catWallet();
address feeWallet = IBase(base).feeWallet();
for (uint256 i = 0; i < count; i++) {
address p = participants[i].addr;
if (p == address(0)) continue;
uint256 prem = insurancePremiumPaid[p];
if (prem > 0) {
delete insurancePremiumPaid[p];
}
uint256 gross = A + prem;
require(gross > paymentFee, "Refund <= fee");
uint256 net = gross - paymentFee;
if (net > 0) usdt.safeTransfer(p, net);
if (paymentFee > 0) usdt.safeTransfer(catWallet, paymentFee);
}
insurancePool = 0;
insuredCount = 0;
uint256 leftover = usdt.balanceOf(address(this));
if (leftover > 0) usdt.safeTransfer(feeWallet, leftover);
}
emit PositionClosed(block.timestamp, reason);
IBase(base).onPositionClosed(address(usdt), A, B, C, symbol);
}
// ===== Internal: Payout winners =====
function _payoutWinners(address[] memory winners, bool[] memory isWinner, string memory reason) internal {
require(!closed, "Already closed");
require(volumesSubmitted, "No volumes");
require(participants.length == B, "Not full");
require(winners.length == C, "Wrong winners length");
require(isWinner.length == B, "Bad winner flags");
closed = true;
closeReason = reason;
emit PositionClosed(block.timestamp, reason);
uint256 feeBps_ = IBase(base).feeBps();
require(feeBps_ <= MAX_FEE_BPS, "FeeBps too high");
uint256 paymentFee = IBase(base).paymentFeeUSDT();
require(paymentFee <= MAX_PAYMENT_FEE, "Payment fee too high");
require(A > paymentFee, "A <= fee");
address feeWallet = IBase(base).feeWallet();
address catWallet = IBase(base).catWallet();
// must cover base deposits; insurance is handled separately but still sits on balance
uint256 bal = usdt.balanceOf(address(this));
require(bal >= (A * B), "Bad balance");
// ===== OLD payout math (unchanged) =====
uint256 losersPool = A * (B - C);
uint256 protocolFee = (losersPool * feeBps_) / 10_000;
uint256 winnersGross = losersPool - protocolFee;
uint256 pnlPerWinner = winnersGross / C;
uint256 dust = winnersGross - (pnlPerWinner * C);
uint256[] memory prizes = new uint256[](C);
for (uint256 i = 0; i < C; i++) {
address w = winners[i];
uint256 prize = A + pnlPerWinner;
if (i == 0 && dust > 0) prize += dust;
if (paymentFee > 0) {
require(prize > paymentFee, "Prize <= fee");
prize -= paymentFee;
}
prizes[i] = prize;
usdt.safeTransfer(w, prize);
}
if (protocolFee > 0) usdt.safeTransfer(feeWallet, protocolFee);
uint256 totalPaymentFee = paymentFee * C;
if (totalPaymentFee > 0) usdt.safeTransfer(catWallet, totalPaymentFee);
// ===== Insurance settlement (separate from A-economy) =====
uint256 insuranceFeeToCat = _settleInsurance(isWinner);
if (insuranceFeeToCat > 0) usdt.safeTransfer(catWallet, insuranceFeeToCat);
// sweep any remainder (including insurance dust) to feeWallet
uint256 leftover = usdt.balanceOf(address(this));
if (leftover > 0) usdt.safeTransfer(feeWallet, leftover);
emit WinnersPaid(winners, prizes, protocolFee, totalPaymentFee);
IBase(base).onPositionClosed(address(usdt), A, B, C, symbol);
}
function _settleInsurance(bool[] memory isWinner)
internal
returns (uint256 paymentFeeToCat)
{
uint256 pool = insurancePool;
uint256 insured = insuredCount;
if (pool == 0 || insured == 0) {
insurancePool = 0;
insuredCount = 0;
return 0;
}
uint256 paymentFee = IBase(base).paymentFeeUSDT();
require(paymentFee <= MAX_PAYMENT_FEE, "Payment fee too high");
uint256 insuredLosers = 0;
for (uint256 i = 0; i < participants.length; i++) {
address p = participants[i].addr;
uint256 prem = insurancePremiumPaid[p];
if (prem == 0) continue;
if (!isWinner[i]) insuredLosers++;
}
// Case 1: no insured losers => return premium to every insured (minus fee)
if (insuredLosers == 0) {
for (uint256 i = 0; i < participants.length; i++) {
address p = participants[i].addr;
uint256 prem = insurancePremiumPaid[p];
if (prem == 0) continue;
// prem > paymentFee enforced at purchase
uint256 net = prem - paymentFee;
usdt.safeTransfer(p, net);
paymentFeeToCat += paymentFee;
emit InsurancePremiumReturned(p, prem, net, paymentFee);
delete insurancePremiumPaid[p];
}
insurancePool = 0;
insuredCount = 0;
emit InsuranceSettled(pool, insured, 0, 0, paymentFeeToCat, 0);
return paymentFeeToCat;
}
// Case 2: insured losers exist => split pool among insured losers (minus fee each)
uint256 perGross = pool / insuredLosers;
uint256 dustLeft = pool - (perGross * insuredLosers);
require(perGross > paymentFee, "Insurance <= fee");
for (uint256 i = 0; i < participants.length; i++) {
address p = participants[i].addr;
uint256 prem = insurancePremiumPaid[p];
if (prem == 0) continue;
if (!isWinner[i]) {
uint256 net = perGross - paymentFee;
usdt.safeTransfer(p, net);
paymentFeeToCat += paymentFee;
emit InsurancePayout(p, perGross, net, paymentFee);
}
// winners who bought insurance get nothing back in this case (their premium funds losers)
delete insurancePremiumPaid[p];
}
insurancePool = 0;
insuredCount = 0;
emit InsuranceSettled(pool, insured, insuredLosers, perGross, paymentFeeToCat, dustLeft);
return paymentFeeToCat;
}
// ===== Rescue for dust tokens =====
function rescueToken(address token) external onlyBase {
require(closed, "Not closed");
require(token != address(usdt), "USDT locked");
IERC20 t = IERC20(token);
uint256 bal = t.balanceOf(address(this));
if (bal > 0) {
t.safeTransfer(IBase(base).feeWallet(), bal);
}
}
// ===== ETH защита =====
receive() external payable {
revert("No ETH");
}
fallback() external payable {
revert("No ETH");
}
}