Trepa Case Study Cover

Trepa Case Study

Lifting critical invariants on-chain ahead of mainnet.

Communication, coordination, and the overall audit process were excellent. The team presented well-structured findings and consistently supported their conclusions with detailed logs and reproducible tests.

Their willingness to go beyond the defined scope to highlight additional considerations made me realize their strong commitment to quality and added significant value to us.

- Leon Meka (Lead Developer), Trepa

Team

Pyro

Pyro

Lead Security Researcher

Lead Security Researcher at Sherlock.
Over 100 audits performed and 500+ bugs found.

YanecaB

YanecaB

Security Researcher

A promising Security Researcher.
In the space for ~1 year, and showing remarkable results!

Kyan

Kyan

Security Researcher

Security Researcher specializing in Solana smart contracts.
#1 cumulative ranking on Solana Audit Arena.

Ishwar Kumar

Ishwar Kumar

Security Researcher

Strong AppSec background and active in bug bounties.
Experienced in Web2 audits, primarily TypeScript and other languages.

Zuhaibmohd

Zuhaibmohd

Security Researcher

Independent EVM & Solana Security Researcher.
50+ Protocol Reviews | 300+ Impactful Vulnerabilities Identified.

About the Client

Trepa is a Solana-based short-horizon forecasting game (Flash Pools) where every player pays the same fixed entry fee and submits a numeric price estimate for an asset, starting with Bitcoin.

After settlement, each player's error is compared to the field median: closer estimates win their entry back and share a prize pool funded from losers' entries (after a platform take), with accuracy-weighted splits subject to per-round profit caps.

Players also earn a precision score and can qualify for streak mechanics, with a portion of the take flowing to a streak accumulator.

Key Metrics

Severity Count

SeverityCount
High2
Medium6
Low7

15

Total Findings

2 / 2

High Resolved

6 / 6

Medium Resolved

5 Days

Audit Duration

7724...3ba2

Commit hash

6ad6...d87d

Remediation hash

Solana + ts/rs Back end

Network

Prediction Game

Project type

High Severity Issues and Fixes

[H-01] Backend oracle finalizes pools with a stale price from a capped aggregate-trades page

Impact: The 120-second BTCUSDT lookup was capped at 1000 rows by the data provider, returning the oldest page instead of the newest. The stale page-tail price fed straight into pool resolution, producing wrong winners and a valid Merkle root for an economically incorrect outcome.

Fix: Replaced the wide window query with a direct nearest-before lookup (`endTime: outcomeMs - 1`, `limit: 1`) and added a drift bound, eliminating the truncation class entirely.

[H-02] Streak reward claim transaction binding bypass enables double payout

Impact: The submit endpoint trusted the client-supplied `streak_reward_id` without verifying it matched the signed transaction. An attacker could swap the reward ID after signing to mark a different reward claimed in the DB while the chain executed the original payout, leaving the original reward repeatedly claimable and draining the streak pool.

Fix: The signed proof is now bound to a context object containing `streak_reward_id` and `wallet_address`; the submit endpoint re-verifies that context before updating `is_claimed`, so swapping the reward ID after signing now fails verification.

Medium Severity Issues and Fixes

[M-01] Indexer consistency and resolution trust can lead to incorrect payout finalization

Impact: The indexer did not enforce event ordering and ran live subscription and backfill in parallel, so an old backfill event could overwrite a newer live update. The resolution path then trusted the resulting state without checking completeness, opening the door to incorrect winners and permanently incorrect on-chain payouts.

Fix: A `prediction_revisions` counter was introduced on-chain and used as a sync cursor; the indexer enforces ordering against it and the resolver verifies indexer sync before submitting finalization.

[M-02] WASM reward cap waterfilling underpays uncapped winners

Impact: When some winners hit the per-round gain cap and others did not, the WASM distribution engine kept the raw proportional alpha and routed the capped winners' leftover dividends to the takeout instead of redistributing them. Uncapped winners were silently underpaid and the protocol over-collected.

Fix: The waterfilling algorithm now redistributes capped winners' excess to still-uncapped winners and explicitly fails on invalid/no-alpha inputs instead of silently falling back to zero.

[M-03] Non-idempotent streak reward settlement can lead to duplicate payouts under failure conditions

Impact: The streak claim flow marked the DB row claimed, broadcast the on-chain transaction, and rolled back the DB row if confirmation failed. If the chain accepted the transaction but the backend missed the confirmation (RPC timeout, network partition), the user could retry and receive a second on-chain payout.

Fix: An on-chain replay-protection PDA seeded with `(reward_id, streak_id, wallet)` was added so the chain itself rejects duplicate claims; the API forwards `reward_id` and the indexer drives DB state from emitted events with strict mapping checks.

[M-04] Non-qualifying streak participants are not reset until someone wins

Impact: `processStreakWinners` returned early when no participant reached the required streak count, skipping the reset step for users who failed the precision threshold. Stale streak counters survived into later rounds and a user with a `qualify → fail → qualify` sequence could be incorrectly treated as a streak winner, generating invalid claimable rewards.

Fix: The reset step was moved before the early-return check so non-qualifying participants are reset every round, and the winner query was tightened to wallets that actually qualified in the current pool.

[M-05] Pool can be permanently bricked when a winner's leaf exceeds `max_roi`

Impact: The on-chain claim path enforces a per-winner ROI cap, but the WASM distribution engine used a hardcoded `max_roi` of 100x instead of the pool's configured value. Any pool with a tighter cap could ship leaves above the on-chain bound; every claim would revert with `MaxRoiExceeded`, `claims_left` would never reach zero, and the entire vault would be permanently stranded with no attacker required.

Fix: The WASM reward calculator now consumes the pool's configured `max_roi` so leaves can no longer be constructed above the on-chain enforcement boundary.

[M-06] Token-2022 ATA derivation mismatch can DoS `finalize_pool` on time-series pools

Impact: `assert_valid_token_account` derived the canonical streak ATA with legacy SPL `get_associated_token_address`, ignoring the token program. For Token-2022 mints the derived address differs from the real ATA, so finalization with the correct Token-2022 streak account always failed validation and blocked time-series pools from settling.

Fix: Switched the derivation to `get_associated_token_address_with_program_id`, so Token-2022 mints resolve to the correct address. Fix landed defensively even though Trepa does not currently use Token-2022 mints in production.

Why Phage Security?

Trepa selected Phage Security to review a protocol that intentionally keeps the on-chain Anchor program minimal and pushes reward math, oracles, and indexing into the backend. That split surface needed researchers comfortable on both sides of the boundary. Our team mapped the trust assumptions across the resolver-signed path, the off-chain indexer, and the WASM distribution engine, and surfaced the cases where each side was over-trusting the other.

The general direction throughout the review was to lift critical invariants onto the on-chain program, validate oracle and indexer freshness instead of assuming it, and make money-moving flows idempotent. The Trepa team responded promptly and shipped fixes for 14 of the 15 reported issues during the remediation window — most of the patches went beyond the literal bug — leaving the codebase in good shape for mainnet.