See more @ WELEPHANT.XYZ

Prediction Mining - Architecture & Design Decisions

Created: January 23, 2026 Last Updated: March 7, 2026

This document tracks architectural decisions and optimizations made to ensure the simulator and customer-facing tools scale efficiently.


Smart Contract Architecture

Overview

Prediction Mining is a decentralized prediction game built on the BNB Smart Chain. Players play ETH/BNB on numbered squares, and when a round ends, a winning square is randomly selected. Winners share the pot proportionally to their play amounts. The system integrates with the WELEPHANT/ELEPHANT token ecosystem for rewards and staking.

Design Philosophy

The system is designed to be:

  1. Rug-proof - No emergency withdraw functions, fee percentages are constants
  2. Future-proof - On-chain history storage instead of relying on external indexers
  3. Permissionless - Staking vault has no owner, rewards are fully claimable
  4. Gas-efficient - Batch processing with gas-checked cursors for large operations
  5. Upgradable - RaffleMining is stateless; all mutable game state lives in persistent registries that survive upgrades
  6. Sponsorship-friendly - Supports third-party automation and gifting

Sponsorship Model

The system allows msg.sender to pay on behalf of other players, enabling:

Operation Who Pays Who Benefits Protection
play(players[], ...) msg.sender players[] addresses N/A - sponsor loses funds
registerAutoPlay(player, ...) msg.sender player address !active check prevents DoS
deposit(player) msg.sender player address N/A - sponsor loses funds

Security invariants:

Contract Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                           USER INTERFACE                                 │
├─────────────────────────────────────────────────────────────────────────┤
│  RaffleMiningViews.sol - Read-only queries with pagination               │
└────────────────────────────────┬────────────────────────────────────────┘
                                 │
┌────────────────────────────────▼────────────────────────────────────────┐
│                         GAME ENGINE LAYER                                │
├─────────────────────────────────────────────────────────────────────────┤
│  RaffleMining.sol          │  RaffleMiningCrank.sol (CrankCore)          │
│  - Player interactions     │  - Crank orchestration                      │
│  - Stateless coordinator   │  - Auto-play batch processing               │
│  - Configuration           │  - Pending plays processing                 │
│  - Access control          │  - Round phase tracking                     │
│                            │                                              │
│                            │  CrankFinalization.sol                       │
│                            │  - Round finalization & entropy              │
│                            │  - Single winner selection                   │
│                            │  - WELEPHANT/ETH reward distribution         │
│                            │  - Vault swap processing                     │
│                            │  - Fee distribution                          │
└─────────────┬───────────────────────────┬───────────────────────────────┘
              │                           │
┌─────────────▼───────────────────────────▼───────────────────────────────┐
│                         STATE STORAGE LAYER                              │
├─────────────────────────────────────────────────────────────────────────┤
│  GameHistory.sol           │  Verification.sol                           │
│  - Round data              │  - Hash-chained entropy accumulation        │
│  - Player stats            │  - Randomness verification                  │
│  - Round summaries         │  - Finalization results                     │
│  - Participant tracking    │  - Audit trail                              │
├────────────────────────────┼────────────────────────────────────────────┤
│  AutoPlayRegistry.sol      │  PendingPlaysRegistry.sol                   │
│  - Auto-play structs       │  - Pending play queue                       │
│  - Active user set         │  - ETH custody (between rounds)             │
│  - Player settings         │                                              │
│  - ETH custody (balances)  │                                              │
└─────────────────────────────────────────────────────────────────────────┘
              │
┌─────────────▼───────────────────────────────────────────────────────────┐
│                           VAULT LAYER                                    │
├─────────────────────────────────────────────────────────────────────────┤
│  WelephantRewards    │ StakingVault     │ WelephantVault │ GasChargeVault│
│  - ETH rewards       │ - ERC4626 vault  │ - WELEPHANT    │ - Gas refunds │
│  - WELEPHANT rewards │ - Yield tracking │   liquidity    │ - Pool mgmt   │
│  - Partial claims    │ - APR calc       │ - JIT delivery │               │
└─────────────────────────────────────────────────────────────────────────┘
              │
┌─────────────▼───────────────────────────────────────────────────────────┐
│                         EXTERNAL INTEGRATIONS                            │
├─────────────────────────────────────────────────────────────────────────┤
│  WelephantSwap.sol         │  External Contracts                         │
│  - ETH ↔ WELEPHANT swaps   │  - ELEPHANT token                           │
│  - Fee collection          │  - WELEPHANT wrapper                        │
│  - UniswapV2 routing       │  - PancakeSwap router                       │
└─────────────────────────────────────────────────────────────────────────┘

Core Contracts

Contract Role Key Features
RaffleMining.sol Main game (stateless) Player plays, configuration, access control — delegates all state to registries
RaffleMiningCrank.sol Crank orchestrator (CrankCore) Auto-play/pending play batching, round phase tracking, heartbeat
CrankFinalization.sol Finalization & distribution Round finalization, winner selection, reward distribution, vault swaps, fee distribution
GameHistory.sol State storage Rounds, stats, summaries (on-chain database)
Verification.sol Randomness Hash-chained entropy accumulation, provable fairness
RaffleMiningViews.sol Read interface Pagination, batch queries
RaffleMiningLib.sol Utilities Pure functions, calculations
RaffleMiningTypes.sol Operational types Game engine constants, PendingWork struct, RoundPhase enum
GameHistoryTypes.sol History data-model types RoundSummary, PlayerStats, PlayerRoundInfo, PlayerRoundPerformance
RegistryTypes.sol Registry types AutoPlay, PlayerAutoPlaySettings, PendingPlay

Vault Contracts

Contract Role Security
WelephantRewards.sol Player rewards DEPOSITOR_ROLE for credits, partial claims, ReentrancyGuard
StakingVault.sol WELEPHANT staking ERC4626, no owner (100% permissionless)
WelephantVault.sol WELEPHANT liquidity Central vault, async swaps, try/catch safety
GasChargeVault.sol Gas refunds Never reverts (graceful cap), 10% buffer before sweep
FeeAccounting.sol Fee ledger Async fee tracking, decoupled from transfers

Registry Contracts (Persistent State)

These contracts hold all mutable game state. They survive RaffleMining upgrades — the gameContract pointer is updated to the new RaffleMining address and all player state (funds, registrations, pending plays) remains intact.

Contract Role Security
AutoPlayRegistry.sol Auto-play state + ETH custody Merges former AutoPlayRegistry ETH balances with auto-play registration state (structs, active user set, player settings). Game-authorized operations only, no direct withdraw (anti-DDoS)
PendingPlaysRegistry.sol Pending play queue + ETH custody Holds plays submitted after round expiry and their ETH until the crank processes them into the next round. Game-authorized push/pop only

External Integration

Contract Role
WelephantSwap.sol DEX integration (PancakeSwap V2), TWAP oracle, dual-path routing
IPcsSnapshotTwapOracle External TWAP oracle for manipulation-resistant pricing

Access Control Roles

The system uses OpenZeppelin AccessControl for role-based permissions. Two contracts use Ownable instead (noted below).

AccessControl Roles (9 roles across 7 contracts)

Role Contract Permissions Granted To
DEFAULT_ADMIN_ROLE RaffleMining Grant/revoke all roles, game initialization Deployer/Multisig
ADMIN_ROLE RaffleMining Update game parameters (payday chance, min players, gas settings, contract addresses) Governance
PAUSER_ROLE RaffleMining Pause/unpause game Emergency operator
STORAGE_WRITER_ROLE RaffleMining Write round state, player stats, emit events RaffleMiningCrank, CrankFinalization
WRITER_ROLE GameHistory Write round data, player stats, payday fund RaffleMining
WRITER_ROLE Verification Initialize/update/finalize round seeds RaffleMining
WRITER_ROLE FeeAccounting Record admin and staking fees CrankFinalization
DEPOSITOR_ROLE WelephantRewards Credit WELEPHANT rewards (accounting only) RaffleMining
DEPOSITOR_ROLE GasChargeVault Deposit/withdraw gas charge ETH RaffleMining
DEPOSITOR_ROLE WelephantVault Deposit ETH for swapping RaffleMining
CONSUMER_ROLE WelephantVault Withdraw WELEPHANT (JIT delivery) RaffleMining, WelephantRewards, CrankFinalization
CRANK_ROLE WelephantVault Trigger swaps, update TWAP oracle CrankFinalization
DISTRIBUTOR_ROLE FeeAccounting Distribute accumulated fees CrankFinalization

Ownable Contracts

Contract Protected Function Owner
AutoPlayRegistry setGameContract() Deployer
PendingPlaysRegistry setGameContract() Deployer
AddressMinHeap setScore() GameHistory (set via constructor msg.sender)

Role Assignment Flow

Deployer (DEFAULT_ADMIN_ROLE on all AccessControl contracts)
│
├─ RaffleMining
│   ├─ grants STORAGE_WRITER_ROLE → RaffleMiningCrank, CrankFinalization
│   ├─ holds WRITER_ROLE on GameHistory, Verification
│   ├─ holds DEPOSITOR_ROLE on GasChargeVault, WelephantVault, WelephantRewards
│   └─ holds CONSUMER_ROLE on WelephantVault
│
├─ RaffleMiningCrank (CrankCore)
│   ├─ holds STORAGE_WRITER_ROLE on RaffleMining
│   └─ delegates to CrankFinalization via setFinalizationContract()
│
├─ CrankFinalization
│   ├─ holds STORAGE_WRITER_ROLE on RaffleMining
│   ├─ holds WRITER_ROLE on FeeAccounting
│   ├─ holds CONSUMER_ROLE on WelephantVault
│   ├─ holds CRANK_ROLE on WelephantVault
│   ├─ holds DISTRIBUTOR_ROLE on FeeAccounting
│   └─ restricted to CrankCore via onlyCrankCore modifier
│
├─ WelephantRewards
│   └─ holds CONSUMER_ROLE on WelephantVault
│
└─ StakingVault — 100% permissionless (no access control)

Ownership Model & Renouncement

The system is split into two trust domains: a permissionless asset layer and an administered game layer.

Permissionless Contracts (No Admin, No Owner)

These contracts have zero admin functions. No one can modify, pause, or interfere with their operation. They are fully autonomous and immutable post-deployment.

Contract Type Why Permissionless
StakingVault ERC4626 vault Deposit, withdraw, and yield accrual are fully autonomous. No owner, no roles, no kill switch
WelephantSwap DEX router All addresses are immutable (hardcoded or constructor-set). Swap fees route to StakingVault automatically
Multicall3 Utility Stateless batch-read helper
RaffleMiningViews Read-only Pure view functions, no state mutations

Asset sovereignty: ELEPHANT, WELEPHANT, and stWELEPHANT are permissionless tokens. Once in a player's wallet, no admin can freeze, seize, or block transfers. The swap and staking infrastructure operates without any administrative dependency.

Administered Contracts (AccessControl — Multisig)

Game-layer contracts use OpenZeppelin AccessControl with DEFAULT_ADMIN_ROLE held by a multisig. These require active administration for operational needs.

Contract Why Admin Is Needed
RaffleMining Role grants, contract address updates, game parameter tuning
WelephantVault Swap configuration (chunk size, slippage tolerance), swap contract updates
WelephantRewards Vault pointer management (which vault serves WELEPHANT claims)
GameHistory Writer role grants for new game contract versions
Verification Writer role grants for new game contract versions
FeeAccounting Writer/distributor role grants
GasChargeVault Depositor role grants

What admin CAN do: Adjust game parameters, update contract wiring, grant/revoke roles, pause/unpause the game.

What admin CANNOT do: Change fee percentages (immutable constants), drain player funds, manipulate randomness outcomes, prevent fund withdrawals during pause.

Ownable Contracts (Deployer → Multisig)

Three contracts use OpenZeppelin Ownable for a single admin function each:

Contract Protected Function Owner
AutoPlayRegistry setGameContract() Deployer → Multisig
PendingPlaysRegistry setGameContract() Deployer → Multisig
AddressMinHeap setScore() GameHistory (constructor)

These support the upgrade path: when a new RaffleMining is deployed, the owner updates the gameContract pointer so the registries accept writes from the new contract.

Why Full Renouncement Is Not Appropriate

Renouncing ownership on the game layer would permanently brick critical operational functions:

  1. Contract upgradessetGameContract() on registries could never be called, making RaffleMining non-upgradeable
  2. Role management — New crank or finalization contracts could never be authorized
  3. Swap resilience — If swap parameters need adjustment (slippage, chunk size) due to market conditions, admin is required
  4. Vault wiringsetWelephantVault on WelephantRewards must be callable to point to a functioning vault

The permissionless asset layer (StakingVault, WelephantSwap) is already fully renounced by design — these contracts were deployed without any admin capability. Players minimize game-layer exposure by claiming rewards frequently, moving assets into the sovereign token layer.

Delegated Admin Checks

RaffleMiningCrank and CrankFinalization do not inherit AccessControl themselves but delegate admin checks through the storage contract:

require(storageContract.hasRole(bytes32(0), msg.sender), "E60"); // DEFAULT_ADMIN_ROLE

This protects setFinalizationContract() on CrankCore, and setCrankCore(), setWelephantVault(), setFeeAccounting() on CrankFinalization.

Cross-Contract Access Control

CrankFinalization uses an onlyCrankCore modifier for functions called by CrankCore during crank execution:

modifier onlyCrankCore() {
    require(msg.sender == crankCore, "E60");
    _;
}

This restricts finalization, distribution, and vault swap functions to only be callable by the CrankCore contract.

Emergency Pause Behavior

When PAUSER_ROLE calls pause() on RaffleMining, only new-money-in functions are blocked. All fund withdrawal paths remain available:

Feature Available When Paused? Details
Place plays No whenNotPaused — blocks new plays
Register Auto-Play No whenNotPaused — blocks new registrations
Cancel Auto-Play Yes Returns remaining deposit to player
Claim rewards Yes WelephantRewards is a separate contract with no pause dependency
Unstake WELEPHANT Yes StakingVault is 100% permissionless with no pause dependency
Crank processing Yes Continues finalizing rounds and distributing rewards

Design intent: Stop new money entering the game; let all existing money exit through normal paths. The crank keeps running so in-progress rounds can still finalize and credit rewards to winners.

Fee Distribution (Constants)

Fee Percentage Destination
Admin Fee 10% feeCollector address
Staking Fee 20% StakingVault.fundYield()
Payday Fund 20% GameHistory.addPaydayFund() (accounting)
Winner Pool 50% Distributed to winners

BNB Flow Through the System

This section traces how BNB enters, moves through, and exits the system.

Entry Points

BNB enters through these functions:

Function Components Destination
play() Play amount + Gas tax (1%) GameHistory pot + GasChargeVault
registerAutoPlay() Total plays + Gas tax (1%) AutoPlayRegistry
AutoPlayRegistry.deposit() Top-up amount AutoPlayRegistry (player balance)

Gas Tax Model:

Gas charges use a flat percentage tax on play amounts (1% = 100 basis points). This replaces the earlier per-play gas unit model. The gas tax is simple, predictable, and scales proportionally with play size.

gasCharge = totalPlayAmount × gasTaxBasisPoints / 10000

play() breakdown:

msg.value = playAmount + gasCharge (1% of playAmount)
    ├─→ gasCharge      → GasChargeVault.deposit()
    └─→ playAmount     → GameHistory (pot accounting)
                       → gameFee = playAmount × 10%

registerAutoPlay() breakdown:

totalPlayAmount = amountPerSquare × squares × rounds
msg.value = totalPlayAmount + gasCharge (1% of totalPlayAmount)
    └─→ All funds      → AutoPlayRegistry.register(player, ...)
        (stores auto-play struct + credits ETH balance; debited per round by crank)

AutoPlayRegistry.deposit() breakdown:

msg.value = top-up amount
    └─→ AutoPlayRegistry._balances[player] += msg.value
        (available for future auto-play rounds)

Storage Locations

Vault Purpose Holds
GasChargeVault Operator compensation Accumulated gas tax (1% of plays)
AutoPlayRegistry Multi-round plays Auto-play structs, active user set, player settings, ETH balances
PendingPlaysRegistry Between-round plays Pending play queue + ETH for plays submitted after round expiry
WelephantVault Fee conversion ETH awaiting swap to WELEPHANT
WelephantRewards Winner claims Credited rewards (ETH + WELEPHANT)

Incremental Fee Forwarding (Anti-Front-Running)

Previously, all game fee ETH was deposited to WelephantVault at round finalization, creating a large predictable swap that could be front-run. Now fees are forwarded incrementally:

Play Type Fee Forwarding Timing
Manual plays Immediately in _recordPlay() Per-play, as plays occur
Auto-plays Batch accumulated, forwarded at end of _processAutoPlays() End of crank batch
Pending plays Batch accumulated, forwarded at end of _processPendingPlays() End of crank batch

Flow:

Manual Play:
    _recordPlay()
        ├─ gameHistory.addToRoundPot(roundId, amount, fee)  // Track for accounting
        └─ welephantVault.depositETH{value: fee}()          // Forward immediately

Auto-Play Batch (reads from AutoPlayRegistry):
    _processAutoPlays()
        ├─ Loop: fee = _executeAutoPlay() → accumulate autoPlayBatchAccumulatedFees
        └─ End: depositETHToVault(autoPlayBatchAccumulatedFees)  // Forward at batch end

Pending Plays Batch (pops from PendingPlaysRegistry):
    _processPendingPlays()
        ├─ Loop: calculate fee → accumulate pendingPlaysBatchAccumulatedFees
        └─ End: depositETHToVault(pendingPlaysBatchAccumulatedFees)  // Forward at batch end

Why This Prevents Front-Running:

Round Finalization Flow

When a round ends:

Round Pot (e.g., 100 BNB)
    │
    ├─→ Game Fee (10%) = 10 BNB
    │       └─→ Already deposited to WelephantVault (incrementally)
    │           └─→ Async swap to WELEPHANT
    │               └─→ Split:
    │                   ├─ Admin (10% of fee)  → feeCollector
    │                   ├─ Staking (20% of fee) → StakingVault
    │                   └─ Payday (20% of remaining) → accumulates
    │
    └─→ Winners Pool (90%) = 90 BNB
            └─→ Proportional to winning square holders
                └─→ Per player, based on settings:
                    ├─ loopEthRewards=true  → AutoPlayRegistry
                    └─ loopEthRewards=false → WelephantRewards

Note: At finalization, _finalizeRound() only estimates WELEPHANT for accounting purposes - the fee ETH has already been deposited incrementally during plays.

Winner Distribution

Winners on the winning square share 90% of the pot proportionally:

Your Reward = (Your Play on Square / Total on Square) × Winners Pool

Example: You play 0.1 BNB on Square 5. Total on Square 5 is 0.5 BNB. Round pot is 10 BNB.

Bitmask Storage Model & Uniform Amount Constraint

The Design Compromise

Players select one or more of the 16 squares and allocate a uniform amount per square — the same BNB amount on every selected square. This is a deliberate gameplay compromise that enables significant gas savings:

What players CAN do:

What players CANNOT do:

This constraint is inherent to the storage model — each player stores a single amountPerSquare value, not an array of per-square amounts. Players who want different allocations across squares would need to use multiple accounts.

Storage Layout

Each player's play in a round is stored as two values:

mapping(address => uint256) playerSquareBitmask;     // bit N-1 = square N selected
mapping(address => uint256) playerAmountPerSquare;   // uniform amount on each selected square

Encoding: Square N is represented by bit (N-1) in the bitmask. For 16 squares, only bits 0–15 are used.

Example: A player selecting squares 3, 7, and 12 with 0.5 BNB each:

squareBitmask    = 0b0000100001000100  (bits 2, 6, 11 set)
amountPerSquare  = 0.5 ether
totalPlay        = 0.5 × 3 = 1.5 ether

Gas Optimization: 2 SSTOREs vs 32+ Array Pushes

The bitmask model replaces per-square dynamic arrays with two fixed storage slots per player per round:

Operation Old Per-Square Arrays Bitmask Model
Record player's squares 1 SSTORE per square (up to 16) 1 SSTORE (bitmask)
Record player's amounts 1 SSTORE per square (up to 16) 1 SSTORE (amountPerSquare)
Total SSTOREs per play Up to 32 2
Read player's selection Loop over array Single SLOAD + bit check
Check if player is on square Array search O(n) Bit check O(1)

At ~20,000 gas per new SSTORE, a 16-square play saves approximately 600,000 gas (30 avoided SSTOREs × 20,000 gas).

squareTotals Maintenance

While player data is compact (2 slots), the contract still maintains per-square aggregate totals for reward calculation:

mapping(uint256 => uint256) squareTotals;  // square => total BNB on that square

When a play is recorded, squareTotals is updated by iterating the bitmask:

for (uint256 i = 0; i < numSquares; i++) {
    if ((squareBitmask & (1 << i)) != 0) {
        round.squareTotals[i + 1] += amountPerSquare;
    }
}

This is necessary for proportional reward distribution: reward = winnersPool × playerAmountPerSquare / squareTotals[winningSquare].

One Play Per Round

Each player is limited to one play per round (manual or auto-play). This simplifies the bitmask model — the player's squareBitmask and amountPerSquare are written once and never updated within a round. Duplicate play attempts revert with E43. Pending plays that arrive after a player has already played in the round are refunded via depositEthReward.

Exit Points

BNB exits the system through only three paths:

Exit Trigger Recipient
Claim rewards WelephantRewards.claimRewards() Winner (msg.sender)
Cancel auto-play RaffleMining.cancelAutoPlay() Player (balance refund)
Gas refunds Crank execution Crank operator

Note: Admin fees and staking fees are distributed as WELEPHANT (after swap), not BNB. The game fee BNB is swapped to WELEPHANT in WelephantVault before distribution.

Complete Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│                         ENTRY (BNB)                              │
│                                                                  │
│  play() ─────────────────┬─→ Gas Tax (1%) → GasChargeVault      │
│                          └─→ Play Amount → GameHistory pot      │
│                                                                  │
│  registerAutoPlay() ─────────→ AutoPlayRegistry.credit(player)   │
│                                                                  │
│  AutoPlayRegistry.deposit() ──→ AutoPlayRegistry (top-up balance) │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ↓ Round Ends
┌─────────────────────────────────────────────────────────────────┐
│                      FINALIZATION                                │
│                                                                  │
│  Game Fee (10% BNB) ─────────→ WelephantVault                   │
│                                   │                              │
│                                   ↓ Async Swap                   │
│                              WELEPHANT (not BNB)                 │
│                                   │                              │
│                     ┌─────────────┼─────────────┐               │
│                     ↓             ↓             ↓               │
│               Admin Fee     Staking Fee    Payday Fund          │
│             (WELEPHANT)    (WELEPHANT)    (WELEPHANT)           │
│                     │             │             │               │
│                     ↓             ↓             ↓               │
│              feeCollector   StakingVault   Jackpot Pool         │
│                                                                  │
│  Winners Pool (90% BNB) ─────→ Distribution                     │
│                                   │                              │
│                     ┌─────────────┴─────────────┐               │
│                     ↓                           ↓               │
│            loopEthRewards=true       loopEthRewards=false       │
│                     │                           │               │
│                     ↓                           ↓               │
│            AutoPlayRegistry           WelephantRewards           │
│            (BNB for next round)      (BNB claimable)            │
└─────────────────────────────────────────────────────────────────┘
                                              │
                                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                       EXIT (BNB only)                            │
│                                                                  │
│  claimRewards() ─────────────→ Winner receives BNB              │
│  cancelAutoPlay() ───────────→ Player receives BNB refund       │
│  Gas refunds ────────────────→ Crank operator receives BNB      │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Gas Charge Pool Sweep (Excess → Buybacks)

The GasChargeVault is capped at maxGasChargePool (default 10 ETH). When the pool exceeds 110% of the cap (a 10% buffer), the excess above maxGasChargePool is swept to WelephantVault. The excess pads WELEPHANT buybacks — it does not change the economics of the fee split.

GasChargeVault Pool
    │
    ├─ ≤ 110% of cap  → No sweep (buffer avoids frequent small sweeps)
    │
    └─ > 110% of cap   → Sweep (pool - cap) to WelephantVault
                              └─→ Pads WELEPHANT buybacks
                              └─→ Pool drains back down to cap

Configuration:

uint256 maxGasChargePool = 10 ether;     // Cap (adjustable: 0.01–100 ETH)
uint256 sweepThreshold = cap + cap / 10; // 110% of cap triggers sweep

Vault Balance Invariants

The system maintains these invariants:

  1. GasChargeVault.gasChargePool >= sum of all pending gas refunds (capped at maxGasChargePool, excess swept to pad buybacks)
  2. AutoPlayRegistry.totalTracked = sum of all player ETH balances
  3. PendingPlaysRegistry ETH balance = sum of all pending play amounts
  4. WelephantRewards balances = credited but unclaimed rewards
  5. WelephantVault ETH balance = pending swaps to WELEPHANT

Rewards System

This section provides a comprehensive overview of how all rewards are calculated and distributed.

Fee Percentages (Constants in RaffleMiningTypes.sol)

Fee Percentage Source Destination
Game Fee 10% Each play amount Swapped to WELEPHANT
Winners Pool 90% Each play amount BNB to winning square
Admin Fee 10% Of WELEPHANT (from game fee) feeCollector address
Staking Fee 20% Of WELEPHANT (from game fee) StakingVault.fundYield()
Payday Fund 20% Of WELEPHANT (from game fee) Accumulates until payday
Player Share 50% Of WELEPHANT (from game fee) Winners on winning square

Complete Reward Calculation Example

For a round with 10 BNB total plays:

TOTAL INPUT: 10 BNB

BNB SPLIT:
├─ Game Fee:     1 BNB (10%) → WelephantVault → swapped to WELEPHANT
└─ Winners Pool: 9 BNB (90%) → distributed to winning square

WELEPHANT FROM 1 BNB SWAP (assume 1 BNB = 10 WELEPHANT):

├─ Admin Fee:    1 WELEPHANT (10%) → feeCollector
├─ Staking Fee:  2 WELEPHANT (20%) → StakingVault (as yield)
├─ Payday Fund:  2 WELEPHANT (20%) → accumulates for payday round
└─ Player Share: 5 WELEPHANT (50%) → distributed to winners

BNB Rewards Distribution

Winners on the winning square share the Winners Pool (90% of pot) proportionally:

Your BNB Reward = (Your Play on Square / Total on Square) × Winners Pool

Distribution paths based on player settings:

Setting Path Use Case
loopEthRewards = false WelephantRewards (claimable) Manual claiming
loopEthRewards = true AutoPlayRegistry (re-entry) Continuous auto-play

WELEPHANT Rewards Distribution

Winners receive WELEPHANT proportionally (same formula as BNB):

Your WELEPHANT = (Your Play on Square / Total on Square) × Player Share

Distribution paths based on player settings:

Setting Path Benefit
autoStakeWelephant = false WelephantRewards (claimable) Liquid tokens
autoStakeWelephant = true StakingVault.deposit() Earn yield immediately

Auto-Staking Flow:

Winner with autoStakeWelephant = true
    │
    ▼ (Inline in distribution loop)
    │
try depositToStakingVaultForPlayer(winner, reward)
    │
    ├─ SUCCESS: Emit WelephantAutoStaked(winner, reward, shares)
    │             └─ Winner receives vault shares, earns yield automatically
    │
    └─ CATCH:   depositWelephantReward(winner, reward)
                  └─ Fallback to claimable reward in WelephantRewards

Design Note: Auto-staking uses try/catch so that any failure mode (insufficient vault liquidity, token transfer issue, staking vault error) gracefully falls back to a claimable reward. No pre-check needed — the try/catch handles everything. Since ETH fees are deposited and swapped throughout the round, the vault almost always has sufficient WELEPHANT by distribution time, so the catch path is rare.

Payday Mechanism

Trigger: 1/625 probability per round (determined by on-chain randomness)

Accumulation (regular rounds):

Distribution (payday round):

Example:

Regular rounds: Fund accumulates 2 WELEPHANT each round

After 10 rounds without payday:
  paydayFund = 20 WELEPHANT (accumulated)

Round 11 - PAYDAY triggered:
  totalForWinners = playerShare + paydayFund
                  = 5 + 20 = 25 WELEPHANT

  All 25 WELEPHANT distributed to winning square players
  Carryover: 2.5 WELEPHANT stays for next fund

ETH Looping (Auto-Reinvestment)

When loopEthRewards = true, BNB rewards are credited to AutoPlayRegistry instead of WelephantRewards:

Winner receives BNB reward
    │
    ▼ (Inline in distribution loop)
    │
Check loopEthRewards setting
    │
    ├─ YES: creditAutoPlayRegistry(winner, reward)
    │         └─ Balance available for next auto-play round
    │
    └─ NO:  depositEthReward(winner, amount)
              └─ WelephantRewards.depositEth(winner, amount)
                  └─ Player claims manually via claimRewards()

Design Note: ETH looping is handled inline in the BNB distribution loop. The ETH funds are already held by RaffleMining, so creditAutoPlayRegistry is called directly for each winner — no intermediate queue or flush needed.

Use Case: Enables continuous compounding - winnings automatically fund future plays.

Reward Claiming

Players claim rewards from WelephantRewards:

WelephantRewards.claimRewards()
    ├─ Transfer ETH balance directly to msg.sender (always succeeds if balance > 0)
    │
    └─ Withdraw WELEPHANT from WelephantVault
        ├─ If sufficient liquidity: transfer to msg.sender
        └─ If insufficient: WELEPHANT balance restored for retry (ETH still claimed)

Important: Claims support partial completion. If WELEPHANT vault is underfunded, ETH is still delivered and WELEPHANT balance is preserved for a later retry. Claims never revert.

Complete Reward Flow Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                        ROUND FINALIZATION                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  Total Pot (e.g., 10 BNB)                                               │
│      │                                                                   │
│      ├─► Game Fee (10% = 1 BNB)                                         │
│      │       └─► WelephantVault.depositETH()                            │
│      │               │                                                   │
│      │               ▼ (Async swap via crank)                           │
│      │           WELEPHANT                                               │
│      │               │                                                   │
│      │       ┌───────┼───────┬───────────┐                              │
│      │       ▼       ▼       ▼           ▼                              │
│      │    Admin   Staking  Payday    Players                            │
│      │     10%      20%     20%        50%                              │
│      │       │       │       │           │                              │
│      │       ▼       ▼       ▼           ▼                              │
│      │    feeColl  Staking  Fund    Winners                             │
│      │    ector    Vault   (accum)  (proportional)                      │
│      │                                   │                              │
│      │                           ┌───────┴───────┐                      │
│      │                           ▼               ▼                      │
│      │                    autoStake=true   autoStake=false              │
│      │                           │               │                      │
│      │                           ▼               ▼                      │
│      │                    StakingVault    WelephantRewards              │
│      │                    (shares)        (claimable)                   │
│      │                                                                   │
│      └─► Winners Pool (90% = 9 BNB)                                     │
│              │                                                           │
│              ▼ (Proportional to play amounts)                           │
│          Winners on winning square                                       │
│              │                                                           │
│      ┌───────┴───────┐                                                  │
│      ▼               ▼                                                  │
│  loop=true      loop=false                                              │
│      │               │                                                  │
│      ▼               ▼                                                  │
│  AutoPlay       Welephant                                               │
│  Deposit        Rewards                                                 │
│  (re-entry)     (claimable)                                             │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Liquidity Safety Checks

WELEPHANT distributions handle insufficient liquidity gracefully:

Location Mechanism On Failure
_distributeWelephantToWinners() (inline) try/catch around depositToStakingVaultForPlayer Fallback to claimable reward
_finalizeRound() (single winner) try/catch around depositToStakingVaultForPlayer Fallback to claimable reward
_distributePendingFees() welephantVault.canWithdraw(amount) Skip, retry next crank
WelephantRewards.claimRewards() Check vault balance Return false, player retries

Auto-stake uses try/catch rather than a pre-check, so any failure mode (insufficient liquidity, token issues, staking vault errors) gracefully falls back to a claimable reward. The crank never reverts due to auto-staking failures. ETH and WELEPHANT rewards are deposited directly per-winner (no batching), eliminating edge cases from batch accumulation.


Async Swap Architecture

Design Philosophy

The system decouples ETH→WELEPHANT swaps from round finalization to ensure resilient, non-blocking game operation:

OLD MODEL (problematic):

Round Ends → Swap ETH→WELEPHANT → Distribute Rewards
                    ↓
            If swap fails → GAME BRICKED FOREVER

NEW MODEL (resilient):

Round Ends → Deposit ETH to Vault → Record fees in FeeAccounting
                                              ↓
                              Crank processes swaps async (with try/catch)
                                              ↓
                              Distribute fees when liquidity available

Component Responsibilities

┌─────────────────────────────────────────────────────────────────────────┐
│                    ASYNC SWAP FLOW                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  Round Finalization (CrankFinalization)                                  │
│  ├── depositETHToVault(gameFee)        → ETH sits in WelephantVault     │
│  ├── feeAccounting.addAdminFee()       → Accounting only, no transfer   │
│  └── feeAccounting.addStakingFee()     → Accounting only, no transfer   │
│                                                                          │
│  Crank Processing (CrankFinalization.processVaultSwaps)                 │
│  ├── welephantVault.update()           → Refresh TWAP oracle paths      │
│  ├── welephantVault.swapReserves()     → ETH→WELEPHANT (try/catch)      │
│  └── _distributePendingFees()          → Transfer when liquidity ready  │
│                                                                          │
│  WelephantVault                                                          │
│  ├── depositETH()                      → Receive ETH from game          │
│  ├── swapReserves()                    → Chunked swaps with try/catch   │
│  ├── withdrawWelephant()               → JIT delivery to consumers      │
│  └── canWithdraw()                     → Check available liquidity      │
│                                                                          │
│  WelephantSwap                                                           │
│  ├── update()                          → Update TWAP oracle paths       │
│  ├── estimateWelephantForETHTwap()     → TWAP-based price estimate      │
│  ├── swapExactETHForWrappedTWAP()      → Swap using TWAP path selection │
│  └── _getBestPathTWAP()                → Compare direct vs indirect     │
│                                                                          │
│  FeeAccounting                                                           │
│  ├── addAdminFee() / addStakingFee()   → Record (no transfer)           │
│  ├── distributeAdminFees()             → Return amount, zero state      │
│  └── distributeStakingFees()           → Return amount, zero state      │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

WelephantVault: Central Liquidity Hub

Purpose: Holds all WELEPHANT liquidity and orchestrates async ETH→WELEPHANT swaps.

Key Features:

Configuration:

uint256 defaultSwapChunk = 1 ether;      // Max ETH per swap
uint256 minSwapAmount = 0.01 ether;      // Dust prevention
uint256 maxSlippageBps = 500;            // 5% slippage tolerance

WelephantSwap: TWAP-Aware Swap Router

Purpose: Handles ETH↔WELEPHANT swaps with dual-path routing and TWAP oracle integration.

TWAP Oracle Integration:

Dual-Path Routing:

Direct Path:   WBNB → ELEPHANT
Indirect Path: WBNB → BUSD → ELEPHANT

Best path selected based on:
- TWAP prices (for crank/automated swaps)
- Spot prices (for user/UI swaps)

Price Functions:

Function Use Case Method
estimateWelephantForETHTwap() Crank accounting TWAP oracle
pathEthForCore() UI/user swaps Router spot prices
getWelephantPriceInUSD() Display Chainlink BNB/USD

Fee Model:

FeeAccounting: Async Fee Ledger

Purpose: Separates fee accounting from token transfers, enabling async distribution.

Flow:

1. Round Finalization (CrankFinalization._finalizeRound):
   feeAccounting.addAdminFee(amount)    → Recorded, not transferred
   feeAccounting.addStakingFee(amount)  → Recorded, not transferred

2. Crank Distribution (CrankFinalization._distributePendingFees, when liquidity available):
   amount = feeAccounting.distributeAdminFees()
   welephantVault.withdrawWelephant(feeCollector, amount)

   amount = feeAccounting.distributeStakingFees()
   welephantVault.withdrawWelephant(crankFinalization, amount)
   stakingVault.fundYield(amount)

Key Property: Fee accounting never blocks round finalization.

Safety Mechanisms Summary

Mechanism Location Benefit
Try/Catch WelephantVault.swapReserves() Crank never reverts on swap failure
TWAP Oracle WelephantSwap Resists price manipulation
Chunked Swaps WelephantVault Reduces single-swap exposure
Slippage Protection estimateWelephantForETHTwap() Guarantees minOut
Dust Prevention minSwapAmount Avoids gas-inefficient small swaps
Failure Tracking consecutiveSwapFailures Enables monitoring/alerting
Graceful Withdrawal withdrawWelephant() Returns false if insufficient
Async Fees FeeAccounting Decouples accounting from transfer

Simulator & Tooling Overview

The Prediction Mining system consists of:

raffle-mining/
├── contracts/                 # Smart contracts
│   ├── RaffleMining.sol       # Main game logic (stateless coordinator)
│   ├── RaffleMiningViews.sol  # Read-only view functions (includes paginated batch reads)
│   ├── RaffleMiningCrank.sol  # Crank orchestration (CrankCore)
│   ├── CrankFinalization.sol  # Round finalization, distribution, vault swaps
│   ├── RaffleMiningLib.sol    # Shared libraries
│   ├── RaffleMiningTypes.sol  # Operational types & constants
│   ├── GameHistoryTypes.sol   # Data-model types (decoupled from game engine)
│   └── RegistryTypes.sol      # Registry types (decoupled from game engine)
├── simulator/src/             # Simulator
│   ├── simulation.ts          # Main orchestration
│   ├── players.ts             # Player management (optimized)
│   ├── roundLogger.ts         # Round data reading & reporting (optimized)
│   ├── crank.ts               # Crank execution
│   └── deployer.ts            # Contract deployment
└── frontend/                  # Web UI

RPC Optimization Strategy

Design Principle

Never perform O(n) individual RPC calls when batch alternatives exist or can be created.

Each RPC call incurs:

For 100 participants with 2 calls each = 200 calls × 100ms = 20 seconds of blocking I/O.

Pagination Pattern (CRITICAL)

Never assume bounded data sizes. Any function returning arrays must use cursor-based pagination:

// WRONG - Naive, can fail with large n
function getRoundParticipants(uint256 roundId) returns (address[] memory)

// RIGHT - Paginated, safe for any n
function getRoundParticipantCount(uint256 roundId) returns (uint256 count)
function getRoundParticipants(uint256 roundId, uint256 offset, uint256 limit)
    returns (address[] memory participants, uint256 nextCursor)

Client-side pattern:

const PAGE_SIZE = 100;
let cursor = 0;
const allData: Data[] = [];

do {
  const [page, nextCursor] = await contract.getData(cursor, PAGE_SIZE);
  allData.push(...page);
  cursor = nextCursor;
} while (cursor > 0);  // nextCursor = 0 means no more data

Identified Anti-Patterns & Status

1. Round Logging - Player Info Fetching

Location: simulator/src/roundLogger.ts:265-346

Original Problem: Loop fetched player data one-by-one:

for (const participant of participants) {
  const playerInfo = await raffleMining.getPlayerRoundInfo(participant, roundId);  // RPC per participant
}

Impact: 1 RPC call × N participants

Status: ✅ OPTIMIZED (with pagination)

Solution Implemented:

const PAGE_SIZE = 100;
let cursor = 0;

do {
  // Single RPC call fetches a page of all data
  const [participants, infos, bets, nextCursor] =
    await raffleMiningViews.getPlayersRoundDataBatch(roundId, cursor, PAGE_SIZE);

  for (let i = 0; i < participants.length; i++) {
    // Process participant data...
  }
  cursor = nextCursor;
} while (cursor > 0);

Result: 200 RPC calls → ceil(n/100) calls (99%+ reduction, scales safely)


2. Auto-Play Registration - Repeated Contract Constants

Location: simulator/src/players.ts:169-207

Original Problem: Contract constants read inside loop for each player:

for (const player of autoPlayers) {
  const currentRoundId = await raffleMining.currentRoundId();        // Same for all!
  const gasUnitsPerPlay = await raffleMining.gasUnitsPerPlay();        // Same for all!
  const gasTaxBasisPoints = await raffleMining.gasTaxBasisPoints();       // Same for all!
  const defaultGasPrice = await raffleMining.defaultGasPrice();      // Same for all!
}

Impact: 4+ redundant RPC calls × N players

Status: ✅ OPTIMIZED

Solution Implemented: Cache constants before loop with parallel fetch:

// Fetch all constants in parallel ONCE before loop
[currentRoundId, gasTaxBasisPoints, defaultGasPrice] = await Promise.all([
  raffleMining.currentRoundId(),
  raffleMining.gasTaxBasisPoints(),
  raffleMining.defaultGasPrice()
]);

// Reuse cached values in loop
for (const player of autoPlayers) {
  // Uses cached currentRoundId, gasUnitsPerPlay, etc.
}

Result: 200+ RPC calls → 4 RPC calls (98% reduction for constants)


3. Reward Claiming - Balance Checks

Location: simulator/src/players.ts:500-544

Original Problem: Balance checked individually per player:

for (const player of this.players) {
  const balance = await raffleMining.playerBalances(player.address);  // RPC per player
}

Impact: 1 RPC call × N players

Status: ✅ OPTIMIZED

Solution Implemented:

// Single RPC call fetches all balances
const playerAddresses = this.players.map(p => p.address);
const balances = await raffleMiningViews.getPlayerBalancesBatch(playerAddresses);

for (let i = 0; i < this.players.length; i++) {
  const balance = balances[i];  // Already fetched
}

Result: 100 RPC calls → 1 RPC call (99% reduction)


Contract Batch Read Support

Cursor-Based Paginated Functions (Safe for Large N)

All paginated functions follow the pattern: functionPaginated(offset, limit)(data[], nextCursor)

Function Description Returns
getRoundParticipantCount(roundId) Count of participants uint256
getRoundParticipantsPaginated(roundId, offset, limit) Paginated addresses (address[], nextCursor)
getRoundSummariesPaginated(offset, limit) Paginated round summaries (RoundSummary[], nextCursor)
getPlayerRoundIdsPaginated(player, offset, limit) Player's round IDs (uint256[], nextCursor)
getPlayerRoundHistoryPaginated(player, offset, limit) Player's round history (PlayerRoundInfo[], nextCursor)
getPlayersRoundDataBatch(roundId, offset, limit) Combined paginated batch (address[], PlayerRoundInfo[], nextCursor)

Count Functions (For Total Size)

Function Description Returns
getRoundParticipantCount(roundId) Total participants in round uint256
getPlayerRoundIdCount(player) Total rounds player participated in uint256
getTotalRoundSummaries() Total finalized rounds uint256

Address-Based Batch Functions (Caller Controls Size)

Function Description Notes
getPlayerBalancesBatch(addresses[]) Multiple player balances Caller provides addresses, controls batch size

Legacy Functions (Convenience, Bounded Use Only)

These exist for simple use cases with known-small N:

Function Notes
getRoundParticipants(roundId, offset, limit) Paginated but no cursor return - use Paginated variant for large N
getRoundSummaries(offset, limit) No cursor return - use Paginated variant for large N
getRecentRounds(count) Safe if count is bounded

Removed Functions (previously unsafe O(n)):


Optimization Summary

Achieved Impact

Scenario Players Before After Reduction
Round logging 100 200 calls 1 call 99.5%
Round logging 10,000 20,000 calls 100 calls 99.5%
Auto-play registration (constants) 50 200 calls 4 calls 98%
Reward claim checks 100 100 calls 1 call 99%

Implementation Completed


Other Design Decisions

Crank Architecture: CrankCore + CrankFinalization

The crank is split into two deployed contracts to stay under the 24,576 byte EVM contract size limit:

RaffleMiningCrank (CrankCore) — Orchestration:

CrankFinalization — Finalization & distribution:

Cross-contract delegation: CrankCore calls CrankFinalization via the ICrankFinalization interface. CrankFinalization restricts these calls via onlyCrankCore modifier. Both contracts share the same storageContract (IRaffleMiningWritableStorage) immutable reference.

Stateless RaffleMining (Upgrade Path)

RaffleMining holds no mutable game state between rounds. All persistent state lives in dedicated registry and storage contracts:

State Contract
Auto-play registrations, active user set, ETH balances AutoPlayRegistry
Pending plays queue + ETH PendingPlaysRegistry
Round history, pots, player stats GameHistory
Entropy chain, finalization results Verification

Upgrade procedure: Pause RaffleMining, wait for the crank to finish processing, deploy a new RaffleMining, update each registry's gameContract pointer to the new address. All player state (funds, registrations, pending plays, history) remains intact.

All batch operations use a single-level state machine (cursor + inProgress flag). There are no nested state machines — inline operations like auto-staking and ETH looping are processed within the parent distribution loop, ensuring the cursor always advances and the distribution always completes.

Event-Based Data Collection

Where possible, prefer reading events over direct state queries:

Parallel RPC Calls

When multiple independent reads are needed, use Promise.all():

const [a, b, c] = await Promise.all([
  contract.methodA(),
  contract.methodB(),
  contract.methodC()
]);

This executes calls concurrently rather than sequentially.

ethers.js Frozen Objects

ethers.js returns frozen Result objects from contract calls. When iterating over batch results:

// Convert to mutable values
const value = BigInt(result[i].field);
const flag = Boolean(result[i].boolField);
const addr = String(result[i].addressField);

Security Q&A

Can an attacker receive outsized rewards?

No. The system has layered defenses that prevent any single actor from extracting disproportionate value:

Attack Vector Defense
Predict the winning square Seed is masked during the round and finalized with entropy from 6 future blocks that don't exist at play time
Flash loan a massive play Plays close at round end; finalization happens 6+ blocks later. A loan cannot span blocks
Front-run round finalization Plays placed after the round timer expires go to the next round's pending queue, not the current round
Manipulate reward share Winner payouts are purely proportional — (yourBet / totalBetsOnSquare) × winnerPool — immutable after the play is recorded
Claim rewards twice CEI pattern zeroes balances before any transfer; nonReentrant on all claim functions
Play small on every square to guarantee single-winner prize Single winner is selected weighted by play amount, not by player count. A 0.0001 BNB play gets ~0.0001 BNB worth of probability
Predict payday rounds Payday is derived from the same masked, post-cooldown seed as the winning square. Cannot be known before plays close
Crank operator selects favorable outcomes Randomness is finalized from block data that post-dates the closing block. The crank processes players in deterministic sequential order — it cannot skip or reorder
JIT yield snipe on StakingVault The crank calls fundYield() every ~60 seconds with small, predictable amounts. Per-round yield is too small relative to gas costs for sandwich attacks to be profitable. The continuous drip cadence acts as an application-layer mitigation without requiring time-locks or entry fees, preserving the vault's fully permissionless design
AutoPlay register/withdraw DDoS AutoPlayRegistry has no direct withdraw() — players must use cancelAutoPlay() which cleans up both ETH balance and registration state atomically. cancelAutoPlay() requires at least 1 round played (E85), preventing register-then-cancel churn

The strongest architectural defense is time separation: plays close at round end, then a 6-block cooldown passes, then the seed is finalized using entropy that didn't exist when plays were placed. There is no single-transaction path from "place play" to "collect winnings."


Changelog

Date Change Author
2025-12-02 Initial architecture doc - documented O(n) anti-patterns Claude
2025-12-02 Implemented batch functions, updated simulator Claude
2025-12-02 Converted to cursor-based pagination for unbounded safety Claude
2025-12-02 Added full pagination coverage: getPlayerRoundIdsPaginated, getRoundParticipantsPaginated, getRoundSummariesPaginated, getPlayerRoundHistoryPaginated Claude
2026-01-23 Removed O(n) unbounded functions from RaffleMiningViews: getRoundParticipants(roundId), getPlayerRoundHistory, getPlayerRecentRounds. Fixed getPlayerTotalRounds and getPlayerDetailedStats to use O(1) count functions. Claude
2026-01-29 Added Async Swap Architecture section documenting WelephantVault, WelephantSwap TWAP integration, FeeAccounting, and safety mechanisms Claude
2026-02-03 Added Incremental Fee Forwarding section documenting anti-front-running mechanism Claude
2026-02-07 Updated distribution docs: inline auto-stake/ETH-loop replace nested flush queues (crank bricking fix) Claude
2026-02-07 Removed reward batch accumulation: all deposits (ETH, WELEPHANT, auto-stake) are now per-winner inline. Auto-stake uses try/catch for universal failure handling Claude
2026-02-06 Replaced XOR entropy accumulation with hash-chaining (keccak256) in Verification.updateSeed(); added block.prevrandao to playEntropy and autoplay square selection Claude
2026-02-07 Comprehensive ACL documentation: added DEPOSITOR_ROLE, Ownable contracts, delegated admin checks, role assignment flow diagram. Updated WelephantRewards to reflect partial claims and removed batch deposit references Claude
2026-02-13 Documented CrankCore + CrankFinalization split: RaffleMiningCrank split into two contracts to stay under 24,576 byte EVM limit. Updated contract diagram, core contracts table, access control roles (CONSUMER/CRANK/DISTRIBUTOR now to CrankFinalization), role assignment flow, async swap architecture, and file listing Claude
2026-02-13 Added JIT yield sniping defense to Security Q&A: crank's 60-second fundYield() cadence mitigates sandwich attacks without time-locks or entry fees Claude
2026-02-13 Removed AutoPlayRegistry.withdraw() DDoS vector (AP-07): added anti-churn defense to Security Q&A, updated vault description Claude
2026-02-17 Replaced per-play gas unit model with flat gas tax (gasTaxBasisPoints). Removed gasUnitsPerPlay and gasChargeMultiplier. Added 10% buffer threshold before sweeping gas charge excess to WelephantVault — excess pads buybacks without changing fee split economics Claude
2026-02-24 Increased gas tax from 0.1% (10 bps) to 1% (100 bps) to ensure crank gas costs are covered at lower play volumes Claude
2026-02-19 RaffleMining is now stateless: auto-play state (structs, active set, settings, ETH) moved to AutoPlayRegistry; pending plays moved to PendingPlaysRegistry. AutoPlayDeposit retired. RaffleMining can be upgraded by pausing, finishing the crank, deploying a new instance, and updating registry gameContract pointers Claude
2026-02-22 Types decoupling: split RaffleMiningTypes into three independent files. GameHistoryTypes.sol isolates data-model structs (RoundSummary, PlayerStats, etc.) so GameHistory is unaffected by operational type changes. RegistryTypes.sol isolates registry structs (AutoPlay, PendingPlay, PlayerAutoPlaySettings) so AutoPlayRegistry and PendingPlaysRegistry are unaffected by game engine type changes. RaffleMiningTypes.sol retains only operational types (PendingWork, RoundPhase, constants) Claude
2026-02-22 Added Ownership Model & Renouncement section: documented permissionless asset layer (StakingVault, WelephantSwap), administered game layer (AccessControl contracts), Ownable contracts, delegated admin contracts, and why full renouncement is not appropriate for the game layer. Consolidated delegated admin and cross-contract access control documentation into the new section Claude
2026-03-07 Added Bitmask Storage Model section: documented uniform amount constraint (gameplay compromise), 2-SSTORE vs 32-push gas savings, squareTotals maintenance, one-play-per-round enforcement, and encoding details Claude