PerpLog Trust Model: What We Can and Cannot Do

What “trustless” covers on v3

The v3 contract (deployed 2026-04-17 at 0x661a949597420C21939047515361f37b15F0b44F) is ownerless. It has no Ownable parent, no Pausable, no transferOwnership, no renounceOwnership, and no rescueNative / rescueToken escape hatches. Every function except setMerkleRoot is permissionless:

  • createSeason(id, rulesHash, scorer) — anyone can open a season slot and commit the scoring rules hash + scorer address for that season.
  • deposit(id) payable — anyone can fund any open season pool in native HYPE.
  • claim(id, amount, proof) — any winner claims with a Merkle proof. Amount binds to the caller; no third party can claim on their behalf.
  • sweepToSeason(from, to) — after 90 days, anyone can route unclaimed funds from a finalized season to another open season. No admin-chosen destination.
  • sweepOrphanedToSeason(to) — anyone can rescue HYPE force-sent via SELFDESTRUCT and route it into an open season. No admin, no lock.

If PerpLog disappeared tomorrow, every past season's winners could still claim with their proofs (published via this site and backed up in Firestore). Unclaimed pools route to the next open season automatically once anyone triggers the sweep. Orphaned HYPE becomes rescuable by the community, not by us. Season claim windows remain 90 days.

The scoring pipeline — the honest trust surface

The contract cannot verify whatscoring the Merkle root encodes; it only verifies the proof matches the root. So the real trust surface is the off-chain scoring + the scorer's integrity for each season.

  • Season scores are computed off-chain by a TypeScript engine reading data from Firestore (trades, reviews, playbooks, discipline signals).
  • The scoring engine multiplies trading points × discipline points × engagement points × theme multiplier × tier multiplier × PerpScore multiplier. The code is publicly readable in the repository but is not anchored on-chain.
  • Eligibility rules (minimum trades, weekly reviews, pre-season activity) live in Firestore documents that can, in principle, be modified by the PerpLog team — except the rulesHash committed at createSeasonis immutable, so any mid-season rule change makes the scorer's published root provably mismatch the committed rules. Discrepancy is publicly auditable via /season/verify/*.
  • At season end, the admin panel builds the winners Merkle tree off-chain, and the pinned scorer wallet calls setMerkleRoot(seasonId, root). One-shot — the contract verifies nothing about the content of the root, only that it has not been set before.

What this means in practice. A user currently has to trust that (a) the scoring rules published on the Season rules page are the rules actually applied, (b) those rules do not quietly change mid-season, and (c) the winnersJson payload submitted to setWinners matches what the rules produce. Today these are social commitments backed by a transparent backend. They are not yet cryptographic commitments.

Every function on the v3 contract

Full disclosure of every external function. All permissionless except setMerkleRoot:

FunctionAccessWhat it can doWhat it cannot do
createSeason(id, rulesHash, scorer)permissionlessOpen a season slot with immutable rulesHash commitment and pinned scorer address.Overwrite an existing season (reverts). Change the scorer or rulesHash after creation.
deposit(id) payablepermissionlessDeposit native HYPE (msg.value) into any open season pool.Deposit into a finalised season. Deposit zero.
setMerkleRoot(id, root)scorer-only, one-shotPublish the winners Merkle root for one season, exactly once. Only the address pinned at createSeason can call.Call twice. Change the rules. Modify winner amounts after the fact. Refund deposits.
claim(id, amount, proof)permissionlessPull a winner's prize using a Merkle proof. Funds flow to msg.sender.Claim for another address (leaf binds to caller). Claim twice. Claim past the 90-day window. Claim more than totalDeposited.
sweepToSeason(from, to)permissionless after 90dRoute unclaimed funds from a finalised+expired season into another open (non-finalised) season.Sweep before the 90-day window closes. Sweep into a finalised season. Sweep to an admin address.
sweepOrphanedToSeason(to)permissionlessRoute HYPE force-sent to the contract (e.g. via SELFDESTRUCT) into any open season.Touch reserved season funds. Route to a finalised season. Route to an admin address.
receive() / fallback()anyoneNothing. Both revert with DirectSendRejected — callers must use deposit(id) explicitly.Silently accept value. Hide accounting.

Absent by design: no pause, no unpause, no transferOwnership, no renounceOwnership, no rescueNative, no rescueToken. The contract is native-HYPE only; sending ERC20 tokens to it loses them.

Who owns the active contract

Nobody. The live SeasonPrizePoolV3 at 0x661a949597420C21939047515361f37b15F0b44F on HyperEVM, deployed 2026-04-17 (tx 0x37d0370e1dd473ce71b95cb15be19e3d1366404d6bd1241b2fbcc5ebf648180e), has zero onlyOwner functions. No Ownable, no Pausable, no transferOwnership. The deployer wallet has no ongoing privileges — it signed one CREATE transaction and then became a random address to the contract.

The only access-controlled action in the whole contract is setMerkleRoot(seasonId, root), which is gated to a per-season scorer address pinned at createSeason. That scorer can publish the winners Merkle root exactly once for that one season, and is structurally constrained by the public rulesHash— any observer can re-compute the scoring from the published rules and detect a bad root. A compromised scorer can at worst corrupt one season's pool (< $1k at current scale); all past and future seasons are unaffected because each season pins its own scorer.

Two earlier contracts exist on HyperEVM mainnet but are superseded: 0x373710901daF2bd80DAa561576F82c1E9b955c49 (v1 USDC, paused, zero funds, abandoned) and 0x0DCCaeDcEE88717d1d01eFBCff5521bf556d9193 (v2 native HYPE, paused, zero funds, no Season ever created on it). Both are referenced in AUDIT.md and remain on-chain but are no longer the production target.

Audit status

The v3 contract was internally audited pre-deployment: Solidity 0.8.34, OpenZeppelin 5.6.1, 52/52 tests passing (49 unit + 3 invariants across 128,000 handler calls), 100% function / line coverage (96.4% branch — one defensive ternary uncovered), 0 High / 0 Medium / 0 Low Slither findings on our code. Bytecode is 6,419 bytes runtime, 74% below the 24 kB EIP-170 limit. The full audit report (Round 4) lives at contracts/AUDIT.md in the repository.

An external professional audit is scheduled once the cumulative pool value across seasons crosses $10k. Until then, the caps documented above (pool ≤ $1k per season, conservative owner-wallet fund) contain the blast radius.

Shipped already (history)

The v3 contract started from the strongest honest baseline we could structurally justify for a solo-founded dapp. The items below all shipped before Season 1 launches, so Season 1's winners inherit all of them from day zero:

  • Ownerless contract. No Ownable, no Pausable, no transferOwnership, no renounceOwnership, no rescueNative or rescueToken. Zero onlyOwner functions.
  • Merkle-root distribution. setMerkleRoot is one-shot per season, gated to a per-season scorer address pinned at createSeason. The admin cannot reallocate payouts post-commit — the root is immutable and users claim with their own Merkle proofs. Standard pattern (Sushi, 1inch, Paraswap).
  • Permissionless lifecycle. createSeason, deposit, claim, sweepToSeason, and sweepOrphanedToSeason are all permissionless — anyone can call.
  • On-chain rules commitment. createSeason(id, rulesHash, scorer) emits the rulesHashas an indexed event parameter. Any mid-season change of theme / eligibility rules in Firestore makes the scorer's published root provably mismatch the committed rules — observable publicly on /season/verify/season-1 (or any other seasonId).
  • Immutable scoring history. Firestore scoring writes land in both a live latest doc and an append-only seasonScoreHistory/{seasonId}_{date}_{wallet} snapshot, each tagged with a SHA-256 rulesHash over the canonical theme + eligibility bundle. A second collection seasonRulesHistory chronologically logs every distinct rules-hash emitted.

Possible next steps (not prioritised yet)

At current scale, the structural decentralization above is sufficient and an external audit would be premature. The items below become relevant if the product scales meaningfully:

  • Optimistic dispute window on setMerkleRoot. Add a 7-day challenge period during which anyone can submit an alternative root with a bond. If a valid dispute is proven, the alternative root replaces the original. Removes the residual scorer-compromise risk.
  • Shipped — scoring engine packaged at packages/season-engine. Self-contained TypeScript package (no Firebase, no Next.js, no fetch) that reproduces the scoring, Merkle tree, and rulesHash from public inputs. Published to npm as @perplog/season-engineonce Season 1 is announced. Readme includes a 6-step copy-paste recipe for any observer to rebuild a season's Merkle root and compare against the on-chain commitment.
  • External professional audit. Scheduled once the cumulative pool value across seasons exceeds $10k. Until then, the internal audit + Slither + 128k invariant runs contain the blast radius at negligible pool sizes.

How to verify us right now

  • Contract source verified on hyperevmscan.io — bytecode matches the published Solidity.
  • Audit report at contracts/AUDIT.md in the public repository.
  • Contract activity — every createSeason, deposit, setMerkleRoot, and claim transaction is public on HyperEVM. Compare to the ratios committed on the fee redistribution page.
  • Event logSeasonCreated, Deposit, MerkleRootSet, Claimed, SweptToSeason, and OrphanedSweptToSeason events give a full audit trail. Each SeasonCreated event includes the indexed rulesHash so observers can recompute the scoring independently.
  • Revenue dashboard at /stats/revenue (real-time, reconstructible from the chain).

Where “trustless” appears in our marketing

We previously used the word “trustless” as a short hand for the Season pool mechanic. Based on this audit, we are migrating to “minimized-trust, multisig-bound, on-track to admin-less”in public-facing copy. If you still see an unqualified “trustless” on a page after this doc was published (2026-04-17), that is a bug — report it and we will correct it. The smart contract itself remains genuinely trustless from setWinners onward, and that narrower claim stays on the pages where it applies.


See also: Season 1 Genesis rules · fee redistribution · compensation · no-token philosophy.