Smart Contract

PrizeVaultIncrement

A.262cf58c0b9fbcff.PrizeVaultIncrement

Valid From

130,545,857

Deployed

5d ago
Feb 21, 2026, 02:27:10 PM UTC

Dependents

4 imports
1/*
2PrizeVault Increment - A no-loss lottery system on Flow blockchain
3
4Users deposit FLOW tokens into the vault. The vault stakes these tokens on Increment Labs
5to generate yield. The staking rewards are periodically distributed as prizes to randomly 
6selected depositors using Flow's VRF for verifiable randomness.
7
8Users can withdraw their principal deposits at any time (subject to unstaking period).
9
10Key features:
11- Deposit FLOW tokens
12- Automatic staking via Increment Labs liquid staking
13- Prize distribution using commit-reveal randomness
14- Principal withdrawal with two-phase process (request + complete)
15
16In essence, users retain ownership of their principal deposits while participating 
17in periodic prize draws funded by the staking rewards — creating a lossless lottery model.
18*/
19
20import FungibleToken from 0xf233dcee88fe0abe
21import FlowToken from 0x1654653399040a61
22import RandomConsumer from 0x45caec600164c9e6
23
24import stFlowToken from 0xd6f80565193ad727
25import LiquidStaking from 0xd6f80565193ad727
26
27// Increment Finance Swap Contracts
28import SwapConfig from 0xb78ef7afa52ff906
29import SwapInterfaces from 0xb78ef7afa52ff906
30import SwapFactory from 0xb063c16cac85dbd1
31
32access(all) contract PrizeVaultIncrement {
33    
34    // Constants
35    access(all) let minimumDeposit: UFix64
36    
37    // Events
38    access(all) event Deposited(address: Address, amount: UFix64)
39    access(all) event WithdrawalRequested(address: Address, amount: UFix64, withdrawalType: String)
40    access(all) event Withdrawn(address: Address, amount: UFix64)
41    access(all) event InstantWithdrawn(address: Address, stFlowAmount: UFix64, flowReceived: UFix64)
42    access(all) event Staked(amount: UFix64, stFlowReceived: UFix64)
43    access(all) event UnstakeRequested(amount: UFix64, voucherUUID: UInt64, unlockEpoch: UInt64)
44    access(all) event PrizeDrawCommitted(prizeAmount: UFix64, commitBlock: UInt64, receiptID: UInt64)
45    access(all) event PrizeAwarded(winner: Address, amount: UFix64, round: UInt64, commitBlock: UInt64, receiptID: UInt64)
46    access(all) event DepositReceiverCreated(address: Address)
47    access(all) event BlocksPerMonthUpdated(oldValue: UInt64, newValue: UInt64)
48    
49    // Paths
50    access(all) let DepositReceiverStoragePath: StoragePath
51    access(all) let DepositReceiverPublicPath: PublicPath
52    access(all) let VaultStoragePath: StoragePath
53    access(all) let AdminStoragePath: StoragePath
54    access(all) let PrizeDrawReceiptStoragePath: StoragePath
55    access(all) let WithdrawVoucherCollectionStoragePath: StoragePath
56    
57    // State
58    access(all) var totalDeposited: UFix64
59    access(all) var totalStaked: UFix64  // In FLOW terms (original amount staked)
60    access(all) var totalRewardsHarvested: UFix64
61    access(all) var totalPrizesDistributed: UFix64
62    access(all) var prizeRound: UInt64
63    access(all) var lastDrawBlock: UInt64  // Block height of last draw
64    access(all) var blocksPerMonth: UInt64  // Configurable blocks between draws (~30 days)
65    access(self) var monthlyDrawReceipt: @PrizeDrawReceipt?  // Stored receipt for monthly draw
66    
67    // Mappings
68    access(self) let userDeposits: {Address: UFix64}  // Original deposits only
69    access(self) let userPrizes: {Address: UFix64}    // Prizes won
70    access(self) let pendingWithdrawals: {Address: UFix64}
71    access(self) let prizeHistory: {UInt64: Address}
72    
73    // Main vault to hold all deposited FLOW tokens (liquid FLOW only)
74    access(self) let vault: @FlowToken.Vault
75    
76    // stFlowToken vault to hold staked tokens
77    access(self) let stFlowVault: @stFlowToken.Vault
78    
79    // WithdrawVoucher collection to manage unstaking vouchers
80    access(self) let voucherCollection: @LiquidStaking.WithdrawVoucherCollection
81    
82    // RandomConsumer for commit-reveal randomness
83    access(self) let consumer: @RandomConsumer.Consumer
84    
85    // Receipt resource for prize draw commit-reveal
86    access(all) resource PrizeDrawReceipt {
87        access(all) let prizeAmount: UFix64
88        access(self) var request: @RandomConsumer.Request?
89        
90        init(prizeAmount: UFix64, request: @RandomConsumer.Request) {
91            self.prizeAmount = prizeAmount
92            self.request <- request
93        }
94        
95        // Get the block height at which randomness was committed
96        access(all) view fun getRequestBlock(): UInt64? {
97            return self.request?.block
98        }
99        
100        // Pop the request for fulfillment (can only be called once)
101        access(contract) fun popRequest(): @RandomConsumer.Request {
102            let request <- self.request <- nil
103            return <- request!
104        }
105    }
106    
107    // Admin resource to manage contract configuration
108    access(all) resource Admin {
109        // Commit phase: Start a prize draw (returns receipt to be used in reveal)
110        access(all) fun commitPrizeDraw(prizeAmount: UFix64): @PrizeDrawReceipt {
111            return <- PrizeVaultIncrement.commitPrize(amount: prizeAmount)
112        }
113        
114        // Reveal phase: Complete the prize draw using the receipt
115        access(all) fun revealPrizeDraw(receipt: @PrizeDrawReceipt) {
116            PrizeVaultIncrement.revealPrize(receipt: <- receipt)
117        }
118        
119        // Update the blocks per month interval (for adjusting draw frequency)
120        // Default: 2592000 blocks ≈ 30 days (at ~1 second block time on Flow)
121        access(all) fun setBlocksPerMonth(newBlocksPerMonth: UInt64) {
122            assert(newBlocksPerMonth > 0, message: "Blocks per month must be greater than 0")
123            assert(newBlocksPerMonth >= 10000, message: "Minimum 10,000 blocks (~3 hours) to prevent spam")
124            assert(newBlocksPerMonth <= 5184000, message: "Maximum 5,184,000 blocks (~60 days)")
125            
126            let oldValue = PrizeVaultIncrement.blocksPerMonth
127            PrizeVaultIncrement.blocksPerMonth = newBlocksPerMonth
128            
129            emit BlocksPerMonthUpdated(oldValue: oldValue, newValue: newBlocksPerMonth)
130        }
131        
132        // Cashout any mature vouchers from the collection
133        access(all) fun cashoutMatureVouchers() {
134            PrizeVaultIncrement.processMatureVouchers()
135        }
136    }
137    
138    // Public interface that users can expose
139    access(all) resource interface DepositReceiverPublic {
140        access(all) fun deposit(from: @{FungibleToken.Vault})
141        access(all) fun requestDepositWithdrawal(amount: UFix64)
142        access(all) fun requestPrizeWithdrawal(amount: UFix64)
143        access(all) fun completeWithdrawal(): @{FungibleToken.Vault}
144        access(all) fun instantWithdrawDeposit(amount: UFix64, minFlowOut: UFix64): @{FungibleToken.Vault}
145        access(all) fun getBalance(): UFix64  // Total balance (deposits + prizes)
146        access(all) fun getDepositBalance(): UFix64  // Deposits only
147        access(all) fun getPrizeBalance(): UFix64  // Prizes only
148        access(all) fun getPendingWithdrawal(): UFix64
149    }
150    
151    // DepositReceiver resource that users create to interact with the vault
152    access(all) resource DepositReceiver: DepositReceiverPublic {
153        // Track user's balance
154        access(self) var balance: UFix64
155        
156        init() {
157            self.balance = 0.0
158        }
159        
160        // Deposit FLOW tokens into the vault
161        access(all) fun deposit(from: @{FungibleToken.Vault}) {
162            // Get the owner's address
163            let ownerAddress = self.owner?.address ?? panic("No owner address")
164            
165            // Cast to FlowToken.Vault
166            let flowVault <- from as! @FlowToken.Vault
167            let amount = flowVault.balance
168            
169            // Enforce minimum deposit to prevent dust/sybil attacks
170            assert(amount >= PrizeVaultIncrement.minimumDeposit, 
171                   message: "Minimum deposit is ".concat(PrizeVaultIncrement.minimumDeposit.toString()).concat(" FLOW"))
172            
173            // Stake the tokens via Increment Labs
174            PrizeVaultIncrement.stakeTokens(flowVault: <- flowVault)
175            
176            // Update user's balance
177            self.balance = self.balance + amount
178            
179            // Update contract state
180            PrizeVaultIncrement.userDeposits[ownerAddress] = self.balance
181            PrizeVaultIncrement.totalDeposited = PrizeVaultIncrement.totalDeposited + amount
182            
183            emit Deposited(address: ownerAddress, amount: amount)
184            emit DepositReceiverCreated(address: ownerAddress)
185        }
186        
187        // Request deposit withdrawal: Initiates unstaking from Increment Labs
188        // This requires an unstaking period (typically 2-3 epochs) before funds are available
189        access(all) fun requestDepositWithdrawal(amount: UFix64) {
190            let ownerAddress = self.owner?.address ?? panic("No owner address")
191            
192            let userDeposit = PrizeVaultIncrement.userDeposits[ownerAddress] ?? 0.0
193            assert(userDeposit >= amount, message: "Insufficient deposit balance. Your deposit: ".concat(userDeposit.toString()))
194            
195            let existingPending = PrizeVaultIncrement.pendingWithdrawals[ownerAddress] ?? 0.0
196            assert(existingPending == 0.0, message: "You already have a pending withdrawal. Complete it first.")
197            
198            // Verify totalStaked accounting (safety check to prevent underflow)
199            assert(PrizeVaultIncrement.totalStaked >= amount, message: "Internal error: insufficient totalStaked")
200            
201            // Update user's deposit balance
202            PrizeVaultIncrement.userDeposits[ownerAddress] = userDeposit - amount
203            
204            // Update totals
205            PrizeVaultIncrement.totalDeposited = PrizeVaultIncrement.totalDeposited - amount
206            PrizeVaultIncrement.totalStaked = PrizeVaultIncrement.totalStaked - amount
207            PrizeVaultIncrement.pendingWithdrawals[ownerAddress] = amount
208            
209            // Update local balance
210            self.balance = PrizeVaultIncrement.userDeposits[ownerAddress]! + (PrizeVaultIncrement.userPrizes[ownerAddress] ?? 0.0)
211            
212            // Initiate unstaking from Increment Labs
213            PrizeVaultIncrement.unstakeTokens(amount: amount)
214            
215            emit WithdrawalRequested(address: ownerAddress, amount: amount, withdrawalType: "deposit")
216        }
217        
218        // Request prize withdrawal: Withdraws prizes without unstaking
219        // This is instant since prizes are already liquid in the vault
220        access(all) fun requestPrizeWithdrawal(amount: UFix64) {
221            let ownerAddress = self.owner?.address ?? panic("No owner address")
222            
223            let userPrize = PrizeVaultIncrement.userPrizes[ownerAddress] ?? 0.0
224            assert(userPrize >= amount, message: "Insufficient prize balance. Your prizes: ".concat(userPrize.toString()))
225            
226            let existingPending = PrizeVaultIncrement.pendingWithdrawals[ownerAddress] ?? 0.0
227            assert(existingPending == 0.0, message: "You already have a pending withdrawal. Complete it first.")
228            
229            // Update user's prize balance
230            PrizeVaultIncrement.userPrizes[ownerAddress] = userPrize - amount
231            PrizeVaultIncrement.pendingWithdrawals[ownerAddress] = amount
232            
233            // Update local balance
234            self.balance = PrizeVaultIncrement.userDeposits[ownerAddress]! + (PrizeVaultIncrement.userPrizes[ownerAddress] ?? 0.0)
235            
236            // No unstaking needed - prizes are already liquid in the vault
237            
238            emit WithdrawalRequested(address: ownerAddress, amount: amount, withdrawalType: "prize")
239        }
240        
241        // Complete withdrawal: Transfer FLOW from vault to user
242        // For deposit withdrawals, this requires unstaking to be complete
243        // For prize withdrawals, this is instant
244        access(all) fun completeWithdrawal(): @{FungibleToken.Vault} {
245            let ownerAddress = self.owner?.address ?? panic("No owner address")
246            
247            let pendingAmount = PrizeVaultIncrement.pendingWithdrawals[ownerAddress]
248                ?? panic("No pending withdrawal found")
249            
250            assert(pendingAmount > 0.0, message: "No pending withdrawal")
251            
252            // Check vault has enough FLOW
253            let vaultBalance = PrizeVaultIncrement.vault.balance
254            assert(vaultBalance >= pendingAmount, message: "Insufficient FLOW in vault. Unstaking may not be complete yet. Current vault balance: ".concat(vaultBalance.toString()))
255            
256            // Withdraw from vault to user
257            let withdrawn <- PrizeVaultIncrement.withdrawFromVault(amount: pendingAmount)
258            
259            // Clear pending withdrawal
260            PrizeVaultIncrement.pendingWithdrawals[ownerAddress] = 0.0
261            
262            emit Withdrawn(address: ownerAddress, amount: pendingAmount)
263            
264            return <- withdrawn
265        }
266        
267        // Instant withdrawal: Swap stFLOW for FLOW using Increment Finance swap
268        // This allows users to withdraw immediately without waiting for unstaking period
269        // Note: May incur slippage depending on swap pool liquidity
270        access(all) fun instantWithdrawDeposit(amount: UFix64, minFlowOut: UFix64): @{FungibleToken.Vault} {
271            let ownerAddress = self.owner?.address ?? panic("No owner address")
272            
273            let userDeposit = PrizeVaultIncrement.userDeposits[ownerAddress] ?? 0.0
274            assert(userDeposit >= amount, message: "Insufficient deposit balance. Your deposit: ".concat(userDeposit.toString()))
275            
276            let existingPending = PrizeVaultIncrement.pendingWithdrawals[ownerAddress] ?? 0.0
277            assert(existingPending == 0.0, message: "You already have a pending withdrawal. Complete it first.")
278            
279            // Verify totalStaked accounting (safety check to prevent underflow)
280            assert(PrizeVaultIncrement.totalStaked >= amount, message: "Internal error: insufficient totalStaked")
281            
282            // Calculate how much stFLOW we need to withdraw
283            let stFlowToWithdraw = LiquidStaking.calcStFlowFromFlow(flowAmount: amount)
284            
285            // Verify we have enough stFLOW
286            assert(PrizeVaultIncrement.stFlowVault.balance >= stFlowToWithdraw, 
287                   message: "Insufficient stFLOW in vault. stFLOW needed: ".concat(stFlowToWithdraw.toString()))
288            
289            // Withdraw stFLOW from contract's vault
290            let stFlowVault <- PrizeVaultIncrement.stFlowVault.withdraw(amount: stFlowToWithdraw) as! @stFlowToken.Vault
291            
292            // Perform the swap: stFLOW -> FLOW
293            let flowVault <- PrizeVaultIncrement.swapStFlowForFlow(
294                stFlowVault: <- stFlowVault, 
295                minFlowOut: minFlowOut
296            )
297            
298            let flowReceived = flowVault.balance
299            
300            // Update user's deposit balance
301            PrizeVaultIncrement.userDeposits[ownerAddress] = userDeposit - amount
302            
303            // Update totals
304            PrizeVaultIncrement.totalDeposited = PrizeVaultIncrement.totalDeposited - amount
305            PrizeVaultIncrement.totalStaked = PrizeVaultIncrement.totalStaked - amount
306            
307            // Update local balance
308            self.balance = PrizeVaultIncrement.userDeposits[ownerAddress]! + (PrizeVaultIncrement.userPrizes[ownerAddress] ?? 0.0)
309            
310            emit InstantWithdrawn(address: ownerAddress, stFlowAmount: stFlowToWithdraw, flowReceived: flowReceived)
311            
312            return <- flowVault
313        }
314        
315        // Get total balance (deposits + prizes) for display
316        access(all) fun getBalance(): UFix64 {
317            let ownerAddress = self.owner?.address ?? panic("No owner address")
318            let deposit = PrizeVaultIncrement.userDeposits[ownerAddress] ?? 0.0
319            let prize = PrizeVaultIncrement.userPrizes[ownerAddress] ?? 0.0
320            return deposit + prize
321        }
322        
323        // Get deposit balance only
324        access(all) fun getDepositBalance(): UFix64 {
325            let ownerAddress = self.owner?.address ?? panic("No owner address")
326            return PrizeVaultIncrement.userDeposits[ownerAddress] ?? 0.0
327        }
328        
329        // Get prize balance only
330        access(all) fun getPrizeBalance(): UFix64 {
331            let ownerAddress = self.owner?.address ?? panic("No owner address")
332            return PrizeVaultIncrement.userPrizes[ownerAddress] ?? 0.0
333        }
334        
335        access(all) fun getPendingWithdrawal(): UFix64 {
336            let ownerAddress = self.owner?.address ?? panic("No owner address")
337            return PrizeVaultIncrement.pendingWithdrawals[ownerAddress] ?? 0.0
338        }
339    }
340    
341    // Create a new DepositReceiver for users
342    access(all) fun createDepositReceiver(): @DepositReceiver {
343        return <- create DepositReceiver()
344    }
345    
346    // Internal function to withdraw from vault
347    access(contract) fun withdrawFromVault(amount: UFix64): @{FungibleToken.Vault} {
348        let withdrawn <- self.vault.withdraw(amount: amount)
349        return <- withdrawn
350    }
351    
352    // Internal function to stake tokens via Increment Labs
353    access(contract) fun stakeTokens(flowVault: @FlowToken.Vault) {
354        let amount = flowVault.balance
355        
356        // Stake on Increment Labs and receive stFlowToken
357        let stFlowReceived <- LiquidStaking.stake(flowVault: <- flowVault)
358        let stFlowAmount = stFlowReceived.balance
359        
360        // Deposit stFlowToken into our vault
361        self.stFlowVault.deposit(from: <- stFlowReceived)
362        
363        // Update total staked (in FLOW terms)
364        self.totalStaked = self.totalStaked + amount
365        
366        emit Staked(amount: amount, stFlowReceived: stFlowAmount)
367    }
368    
369    // Internal function to unstake tokens from Increment Labs
370    // Returns a WithdrawVoucher that can be redeemed after the unlock epoch
371    access(contract) fun unstakeTokens(amount: UFix64) {
372        // Calculate how much stFlowToken we need to unstake
373        let stFlowToUnstake = LiquidStaking.calcStFlowFromFlow(flowAmount: amount)
374        
375        // Withdraw stFlowToken from our vault
376        let stFlowVault <- self.stFlowVault.withdraw(amount: stFlowToUnstake) as! @stFlowToken.Vault
377        
378        // Unstake from Increment Labs - returns a WithdrawVoucher
379        let voucher <- LiquidStaking.unstake(stFlowVault: <- stFlowVault)
380        
381        let voucherUUID = voucher.uuid
382        let unlockEpoch = voucher.unlockEpoch
383        
384        // Store the voucher in our collection
385        self.voucherCollection.deposit(voucher: <- voucher)
386        
387        emit UnstakeRequested(amount: amount, voucherUUID: voucherUUID, unlockEpoch: unlockEpoch)
388    }
389    
390    // Internal function to swap stFLOW for FLOW using Increment Finance
391    // This enables instant withdrawals without waiting for the unstaking period
392    access(contract) fun swapStFlowForFlow(stFlowVault: @stFlowToken.Vault, minFlowOut: UFix64): @FlowToken.Vault {
393        let stFlowAmount = stFlowVault.balance
394        
395        // Get token keys for identifying which token is which in the pair
396        let stFlowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(
397            vaultTypeIdentifier: Type<@stFlowToken.Vault>().identifier
398        )
399        let flowKey = SwapConfig.SliceTokenTypeIdentifierFromVaultType(
400            vaultTypeIdentifier: Type<@FlowToken.Vault>().identifier
401        )
402        
403        // Get the stFLOW/FLOW pair address from SwapFactory
404        let pairAddress = SwapFactory.getPairAddress(token0Key: stFlowKey, token1Key: flowKey)
405            ?? panic("stFLOW/FLOW swap pair not found in SwapFactory. The pair may not exist or tokens may need to be swapped in reverse order.")
406        
407        // Borrow the swap pair public interface
408        let swapPairPublicRef = getAccount(pairAddress)
409            .capabilities.get<&{SwapInterfaces.PairPublic}>(/public/increment_swap_pair)
410            .borrow()
411            ?? panic("Could not borrow swap pair public reference. Pair address: ".concat(pairAddress.toString()))
412        
413        // Calculate expected output amount (for validation)
414        let expectedFlowOut = swapPairPublicRef.getAmountOut(amountIn: stFlowAmount, tokenInKey: stFlowKey)
415        
416        // Verify minimum output (slippage protection)
417        assert(expectedFlowOut >= minFlowOut, 
418               message: "Slippage too high. Expected: ".concat(expectedFlowOut.toString())
419                       .concat(", Minimum: ").concat(minFlowOut.toString()))
420        
421        // Perform the swap
422        // Cast the stFlowVault to FungibleToken.Vault for the swap
423        let tokenIn <- stFlowVault as @{FungibleToken.Vault}
424        
425        // Execute the swap (exactAmountOut: nil means we get all output from input)
426        let tokenOut <- swapPairPublicRef.swap(
427            vaultIn: <- tokenIn,
428            exactAmountOut: nil
429        )
430        
431        // Cast the output back to FlowToken.Vault
432        let flowVault <- tokenOut as! @FlowToken.Vault
433        
434        return <- flowVault
435    }
436    
437    // Process all mature vouchers and deposit FLOW into vault
438    access(contract) fun processMatureVouchers() {
439        let voucherInfos = self.voucherCollection.getVoucherInfos()
440        
441        for info in voucherInfos {
442            let voucherInfo = info as! {String: AnyStruct}
443            let uuid = voucherInfo["uuid"]! as! UInt64
444            let unlockEpoch = voucherInfo["unlockEpoch"]! as! UInt64
445            let lockedAmount = voucherInfo["lockedFlowAmount"]! as! UFix64
446            
447            // Check if voucher is mature (can be cashed out)
448            // We need to get current epoch from DelegatorManager via LiquidStaking
449            // For now, we'll try to cashout and handle the error if not ready
450            
451            // Withdraw voucher from collection (needs Withdraw entitlement)
452            let voucherCollectionRef = (&self.voucherCollection as auth(FungibleToken.Withdraw) &LiquidStaking.WithdrawVoucherCollection)
453            let voucher <- voucherCollectionRef.withdraw(uuid: uuid)
454            
455            // Try to cashout the voucher
456            let flowVault <- LiquidStaking.cashoutWithdrawVoucher(voucher: <- voucher)
457            
458            // Deposit into our vault
459            self.vault.deposit(from: <- flowVault)
460        }
461    }
462    
463    // Calculate available rewards to harvest
464    access(all) fun calculateAvailableRewards(): UFix64 {
465        // Get total stFlowToken balance
466        let stFlowBalance = self.stFlowVault.balance
467        
468        // Convert stFlowToken to FLOW value using Increment's exchange rate
469        let totalFlowValue = LiquidStaking.calcFlowFromStFlow(stFlowAmount: stFlowBalance)
470        
471        // Available rewards = current value - originally staked amount
472        let rewards = totalFlowValue - self.totalStaked
473        
474        return rewards > 0.0 ? rewards : 0.0
475    }
476    
477    // Harvest staking rewards by unstaking the profit portion
478    access(contract) fun harvestStakingRewards() {
479        let availableRewards = self.calculateAvailableRewards()
480        
481        assert(availableRewards > 0.0, message: "No rewards to harvest")
482        
483        // Calculate stFlowToken amount for the rewards
484        let stFlowToUnstake = LiquidStaking.calcStFlowFromFlow(flowAmount: availableRewards)
485        
486        // Withdraw stFlowToken from our vault
487        let stFlowVault <- self.stFlowVault.withdraw(amount: stFlowToUnstake) as! @stFlowToken.Vault
488        
489        // Unstake from Increment Labs
490        let voucher <- LiquidStaking.unstake(stFlowVault: <- stFlowVault)
491        
492        // Store the voucher - it will be cashed out later
493        self.voucherCollection.deposit(voucher: <- voucher)
494        
495        self.totalRewardsHarvested = self.totalRewardsHarvested + availableRewards
496    }
497    
498    // PUBLIC FUNCTION: Start monthly prize draw (anyone can call after sufficient blocks have passed)
499    access(all) fun startMonthlyDraw() {
500        // Check if enough blocks have passed since last draw
501        let currentBlock = getCurrentBlock().height
502        let blocksSinceLastDraw = currentBlock - self.lastDrawBlock
503        assert(
504            self.canDrawNow(), 
505            message: "Not enough blocks have passed since last draw. Current block: "
506                .concat(currentBlock.toString())
507                .concat(", Last draw block: ")
508                .concat(self.lastDrawBlock.toString())
509                .concat(", Blocks since: ")
510                .concat(blocksSinceLastDraw.toString())
511                .concat(", Required: ")
512                .concat(self.blocksPerMonth.toString())
513        )
514        
515        assert(self.monthlyDrawReceipt == nil, message: "Previous monthly draw not completed. Call completeMonthlyDraw() first.")
516        
517        // Calculate available rewards to distribute
518        let availableRewards = self.calculateAvailableRewards()
519        assert(availableRewards > 0.0, message: "No rewards available for distribution")
520        
521        // Harvest the rewards (unstake from Increment)
522        self.harvestStakingRewards()
523        
524        // Note: After calling this, wait for the unstaking period to complete,
525        // then call completeMonthlyDraw() to finish the draw and award the prize
526        
527        // Update last draw block height
528        self.lastDrawBlock = currentBlock
529        
530        // Commit the prize draw (this will be completed later)
531        let receipt <- self.commitPrize(amount: availableRewards)
532        self.monthlyDrawReceipt <-! receipt
533    }
534    
535    // PUBLIC FUNCTION: Complete monthly prize draw (anyone can call after commitment)
536    access(all) fun completeMonthlyDraw() {
537        assert(self.monthlyDrawReceipt != nil, message: "No monthly draw in progress. Call startMonthlyDraw() first.")
538        
539        // First, process any mature vouchers to get FLOW into vault
540        self.processMatureVouchers()
541        
542        // Get the receipt
543        let receipt <- self.monthlyDrawReceipt <- nil
544        
545        // Complete the prize reveal and award
546        self.revealPrize(receipt: <- receipt!)
547    }
548    
549    // Commit phase: Lock in the prize amount and request randomness
550    access(contract) fun commitPrize(amount: UFix64): @PrizeDrawReceipt {
551        assert(self.userDeposits.length > 0, message: "No users with deposits")
552        
553        // Request randomness from RandomConsumer
554        let request <- self.consumer.requestRandomness()
555        
556        // Create receipt with prize amount and random request
557        let receipt <- create PrizeDrawReceipt(
558            prizeAmount: amount,
559            request: <- request
560        )
561        
562        let commitBlock = receipt.getRequestBlock()!
563        
564        emit PrizeDrawCommitted(prizeAmount: amount, commitBlock: commitBlock, receiptID: receipt.uuid)
565        
566        return <- receipt
567    }
568    
569    // Reveal phase: Use receipt to get random number and award prize
570    access(contract) fun revealPrize(receipt: @PrizeDrawReceipt) {
571        let prizeAmount = receipt.prizeAmount
572        let commitBlock = receipt.getRequestBlock()!
573        let receiptID = receipt.uuid
574        
575        // Fulfill the random request to get the random value
576        let request <- receipt.popRequest()
577        let randomNumber = self.consumer.fulfillRandomRequest(<-request)
578        
579        // Destroy the receipt
580        destroy receipt
581        
582        // Select winner using weighted random selection
583        let winnerAddress = self.selectWeightedWinner(randomNumber: randomNumber)
584        
585        // Award the prize by increasing winner's prize balance
586        let currentPrizes = self.userPrizes[winnerAddress] ?? 0.0
587        self.userPrizes[winnerAddress] = currentPrizes + prizeAmount
588        
589        // Update prize tracking
590        self.prizeRound = self.prizeRound + 1
591        self.totalPrizesDistributed = self.totalPrizesDistributed + prizeAmount
592        self.prizeHistory[self.prizeRound] = winnerAddress
593        
594        // Emit prize awarded event
595        emit PrizeAwarded(
596            winner: winnerAddress, 
597            amount: prizeAmount, 
598            round: self.prizeRound, 
599            commitBlock: commitBlock, 
600            receiptID: receiptID
601        )
602    }
603    
604    // Weighted random selection: Select winner based on deposit amount
605    // Each FLOW token = 1 "ticket" in the lottery
606    // User with 100 FLOW has 100x better chance than user with 1 FLOW
607    access(contract) fun selectWeightedWinner(randomNumber: UInt64): Address {
608        let depositors = self.userDeposits.keys
609        assert(depositors.length > 0, message: "No depositors")
610        
611        // Handle single depositor case
612        if depositors.length == 1 {
613            return depositors[0]
614        }
615        
616        // Build cumulative sum array
617        // Example: [10, 70, 20] -> cumulative: [10, 80, 100]
618        var cumulativeSum: [UFix64] = []
619        var runningTotal: UFix64 = 0.0
620        
621        for addr in depositors {
622            let amount = self.userDeposits[addr]!
623            runningTotal = runningTotal + amount
624            cumulativeSum.append(runningTotal)
625        }
626        
627        // Convert UInt64 random number to proportional value in [0, totalDeposited)
628        // We use modulo to get a value in range [0, runningTotal)
629        // For better distribution with large deposits, we scale appropriately
630        let randomValue = UFix64(randomNumber % UInt64(runningTotal * 100000000.0)) / 100000000.0
631        
632        // Find winner using cumulative sum (similar to binary search)
633        // The random value will fall into one depositor's range
634        var winnerIndex = 0
635        for i, cumSum in cumulativeSum {
636            if randomValue < cumSum {
637                winnerIndex = i
638                break
639            }
640        }
641        
642        return depositors[winnerIndex]
643    }
644    
645    // Public getters
646    access(all) fun getTotalDeposited(): UFix64 {
647        return self.totalDeposited
648    }
649    
650    access(all) fun getTotalStaked(): UFix64 {
651        return self.totalStaked
652    }
653    
654    access(all) fun getTotalRewardsHarvested(): UFix64 {
655        return self.totalRewardsHarvested
656    }
657    
658    access(all) fun getTotalPrizesDistributed(): UFix64 {
659        return self.totalPrizesDistributed
660    }
661    
662    access(all) fun getCurrentPrizeRound(): UInt64 {
663        return self.prizeRound
664    }
665    
666    access(all) fun getVaultBalance(): UFix64 {
667        return self.vault.balance
668    }
669    
670    access(all) fun getStFlowBalance(): UFix64 {
671        return self.stFlowVault.balance
672    }
673    
674    access(all) fun getUserDeposit(address: Address): UFix64 {
675        let deposit = self.userDeposits[address] ?? 0.0
676        let prize = self.userPrizes[address] ?? 0.0
677        return deposit + prize
678    }
679    
680    access(all) fun getUserDepositOnly(address: Address): UFix64 {
681        return self.userDeposits[address] ?? 0.0
682    }
683    
684    access(all) fun getUserPrizes(address: Address): UFix64 {
685        return self.userPrizes[address] ?? 0.0
686    }
687    
688    access(all) fun getUserPendingWithdrawal(address: Address): UFix64 {
689        return self.pendingWithdrawals[address] ?? 0.0
690    }
691    
692    access(all) fun getPrizeWinner(round: UInt64): Address? {
693        return self.prizeHistory[round]
694    }
695    
696    access(all) fun getLastDrawBlock(): UInt64 {
697        return self.lastDrawBlock
698    }
699    
700    access(all) fun getBlocksPerMonth(): UInt64 {
701        return self.blocksPerMonth
702    }
703    
704    access(all) fun getCurrentBlock(): UInt64 {
705        return getCurrentBlock().height
706    }
707    
708    access(all) fun getBlocksSinceLastDraw(): UInt64 {
709        return getCurrentBlock().height - self.lastDrawBlock
710    }
711    
712    access(all) fun getBlocksUntilNextDraw(): UInt64 {
713        let blocksSince = self.getBlocksSinceLastDraw()
714        if blocksSince >= self.blocksPerMonth {
715            return 0
716        }
717        return self.blocksPerMonth - blocksSince
718    }
719    
720    access(all) fun canDrawNow(): Bool {
721        return self.getBlocksSinceLastDraw() >= self.blocksPerMonth
722    }
723    
724    access(all) fun isMonthlyDrawInProgress(): Bool {
725        return self.monthlyDrawReceipt != nil
726    }
727    
728    // Get minimum deposit requirement
729    access(all) fun getMinimumDeposit(): UFix64 {
730        return self.minimumDeposit
731    }
732    
733    // Calculate user's winning probability (returns value between 0.0 and 1.0)
734    // Example: 0.05 = 5% chance to win
735    access(all) fun getUserWinningChance(address: Address): UFix64 {
736        let userDeposit = self.userDeposits[address] ?? 0.0
737        
738        if userDeposit == 0.0 || self.totalDeposited == 0.0 {
739            return 0.0
740        }
741        
742        return userDeposit / self.totalDeposited
743    }
744    
745    // Get total value locked (TVL) in FLOW
746    access(all) fun getTotalValueLocked(): UFix64 {
747        // TVL = liquid vault balance + staked value
748        let stFlowValue = LiquidStaking.calcFlowFromStFlow(stFlowAmount: self.stFlowVault.balance)
749        return self.vault.balance + stFlowValue
750    }
751    
752    // Get pending vouchers info
753    access(all) fun getPendingVouchers(): [AnyStruct] {
754        return self.voucherCollection.getVoucherInfos()
755    }
756    
757    // Get stFlowToken to FLOW exchange rate
758    access(all) fun getExchangeRate(): UFix64 {
759        // How much FLOW you get for 1 stFlowToken
760        return LiquidStaking.calcFlowFromStFlow(stFlowAmount: 1.0)
761    }
762    
763    init() {
764        // Initialize constants
765        self.minimumDeposit = 1.0  // 1 FLOW minimum to prevent dust/sybil attacks
766        
767        // Initialize paths
768        self.DepositReceiverStoragePath = /storage/PrizeVaultIncrementDepositReceiver
769        self.DepositReceiverPublicPath = /public/PrizeVaultIncrementDepositReceiver
770        self.VaultStoragePath = /storage/PrizeVaultIncrementMainVault
771        self.AdminStoragePath = /storage/PrizeVaultIncrementAdmin
772        self.PrizeDrawReceiptStoragePath = /storage/PrizeVaultIncrementDrawReceipt
773        self.WithdrawVoucherCollectionStoragePath = /storage/PrizeVaultIncrementVoucherCollection
774        
775        // Initialize state
776        self.totalDeposited = 0.0
777        self.totalStaked = 0.0
778        self.totalRewardsHarvested = 0.0
779        self.totalPrizesDistributed = 0.0
780        self.prizeRound = 0
781        self.lastDrawBlock = 0  // Allow first draw immediately (0 means never drawn before)
782        self.blocksPerMonth = 2592000  // ~30 days at 1 second per block on Flow
783        self.monthlyDrawReceipt <- nil
784        self.userDeposits = {}
785        self.userPrizes = {}
786        self.pendingWithdrawals = {}
787        self.prizeHistory = {}
788        
789        // Create the main vault to hold liquid FLOW
790        self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
791        
792        // Create stFlowToken vault to hold staked tokens
793        self.stFlowVault <- stFlowToken.createEmptyVault(vaultType: Type<@stFlowToken.Vault>()) as! @stFlowToken.Vault
794        
795        // Create WithdrawVoucher collection
796        self.voucherCollection <- LiquidStaking.createEmptyWithdrawVoucherCollection()
797        
798        // Initialize RandomConsumer for commit-reveal randomness
799        self.consumer <- RandomConsumer.createConsumer()
800        
801        // Create and save Admin resource
802        self.account.storage.save(<- create Admin(), to: self.AdminStoragePath)
803    }
804}
805
806