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 atrunLottery
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 areferrer
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 receives10000 - 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 likeuserPoolTotal
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
Users buy tickets from the contract. The total number of tickets is represented as
userPoolTotal
LPs' guarantee for the jackpot is represented as
lpPoolTotal
Every 24 hours, we schedule a backend request to run the lottery.
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.
To ensure randomness, we use Pyth's entropy protocol.
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:
Players bought more tickets than the LPs' jackpot guarantee
Players bought less tickets than the LPs jackpot guarantee, and players win
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 byriskPercentage
, inlp.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
Go to Blastscan > Write Proxy Contract > RunLottery
For
runLottery - payableAmount (ether)
, enter in0.000015000000000002
Fetch this from Blastscan > Read Proxy Contract > getLotteryFee
Convert this from WEI to ETH, thus, divide by 10^18
For
userRandomNumber - bytes32
, enter in0x55fb29339a98ca25bedb7b5aa225041f669ca1407e926a95ce4a9b080ac66907
Note: reusing this is fine, Pyth generates randomness, this seed further maximizes randomness
You can generate your own with
Web3.utils.randomHex(32)
)
Request callback from Pyth, which runs the lottery
Go to Blastscan > Pyth's Blast contract > RevealwithCallback
For these arguments:
provider (address)
sequenceNumber (uint64)
userRandomNumber (bytes32). Add
0x
in frontFetch the values from the
RequestedWithCallback
log from above. This event is emitted with therunLottery
method on our contract is called. Make sure to pass them with the correct signatures.
For the last argument, providerRevelation (bytes32):
Go to the following endpoint: and use the sequence number: https://fortuna.dourolabs.app/v1/chains/blast/revelations/[sequenceNumber]
You will find the
providerRevelation
in the data field. Add0x
in front
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