For the complete documentation index, see llms.txt. This page is also available as Markdown.

EventlyMarkets.sol

Status: Pending deployment — pre-launch audit complete Dependencies: OpenZeppelin ERC-1155, ERC-20 SafeERC20, PRBMath SD59x18, Redstone EVM Connector


Overview

EventlyMarkets is the production prediction markets contract for evently. It combines:

  • ERC-1155 tradable positions — each trade mints/burns fungible shares per outcome

  • LMSR AMM — Logarithmic Market Scoring Rule (b=200 USDm), permanent liquidity provider

  • CLOB (Central Limit Order Book) — bids and asks with price-time priority, matched automatically on placeOrder

  • Creator economy — 1% creator fee on all volume, claimable after finalization

  • Dual whitelist — separate whitelists for market creators (always enforced) and traders (beta-only)

  • Dispute system — any market participant can dispute; dedicated disputeResolver role settles; monthly VRF lottery for successful disputers

  • Oracle markets — price-feed-based binary markets resolved automatically via Redstone Pull-model oracle

  • Lucky Trader raffle — weekly VRF raffle over active traders; winner-takes-all bonus drawn by admin


Key Parameters

Parameter
Value

Creator collateral

50 USDm

Import fee (Polymarket markets)

10 USDm (one-time, goes to treasury)

Dispute collateral

50 USDm (waived for admin)

Dispute window

24 hours after resolution

Finalization buffer

+1 hour after dispute window (sequencer guard)

Burn delay (losing shares)

24 hours after finalization

Total fee

2.5% fixed (1% creator + 1% treasury + 0.5% resolver pool)

Max options per market

4

Min options per market

2

Min betting window

1 hour (prevents flash markets)

Min trade

0.001 shares (1e15)

Min order value

1 USDm (anti-dust)

LMSR liquidity parameter b

200 USDm

Max orders per book

200 per (market, option, side)

Max orders per user per market

10 (all options + sides combined)

Max pause duration

72 hours (auto-expiry)

Treasury withdrawal delay

24 hours timelock

Resolver pool change delay

24 hours timelock

1 winning share pays out

1 USDm exactly

Dispute lottery share

30% of treasury gains from successful disputes

Dispute lottery cycle

30 days

Dispute lottery style

Winner-takes-all (VRF selected)

Max weekly lucky traders

500 per week

Lucky trader week duration

7 days


LMSR Pricing Mechanism

LMSR (Logarithmic Market Scoring Rule) is an automated market maker where price follows a mathematically sound cost function:

  • q[i] = shares outstanding for option i

  • b = liquidity parameter (200 USDm) — controls price sensitivity

  • Cost to buy shares of option i = C(q after) - C(q before)

Implied probability of option i:

At initialization all quantities are 0, so each option starts at 1/n probability. Prices always sum to exactly 1.

Market subsidy: To bootstrap liquidity, the creator locks b × ln(n) USDm at market creation. This subsidy guarantees solvency: poolBalance + subsidyDeposited >= total winning shares at all times.


Architecture


Market Lifecycle

  1. Created — creator locks 50 USDm collateral + LMSR subsidy. winningOption initialized to type(uint256).max (sentinel: not yet resolved).

  2. Active — users buy/sell shares via LMSR AMM and CLOB.

  3. BettingClosed — after betting deadline, no new trades.

  4. Resolved — creator declares winning option with a mandatory evidence hash. 24-hour dispute window opens.

  5. Disputed — any market participant (trader or creator) can dispute by posting 50 USDm collateral. Admin disputes are free (security function). disputeResolver (not admin) adjudicates with evidence hash.

  6. Finalized — dispute window closes without challenge, or disputeResolver settles. Winners redeem at 1 USDm/share.

  7. Cancelled — only allowed before any trading activity, or after resolutionDeadline timeout. If timeout: creator's accrued fees are confiscated to treasury.

  8. Slashed — admin can slash a market in Active, BettingClosed, Resolved, or Disputed status. Cannot slash Finalized, Cancelled, or already-Slashed markets. Creator collateral goes to treasury; shareholders get pro-rata refund.

Mandatory resolution: cancelMarket is blocked if totalVolume > 0 or there are active resting orders, unless resolutionDeadline has passed. A market with any activity must be resolved (or time out) — it cannot be quietly cancelled.


Access Control Roles

Role
Address
Capabilities

admin

Multisig

Whitelist management, pause, ban, slash markets, admin markets, treasury (timelocked), admin cancel orders, transfer roles

disputeResolver

Dedicated address

settleDispute() only — cannot resolve markets or access treasury

Market creator

marketCreatorWhitelisted[addr]

Create markets, resolve their own markets, cancel their own markets

Whitelisted trader

whitelisted[addr]

Trade, dispute participated markets

The disputeResolver and admin are separate roles. Admin cannot unilaterally settle disputes.


Whitelist System

Two independent whitelists:

  • Trader whitelist (whitelisted) — controls who can trade. Enforced during beta; disabled (setWhitelistEnabled(false)) at public launch.

  • Creator whitelist (marketCreatorWhitelisted) — controls who can create markets. Always enforced.

Admin is implicitly a market creator. Admin also bypasses the trader whitelist — the onlyWhitelisted modifier always passes for admin, regardless of whitelistEnabled.


Fee Structure

Recipient
Default
Description

Market creator

1% (creatorFeeBps = 100)

Accrues in creatorAccruedFees, claimable after finalization

Treasury

1% (treasuryFeeBps = 100)

Accumulates in treasuryBalance

Resolver pool

0.5% (resolverFeeBps = 50)

Redirected post-TGE to staking contract (24h timelock on changes)

Total

2.5% (fixed)

Not configurable

Admin markets: creator cut goes to treasury.

Dispute lottery allocation: When a dispute is settled against the creator, 30% of the net treasury gain from that dispute (slashed collateral portion + slashed creator fees) is routed to the current month's dispute lottery pool instead of treasury. Distributed equally to all that month's successful disputers via distributeMonthlyDisputeRewards().


Dispute System

Eligibility

Any address that has placed an order or sold to the AMM on the market (hasParticipated[marketId][user] == true) can dispute. Admin can always dispute (free of charge, security function).

Imported Polymarket markets can be disputed under the same rules.

Collateral

  • Regular disputer: posts 50 USDm. Returned + 25 USDm reward if right; forfeited to treasury if wrong.

  • Admin disputer: no collateral posted. If right: nothing transferred. If wrong: 25 USDm reward added to treasury from creator collateral.

Settlement

disputeResolver calls settleDispute(marketId, creatorWasRight, finalOption, evidenceHash). Evidence hash is mandatory and emitted as ResolutionEvidence event.

If creator was right (dispute rejected):

  • Disputer's 50 USDm collateral → treasury (if they paid it)

If creator was wrong (dispute upheld):

  • Creator loses 50 USDm collateral:

    • Regular disputer: disputer receives back their 50 USDm + 25 USDm reward (75 USDm total). 25 USDm of creator collateral → treasury (after lottery deduction)

    • Admin disputer: full 50 USDm creator collateral → treasury (after lottery deduction)

  • All creator accrued fees slashed to treasury (after lottery deduction)

  • 30% of net treasury gain → current month's lottery pool (monthlyDisputePool[month])

  • Market finalized with _finalOption


Monthly Dispute Lottery

Successful disputers accumulate entries in _monthlyDisputers[month] (one entry per won dispute). After month M ends, admin calls distributeMonthlyDisputeRewards(M, vrfProof, vrfMessage):

  • Winner-takes-all — one disputer is selected via Drand BLS12-381 VRF (MegaETH native precompile). winnerIndex = uint256(keccak256(vrfProof)) % count

  • The full monthlyDisputePool[M] is transferred to the single winner

  • VRF proof is verified on-chain before the winner is selected — invalid proof reverts

  • Cannot be called again for the same month (idempotent guard)

View: getMonthlyDisputers(month) — returns disputer list for any past month.


Oracle Markets

Oracle markets are binary prediction markets resolved automatically from a Redstone price feed, without requiring a human resolver.

Creating an Oracle Market

Constraints:

  • Must have exactly 2 options (binary market)

  • priceFeedId must be non-zero (bytes32("ETH"), bytes32("BTC"), bytes32("SOL"), bytes32("MEGA"), etc.)

  • strikePrice must be non-zero

Supported Price Feeds

Feeds are sourced from the redstone-primary-prod data service (5-signer threshold). The priceFeedId is a right-padded bytes32 encoding of the ticker string:

Asset
priceFeedId (bytes32 of ASCII)

ETH / USD

bytes32("ETH")

BTC / USD

bytes32("BTC")

SOL / USD

bytes32("SOL")

MEGA / USD

bytes32("MEGA")

Resolving an Oracle Market

  • Permissionless — anyone can call it (typically a keeper script)

  • Reads the Redstone price injected in calldata by the keeper

  • conditionMet = strikeAbove ? price >= strike : price <= strike

  • winningOption = conditionMet ? 0 : 1

  • Requires: betting deadline passed, resolution deadline not expired, evidence hash provided

  • Verifies Redstone data freshness and signature count (≥3 signers from redstone-primary-prod)

View: getOracleConfig(marketId) — returns (priceFeedId, strikePrice, strikeAbove).

Keeper Script

scripts/resolve-oracle.js scans all markets, identifies eligible oracle markets, wraps the call with WrapperBuilder (injects Redstone price calldata), and calls resolveMarketWithOracle. Configure with:


Lucky Trader Weekly Raffle

Every week, one active trader wins a USDm bonus drawn by VRF.

Eligibility

Any address that calls placeOrder or sellToAMM is automatically entered into the current week's raffle — at most once per address per week (hasEnteredWeek[week][addr]). The raffle tracks up to 500 addresses per week (MAX_WEEKLY_TRADERS).

Drawing the Winner

  • Admin only; callable once per week (weeklyLuckyDrawn[week])

  • VRF verified on-chain via the Drand BLS12-381 precompile at drandVerifier

  • winnerIndex = uint256(keccak256(vrfProof)) % traderCount

  • bonusAmount is debited from treasuryBalance — reverts if treasury insufficient

  • Emits LuckyTraderWinner(week, winner, bonusAmount)

Views:

  • getWeeklyTraders(week) — returns all entered addresses for that week

  • getCurrentWeek() — returns the active week number (block.timestamp / WEEK_DURATION)

Admin Setup

setDrandVerifier(address) — set the Drand precompile address (admin only). The precompile address is network-specific; confirm via MegaETH documentation before deployment.


Emergency Controls

Pause

pause() — admin only. Blocks all trading and market creation. Auto-expires after 72 hours (MAX_PAUSE_DURATION). Anyone can call unpause() after 72h have elapsed; admin can unpause at any time.

Ban

banAddress(addr) / unbanAddress(addr) — admin only. Banned addresses cannot interact with any market function.

Admin Cancel Orders

adminCancelOrders(orderIds[]) — admin only. Force-cancels resting orders and returns escrowed funds to order owners. Used to remediate griefing or banned-address orders.

Timelocked Admin Operations

Operation
Delay
Request
Execute
Cancel

Treasury withdrawal

24 hours

requestTreasuryWithdrawal(to, amount)

executeTreasuryWithdrawal()

cancelTreasuryWithdrawal()

Resolver pool address change

24 hours

requestResolverPoolChange(addr)

executeResolverPoolChange()

cancelResolverPoolChange()


Security Properties Implemented

ID
Property

F-02

claimCancelRefund — pre-burn snapshot prevents sequential claim insolvency

A-03

MAX_ORDERS_PER_BOOK = 200 per (market, option, side) — prevents O(n) DoS

F-DS-M02

_cleanBook() called on cancelOrder — dead CLOB entries removed immediately

R2-2

claimCreatorFees requires Finalized status — blocked while Disputed

L-01

nonReentrant on all fund-moving external functions; custom inline mutex (not OZ)

GROK-M01

subsidyDeposited included in cancel refund pool — recoverable by shareholders

GPT-R3-3

slashMarket handles Disputed status — no funds stranded

GROK-L02

createdAt != 0 guard on all view functions

BIZ-06

MIN_BETTING_WINDOW = 1 hour — prevents flash markets

ATCK-06

MIN_ORDER_VALUE = 1 USDm per order — prevents dust griefing

ATCK-07

FINALIZE_BUFFER = 1 hour — sequencer timestamp manipulation guard

AC-03

Treasury withdrawal 24h timelock

INV-06

winningOption sentinel = type(uint256).max — prevents accidental option-0 resolution

INV-08

_cleanBook called before length check in _restOrder

MATH-10

Last claimer receives full effectivePool — dust prevention

SEC-PAUSE

Emergency pause with 72h auto-expiry; force-unpause by anyone after expiry

SEC-BAN

Address ban + adminCancelOrders for targeted remediation

SEC-SLIPPAGE

minUsdmOut mandatory in sellToAMM — sandwich attack prevention

SEC-ORDERCAP

MAX_ORDERS_PER_USER = 10 per market per address

SEC-TIMELOCK

Resolver pool address change 24h timelock

SEC-RESOLVER

Separate disputeResolver role — admin cannot settle disputes unilaterally

SEC-EVIDENCE

Mandatory evidence hash on resolveMarket and settleDispute

SEC-DISPUTE

Disputes open to all participants; imported markets disputable; admin free

SEC-MANDATORY

cancelMarket blocked on active markets — mandatory resolution

SEC-CREATORS

Separate marketCreatorWhitelisted mapping

SEC-LOTTERY

30% of treasury gains from successful disputes → monthly lottery pool

Q7-CONDID

_validateConditionId: 66-char, 0x-prefix, lowercase hex only — prevents case-aliased duplicates


Functions

Trading

  • placeOrder(marketId, optionIndex, side, quantity, pricePerShare, minFill) — BUY or SELL limit order; CLOB matched first, remainder to AMM for BUY

  • sellToAMM(marketId, optionIndex, shares, minUsdmOut) — instant sell to LMSR AMM; minUsdmOut required

  • cancelOrder(orderId) — cancel resting order; escrowed USDm or shares returned immediately

Pricing (view)

  • getPrice(marketId, optionIndex) — LMSR implied probability (0 to 1e18)

  • getImpliedPrices(marketId) — array of all option probabilities

  • quoteBuy(marketId, optionIndex, usdmNet) — shares out for a given net USDm (binary search)

  • quoteSell(marketId, optionIndex, shares) — gross USDm out for a given share quantity

  • getQuantities(marketId) — outstanding shares per option

  • getSubsidy(marketId) — subsidy deposited and b parameter

  • getPoolBalance(marketId) — pool balance + subsidy deposited

  • getMarketInfo(marketId) — full market struct

  • getCreatorFees(marketId) — accrued creator fees + claimed flag

  • getBidBook(marketId, optionIndex) — resting bid order IDs

  • getAskBook(marketId, optionIndex) — resting ask order IDs

  • getOrderInfo(orderId) — full order struct

  • isDisputeWindowOpen(marketId) — true if 24h dispute window is still open

  • isBurnReady(marketId) — true if losing shares can be burned (24h after finalization)

  • getMonthlyDisputers(month) — disputer addresses for a given 30-day lottery month

  • getOracleConfig(marketId) — returns (priceFeedId, strikePrice, strikeAbove) for oracle markets

  • getWeeklyTraders(week) — addresses entered in the weekly lucky trader raffle

  • getCurrentWeek() — active week number (block.timestamp / WEEK_DURATION)

Market Management

  • createMarket(...)onlyMarketCreator + onlyWhitelisted; locks 50 USDm collateral + LMSR subsidy

  • createAdminMarket(...)onlyAdmin; pays subsidy only (no collateral)

  • createImportedMarket(...)onlyMarketCreator; pays 10 USDm import fee + subsidy; conditionId validated

  • createOracleMarket(question, options, category, criteria, imageURI, bettingDeadline, resolutionDeadline, priceFeedId, strikePrice, strikeAbove)onlyMarketCreator; exactly 2 options; locks collateral + subsidy; feeds via Redstone

  • resolveMarket(marketId, winningOption, evidenceHash) — creator resolves after betting deadline; evidence hash required. For imported Polymarket markets: admin only (creator cannot resolve)

  • resolveMarketWithOracle(marketId, evidenceHash) — permissionless; reads Redstone price from calldata; resolves oracle market automatically

  • closeBetting(marketId) — permissionless; callable by anyone after bettingDeadline

  • disputeMarket(marketId, proposedOption) — any participant; 50 USDm collateral (waived for admin)

  • settleDispute(marketId, creatorWasRight, finalOption, evidenceHash)onlyDisputeResolver; evidence hash required

  • finalizeMarket(marketId) — permissionless; callable by anyone after dispute window + buffer

  • cancelMarket(marketId) — blocked on active markets (mandatory resolution); allowed after timeout or if no activity

  • slashMarket(marketId, reason)onlyAdmin; works on Active, BettingClosed, Resolved, Disputed

Claims

  • redeemWinnings(marketId) — burn winning shares for 1 USDm each

  • claimCancelRefund(marketId) — pro-rata refund on cancelled/slashed markets; last claimer gets full remaining pool

  • claimCreatorFees(marketId) — creator withdraws accrued fees (post-finalization only)

  • burnLosingShares(marketId, holders[]) — admin keeper; burns losing shares 24h after finalization

  • reclaimCancelledOrder(orderId) — claim escrowed funds from a cancelled order (lazy refund pattern)

Admin

  • distributeMonthlyDisputeRewards(month, vrfProof, vrfMessage) — draw VRF winner-takes-all from monthly dispute lottery pool

  • drawLuckyTrader(week, vrfProof, vrfMessage, bonusAmount) — draw weekly lucky trader winner via VRF; debits treasury

  • setDrandVerifier(addr) — set Drand BLS12-381 precompile address

  • requestTreasuryWithdrawal(to, amount) / executeTreasuryWithdrawal() / cancelTreasuryWithdrawal() — 24h timelocked withdrawal

  • requestResolverPoolChange(addr) / executeResolverPoolChange() / cancelResolverPoolChange() — 24h timelocked resolver update

  • withdrawResolverPool(amount) — pull accumulated resolver fees

  • setDisputeResolver(addr) — assign the disputeResolver role

  • transferAdmin(newAdmin) / acceptAdmin() — two-step admin transfer

  • addToWhitelist(wallet) / batchWhitelist(wallets[]) / removeFromWhitelist(wallet) — trader whitelist

  • batchAddMarketCreators(accounts[]) — bulk add to creator whitelist

  • setWhitelistEnabled(bool) — toggle trader whitelist enforcement

  • addMarketCreator(addr) / removeMarketCreator(addr) — creator whitelist

  • banAddress(addr) / unbanAddress(addr) — block address from all functions

  • adminCancelOrders(orderIds[]) — force-cancel resting orders (returns funds to owners)

  • pause() / unpause() — emergency pause (72h auto-expiry; anyone can force-unpause after)

  • upvoteMarket(marketId) — community upvote (whitelisted traders); emits MarketUpvoted

  • createInviteCode(codeHash) / redeemInviteCode(code) — invite system


Token ID Encoding

MAX_OPTIONS = 4, so each market occupies token IDs [marketId*4, marketId*4+3].


CLOB Design

Property
Value

Order types

BUY and SELL

Matching

Automatic on placeOrder

Price priority

Best price first (ascending for asks, descending for bids)

Time priority

FIFO within same price level

Dead entry cleanup

Immediately on cancelOrder (_cleanBook — F-DS-M02); also before length check in _restOrder (INV-08)

Book cap

200 orders per (market, option, side) — griefing cap

Per-user cap

10 active orders per market per address — per-user saturation cap

Min order value

1 USDm — prevents dust order griefing

Last updated

Was this helpful?