Contract Architecture
17 Solidity contracts, 180 tests, Arbitrum One
Architecture Diagram
PredictionEngine ──→ IOracle (DualOracle)
│ ├── ChainlinkAdapter (primary)
│ └── PythAdapter (fallback)
│
├──→ NotchScore (immutable scoring)
├──→ AssetScoreTracker (per-asset scoring)
├──→ StakingModule (stake $NOTCH to predict)
│
NotchPassMarket ──→ ERC-1155 passes + marketplace
│
└──→ Treasury (fee distribution)
├── 60% buyback + burn
├── 25% predictor rewards
└── 15% reserve
NotchToken ──→ ERC-20 ($NOTCH, 1B fixed supply)
NotchIndex ──→ Bundled predictor indices
TokenVesting ──→ Team allocation (4yr vest, 1yr cliff)
MerkleAirdrop ──→ Early user token distributionCore Contracts
PredictionEngine
Commit-reveal-resolve lifecycle. Stores prediction hashes, verifies reveals, queries oracles, updates scores. Supports batch resolution and resolve bounties.
commit(bytes32 hash)reveal(id, prediction, salt)resolve(id) / batchResolve(ids[])Pausable + ReentrancyGuardStaking enforcement via StakingModuleNon-reveal penalty (expired = scored as wrong)
NotchScore
Immutable scoring contract. Computes the composite Notch Score from four components. Intentionally non-upgradeable — scoring math is a trust guarantee.
getScore(address) → composite scoregetScoreBreakdown(address) → (accuracy, calibration, consistency, volume)getCalibration(address) → 1 - BrierScoreisActive(address) → boolEMA decay 0.95, Volume cap N_ref=500Weights: calibration 40%, accuracy 25%, volume 20%, consistency 15%
NotchPassMarket
ERC-1155 marketplace for prediction access passes. Supports primary sales, secondary market listings, and per-prediction redemption tracking.
createPass(numPredictions, price)buyPass(passId) / fillOrder(listingId)listPass / cancelListingredeemPrediction(passId, predictionId)2.5% protocol fee on all tradesMin score gating (configurable)
Oracle Layer
DualOracle
Combines two oracle sources with automatic fallback. Tries primary first; if stale (>5min) or reverting, falls back to secondary.
getPrice(asset) with staleness detectiongetPriceAt(asset, timestamp) for historical lookupsConfigurable staleness thresholdOwner can swap primary/fallback oracles
ChainlinkAdapter
Adapts Chainlink AggregatorV3 feeds to the IOracle interface. Normalizes prices to 18 decimals. Walks back up to 50 rounds for historical price lookups.
registerFeed(pairName, feedAddress)18-decimal normalization from any feed decimalsRound-walking for getPriceAt()
PythAdapter
Adapts Pyth Network pull-based price feeds to the IOracle interface. Handles Pyth's variable-exponent price format.
registerFeed(pairName, pythPriceId)Variable-exponent → 18-decimal normalizationMinimal IPyth interface (no full SDK dependency)
Modules
NotchToken
$NOTCH ERC-20 token. 1 billion fixed supply, burnable. No mint function exists post-deployment.
ERC20 + ERC20Burnable (OpenZeppelin)Constructor mints 1B to deployerburn(amount) / burnFrom(address, amount)
StakingModule
Predictors stake $NOTCH to commit predictions. Anti-sybil mechanism with slashing for bad actors.
stake(amount) / requestUnstake(amount) / unstake()7-day unstaking cooldownisEligibleToPredict(address) — checked by PredictionEngineslash(address, amount) — only callable by PredictionEngine
Treasury
Collects protocol fees and distributes them according to the 60/25/15 split.
distribute() — splits ETH: 60% buyback, 25% rewards, 15% reservewithdrawTreasury(amount) — owner withdraws from reserveReentrancyGuard on all ETH transfers
NotchIndex
Bundled predictor indices weighted by Notch Score. Rebalances periodically.
createIndex(name, predictors, weights)rebalance(indexId) — auto-rebalance by live scoressubscribeIndex(indexId) payableMax 50 predictors per index
AssetScoreTracker
Per-asset accuracy and calibration tracking. Separate from NotchScore (which is immutable).
getAssetScore(predictor, asset)getAssetBreakdown(predictor, asset)getBestAsset(predictor)Wired into PredictionEngine._resolve()
TokenVesting
Linear vesting with cliff for team token allocation.
claim() — beneficiary claims vested tokensvestedAmount() / claimable() viewsrevoke() — owner can revoke unvested (if revocable)
MerkleAirdrop
Merkle proof-based token claim for early user airdrop.
claim(amount, proof[]) — verify + transferisClaimed(address) / verify(address, amount, proof[])Owner sweep after claim deadline
Security
- ReentrancyGuard on all contracts that transfer ETH
- Pausable on PredictionEngine and NotchPassMarket (emergency stop)
- Custom onlyOwner access control on core contracts, OpenZeppelin Ownable on modules
- 180 passing tests including fuzz tests for scoring math
- BUSL-1.1 license (converts to MIT after 2 years)