Launch and rewardsLottery manager
Lottery manager
The shared service that rolls a Chainlink VRF lottery on every qualifying ShareOFT buy and pays winners from the creator's gauge pot.
LotteryManager4626 turns each qualifying ShareOFT () buy, and each free AMOE entry, into an instant lottery roll. It resolves each creator's stack from the registry, draws randomness from Chainlink VRF 2.5, and pays winners in ShareOFT from the triggering vault's gauge pot.
At a glance
| Role | Instant lottery on hub-chain ShareOFT buys plus AMOE (no purchase required) entries |
| Deployed | Shared, one per chain |
| Key relationships | Resolves stacks via Registry4626; triggered by ShareOFT buys; pays from the gauge controller jackpotReserve; reads CreatorOracle for share pricing, not ticket size |
How it works
- Fund the pot. Every qualifying buy pays the 6.9% fee to the gauge; 69% of that fee stays in the gauge's
jackpotReserveas ShareOFT. - Enter. The buy calls
processSwapLotteryin the same transaction. AMOE entries arrive throughprocessAmoeEntryfrom the authorized relayer (the ZK router'ssubmitAmoeEntryZKpath). - Price the ticket. The stake is the USD the buyer actually spent, before fees. The win chance follows the formula below.
- Roll. Each entry requests Chainlink VRF and is an independent roll.
- Pay or miss. On a win, the manager calls the gauge's
payJackpotand the winner receives 69% of the triggering vault's pot in ShareOFT. On a miss, nothing is paid.
Win chance
Before boosts, winChancePPM = swapValueUSD / 250_000, capped at baseCeilingPPM (40,000 PPM). Put simply, $1 traded is about a 0.0004% instant win chance.
| Trade size | Win chance |
|---|---|
| $1 | 0.0004% |
| $100 | 0.04% |
| $1,000 | 0.4% |
| $10,000 | 4% (pre-boost ceiling) |
Solana remote entries scale that base chance by 400 / 690 (Raydium's published 4% tier vs Base's 6.9% buy fee) before boosts. Other EVM remotes use the Base formula, since they pay the same 6.9% buy fee. Only Solana odds are discounted; stake USD and jackpot EV stay on the true venue amount.
Stake is before fees
amountUSD is the cash the buyer spent: the gross quote times this chain's neutral price feed. Do not subtract the pool fee.
| Path | Stake | Payload |
|---|---|---|
| Base hub | LotteryVenueQuoteBuffer quote × feed | processSwapLottery (same tx) |
| Solana | quote_lamports × SOL/USD | V3 word 6 (nonzero) |
| Other EVM remotes | Same-tx buffer quote × that chain's USDC ($1) or WETH × ETH/USD | V4 word 8 (venueAmountUsd1e6); word 6 stays coverage |
| AMOE | pointsBurnedAsUSD (no pool fee) | ZK / relayer |
Un-upgraded EVM spokes still send V3 and use share × CreatorOracle. If fee tiers change, update only the 400 / 690 constants (SOLANA_TRADE_FEE_BPS, BASE_TRADE_FEE_BPS); do not haircut USD.
A Solana message creates a ticket only when V3 word 6 is a positive venue-USD amount. A zero word returns after RemoteLotteryEntryReceived and emits no LotteryEntryCreated. The submit worker leaves that inbox row queued. With SOLANA_LOTTERY_VENUE_USD_ENABLED off (the default) the reason is solana_lottery_venue_usd_disabled. With the flag on, a zero or unresolvable gross quote uses solana_lottery_venue_usd_missing. The flag default stays off. USD comes from the inbox quote_lamports (gross SOL/WSOL); the hook ring does not carry it.
Prize payout
By default (singleVaultJackpotOnly = true), a winner gets rewardPercentage (default 69%, 6900 bps) of the triggering vault's gauge jackpotReserve, paid in ShareOFT through payJackpot. Multi-vault scanning happens only if singleVaultJackpotOnly is disabled. The payout is always ShareOFT, never vault claims (). See jackpot payout.
Each buy has its own request and stored odds. Pending requests do not earmark shares. A winning callback sizes its prize against the remaining reserve at settlement, subject to the existing fair-EV cap. For example, two uncapped wins against an initial 1,000-share reserve pay 690 shares and then 213.9 shares, even if their callbacks arrive in reverse request order.
The repository manager now emits LotteryEntryCreated for a successful paid entry, as well as AMOE. A buy transaction has no randomness result yet. Match the event's namespaced request ID to LotteryResultProcessed; the raw local consumer ID is converted with keccak256(abi.encode("LOCAL", rawRequestId)). A zero return from the entry call means no request was created, including unavailable VRF funding or an ineligible venue quote.
A failed positive jackpot transfer reverts manager settlement, preserving its request and reserve. VRFConsumer4626 retains the fulfilled random word and exposes permissionless retryLocalCallback(rawRequestId). While the manager is paused, callbacks instead store randomness in its FIFO queue; after unpause, permissionless applyDeferredVrf or processDeferredVrfBatch settles that queue. A failed deferred payout keeps the same FIFO head for another attempt.
The existing grace policy is unchanged: vrfResultGracePeriod defaults to four hours from request creation. An active-manager callback arriving after that deadline settles as an explicit stale loss. This also applies to a retry of an earlier failed payout, so operators must recover those callbacks before the deadline. Randomness received during a manager pause follows the deferred queue and has its timestamp refreshed on settlement. These are repository defaults, not a claim about verified live settings.
Cross-chain fee sponsorship
A hybrid model keeps VRF messaging fees bounded:
- Caller-funded (default).
msg.valuemust equal the integrator-quotednativeFee. If the VRF send fails, the fee is refunded and the roll is skipped. - Sponsored fallback (opt-in). If
msg.value == 0and sponsorship is enabled, the manager pays from its own balance under policy limits.
Key functions
| Function | What it does | Who can call |
|---|---|---|
processSwapLottery(buyer, tokenIn, amountIn, buyerCurrentShareBalance) | Creates an entry for a qualifying buy and dispatches VRF (payable for the VRF fee) | Authorized swap contracts |
processAmoeEntry(buyer, token, pointsBurnedAsUSD) | Creates a no-purchase AMOE entry | authorizedAmoeRelayer only |
receiveRandomWords(requestId, randomWords) / receiveRandomWords(randomWords, sequence) | VRF callbacks (local and cross-chain) that resolve entries | VRF consumer / integrator |
calculateWinChance(swapAmountUSD) | Pre-boost win chance in PPM | View |
setLotteryConfig(minSwap, rewardPercentage, isActive, baseWinChance, maxWinChance, usdMultiplierBps) | Lottery parameters; rewardPercentage changes are timelocked | Owner |
setSponsoredVrfMinSwapAmountUSD(minSwapAmountUSD) | Minimum swap for sponsored VRF | Owner |
setSponsorshipRateLimits(vrfMaxPerBuyerPerEpoch, vrfMaxPerOriginPerEpoch, callbackMaxPerBuyerPerEpoch, callbackMaxPerOriginPerEpoch) | Per-epoch sponsorship caps | Owner |
setVrfSponsorshipPolicy(enabled, maxFeePerMessage, budgetPerEpoch, epochDuration) | VRF sponsorship on/off and budget | Owner |
setCallbackSponsorshipPolicy(enabled, maxFeePerMessage, budgetPerEpoch, epochDuration) | Winner-callback sponsorship on/off and budget | Owner |
Numbers
| Parameter | Value |
|---|---|
| Base win chance | min(swapUSD / 250_000, 40_000) PPM (4% max at a $10k swap) |
| Solana odds scalar | 400 / 690 |
| Win payout | 69% of the triggering pot (rewardPercentage 6900 bps of pot) |
| Personal boost (once armed) | Up to 2.5× |
| Sponsorship | Off by default for both VRF and callbacks |
| Sponsored VRF minimum swap | $10 (sponsoredVrfMinSwapAmountUSD) |
| Sponsorship rate limits per epoch | VRF: 2 per buyer, 10 per origin · Callback: 1 per buyer, 10 per origin |
The 69% win payout is a share of the pot. It is a different number from the 69% slice of the buy fee that funds the pot.
Gotchas
- Boost is not live. As of 2026-09-05, v1 is live with
boostManagerandve4626GaugeVotingataddress(0). That is expected on the live canary: the personal multiplier is 1.0× and the gauge add is 0. - v2 is documented, not live. Personal boost stays at 1.0× until it is armed. v2 is classic Curve on
swapEff = min(swapUSD, 10_000)withD = 100_000, coverage from locked 4626 USD (not decaying ve-units), and a unified 24h UTC consume at 1× veUSD across all chains. Full formula: Lottery personal boost v2. - Share-coverage code in v1 is not v2. Arming the live Share-coverage
_applyBoostpath (l = min(creatorShareUSD, swapUSD),L= creator Share supply) is not v2. The gauge lottery pot stays a separate additive. - Boost order. Personal Curve boost, optional gauge probability, and
maxWinChanceapply after the base formula, once armed. Kernel bounds: ve4626, 2.5× proof, Lean targets. - CreatorOracle does not size tickets. Stake comes from the same-transaction venue quote. A missing quote fails closed; there is no CreatorOracle fallback for stake.
- Legacy AMOE path is off.
LotteryAmoeRouter.submitAmoeEntry(ECDSA) always reverts;submitAmoeEntryZKis the live path. - Deploy order. USD pricing and oracle guards live in
LotteryManager4626PricingLib(an external library call) so the manager stays under EIP-170. Deploy the library first (DeployLotteryManagerCreate2*.s.sol), then the manager. Foundry links the library at CREATE2 (EIP-2470,create2_library_saltdefault0). As of 2026-09-05, that pin is0x1d74A8e2…7C6Dfor the library initcode. Bytecode manifests must fully link placeholders viascript/lib/extract_linked_bytecode.py.