Smart Contract

This is the high-level overview of the audited smart contract:

  • Liquidity providers deposit liquidity at any time. Their liquidity starts guaranteeing a minimum jackpot starting the next raffle.

  • Players can buy raffle tickets at any time.

  • Referrers can claim fees instantly after a ticket is purchased.

  • The raffle drawing is done every 24 hours. A winning ticket is drawn, liquidity providers' balances are updated, fees are distributed to LPs, and then a new minimum jackpot is initialized.

  • LPs can initiate the process to withdraw liquidity at any time, but it takes one raffle drawing to complete.

Deposit liquidity

This is done in lpDeposit which takes in an integer riskPercentage

  • riskPercentage is the variance that an LP chooses. This percentage of the liquidity guarantees the minimum jackpot and must be between 1 and 100.

  • At deposit time, the entirety of the deposits goes into the LP reserves, known as principal

Notes:

  • The risk amount of LP reserves turns into tickets in stakeLps, which is only run after the daily raffle drawing at runLottery

  • lpDeposit is also used when an LP deposits more or changes their variance.

Purchase Ticket

This is done in purchaseTickets which takes in an address referrer

  • referrer is the entity that got a user to buy a ticket. This can be an influencer or a front end to the raffle protocol. Requiring a referrer incentivizes KOLs and users to drive traffic to the protocol, while providing baseline revenue for frontends.

  • Tickets can only be purchased in increments of TICKET_PRICE, which is 0.001 ETH.

  • LP fees and referrer fees are set aside at ticket purchase. The remainder is turned into tickets, where 10000 tickets equal 1 TICKET_PRICE. Thus, a user receives 10000 - feeBps number of tickets.

  • For all cases counting tickets, since they are multiplied by 10000, Bps is added to the var name for clarity (eg. ticketCountTotalBps) This has no impact on WEI-denominated var like userPoolTotal

Claim referrer fees

This is done in withdrawReferralFees . If the address has any amount to withdraw via referralFeesClaimable(address), it can be withdrawn immediately.

Note, referralFeeTotal is amount from all referrers collected from the current raffle.

Raffle Drawing

Context

  1. Users buy tickets from the contract. The total number of tickets is represented as userPoolTotal

  2. LPs' guarantee for the jackpot is represented as lpPoolTotal

  3. Every 24 hours, we schedule a backend request to run the lottery.

    1. Note: This is sufficiently decentralized. Anyone is able to run the lottery after 24 hours has elapsed. This means if our backend service stops working, anyone can run the lottery. We will improve on this more.

  4. To ensure randomness, we use Pyth's entropy protocol.

    1. To request a random number, we send a transaction with a random 32-byte hexadecimal number generated off-chain and receive a sequence number. Entropy calls back the contract with the generated random number once the request is fullfilled by the provider.

The raffle drawing occurs in runLottery and there are three situations:

  1. Players bought more tickets than the LPs' jackpot guarantee

  2. Players bought less tickets than the LPs jackpot guarantee, and players win

  3. Players bought less tickets than the LPs' jackpot guarantee, and LPs win

Before each situation, LP fees are distributed to LPs via distributeLpFees. If no LPs have staked for that round, LP fees are distributed to the userPoolTotal.

Case #1: Players bought more tickets than the LPs' jackpot guarantee

  • A random number is generated between 1 and the number of tickets bought by users.

  • The jackpot amount userPoolTotal, which already does not include LP fees and referrer fees, is set to be claimable by the user.

  • LPs get their guarantee back fully via returnLpPoolBackToLps

Case #2: Players bought less tickets than the LPs jackpot guarantee, and players win

  • A random number is generated between 1 and the number of tickets guaranteed by LPs. Note, this is multiplied by 10000x to match the number of tickets per TICKET_PRICE.

  • If the random number is less than the number of tickets bought by users, represented as ticketCountTotal, then users win!

  • Jackpot amount lpPoolTotal is set to be claimable by the user.

  • Users' entries is distributed to LPs via distributeUserPoolToLps

Case #3: Players bought less tickets than the LPs jackpot guarantee, and LPs win

  • Users' entries is distributed to LPs via distributeUserPoolToLps

  • LP pool is returned to LPs in returnLpPoolBackToLps

After one of the above cases is run:

  • Reset all variables

  • Initialize the next guaranteed jackpot in stakeLps. For every active LP, we take their reserves (lp.principal) and multiply it by riskPercentage, in lp.stake

How to run the Lottery

Our contract is decentralized -- anyone can run the lottery every 24 hour. The core team has a scheduled run that does it, in case anything happens, anyone can run it. Here are the steps:

Kick off the lottery run, which generates a random number from Pyth

  1. Go to Blastscan > Write Proxy Contract > RunLottery

  2. For runLottery - payableAmount (ether) , enter in 0.000015000000000002

    1. Fetch this from Blastscan > Read Proxy Contract > getLotteryFee

    2. Convert this from WEI to ETH, thus, divide by 10^18

  3. For userRandomNumber - bytes32, enter in 0x55fb29339a98ca25bedb7b5aa225041f669ca1407e926a95ce4a9b080ac66907

    1. Note: reusing this is fine, Pyth generates randomness, this seed further maximizes randomness

    2. You can generate your own with Web3.utils.randomHex(32))

Request callback from Pyth, which runs the lottery

  1. Go to Blastscan > Pyth's Blast contract > RevealwithCallback

  2. For these arguments:

    • provider (address)

    • sequenceNumber (uint64)

    • userRandomNumber (bytes32). Add 0x in front

    • Fetch the values from the RequestedWithCallback log from above. This event is emitted with the runLottery method on our contract is called. Make sure to pass them with the correct signatures.

  3. For the last argument, providerRevelation (bytes32):

Withdraw Liquidity

This is done in withdrawAllLP LP reserves can only be withdrawn in full, and cannot be withdrawn partially. This two-step process requires one raffle drawing to occur before liquidity can be withdrawn.

If LP has any liquidity staked, set riskPercentage = 0. The function ends at this time.

After the raffle is run, LP should have no liquidity staked (lp.stake = 0) because stakeLps does not allocate any liquidity if riskPercentage = 0. At this time, we return funds to the LP.

Withdraw winnings

This is done in withdrawWinnings Users can claim their winnings at any time.

Protocol Fee

There is a protocol fee that is only enabled when this protocol reaches massive scale, defined as LP fees are greater than 1 ETH per day. With a LP fee of 2.5%, that means ~$160k of tickets are sold. Only then will 1/10 of LP fees be distributed to the protocol treasury.

FAQ

Why is the contract upgradeable?

We are launching a new protocol and there are many things we cannot anticipate. We want to have the ability to make changes to benefit LPs and players.

We take operational security seriously. Our founder is doxxed (@Patrick_Lung on X), has a strong reputation (ex. Uniswap, Microsoft), and has been in crypto for years.

What changes do you want to make to the smart contract?

  • Increase LP and user limits

    • Optimize search functionality. We have a limit on LPs and user limits so we don't hit block gas limits

  • Allow for delegation

    • This would enable players to buy tickets from any chain. In essence, it bridges funds and buys on behalf of a user in one transaction. It also enables APIs like Farcaster to "Buy with Warps".

  • Explore Diamond standard

    • EIP-2535 allows for modular upgradeability, letting us improve the contract while providing the trust that immutable logic guarantees

Last updated