Smart Contract

PrizeVaultV3

A.262cf58c0b9fbcff.PrizeVaultV3

Valid From

130,409,623

Deployed

1w ago
Feb 21, 2026, 02:16:15 PM UTC

Dependents

4 imports
1/*
2PrizeVault V3 - A no-loss lottery system on Flow blockchain
3
4Users deposit FLOW tokens into the vault. The vault stakes these tokens on Ankr via Flow EVM
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 Flow EVM (Ankr)
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
23import EVM from 0xe467b9dd11fa00df
24
25access(all) contract PrizeVaultV3 {
26    
27    // Constants
28    access(all) let minimumDeposit: UFix64
29    
30    // Events
31    access(all) event Deposited(address: Address, amount: UFix64)
32    access(all) event WithdrawalRequested(address: Address, amount: UFix64)
33    access(all) event Withdrawn(address: Address, amount: UFix64)
34    access(all) event Staked(amount: UFix64)
35    access(all) event Unstaked(amount: UFix64)
36    access(all) event PrizeDrawCommitted(prizeAmount: UFix64, commitBlock: UInt64, receiptID: UInt64)
37    access(all) event PrizeAwarded(winner: Address, amount: UFix64, round: UInt64, commitBlock: UInt64, receiptID: UInt64)
38    access(all) event DepositReceiverCreated(address: Address)
39    access(all) event BlocksPerMonthUpdated(oldValue: UInt64, newValue: UInt64)
40    
41    // Paths
42    access(all) let DepositReceiverStoragePath: StoragePath
43    access(all) let DepositReceiverPublicPath: PublicPath
44    access(all) let VaultStoragePath: StoragePath
45    access(all) let AdminStoragePath: StoragePath
46    access(all) let PrizeDrawReceiptStoragePath: StoragePath
47    
48    // State
49    access(all) var totalDeposited: UFix64
50    access(all) var totalStaked: UFix64
51    access(all) var totalRewardsHarvested: UFix64
52    access(all) var totalPrizesDistributed: UFix64
53    access(all) var prizeRound: UInt64
54    access(all) var lastDrawBlock: UInt64  // Block height of last draw
55    access(all) var blocksPerMonth: UInt64  // Configurable blocks between draws (~30 days)
56    access(self) var monthlyDrawReceipt: @PrizeDrawReceipt?  // Stored receipt for monthly draw
57    access(self) var totalPendingWithdrawalInCOA: UFix64  // Track user withdrawals in COA to prevent drainage
58    
59    // Mappings
60    access(self) let userDeposits: {Address: UFix64}  // Original deposits only
61    access(self) let userPrizes: {Address: UFix64}    // Prizes won
62    access(self) let pendingWithdrawals: {Address: UFix64}
63    access(self) let prizeHistory: {UInt64: Address}
64    
65    // Main vault to hold all deposited FLOW tokens
66    access(self) let vault: @FlowToken.Vault
67    
68    // EVM staking pool contract address (Ankr on Flow EVM)
69    access(self) let evmStakingPoolAddress: EVM.EVMAddress
70    
71    // ankrFLOWEVM token address (liquid staking token)
72    access(self) let ankrFlowTokenAddress: EVM.EVMAddress
73    
74    // Ankr ratio feed address (for exchange rate)
75    access(self) let ankrRatioFeedAddress: EVM.EVMAddress
76    
77    // CadenceOwnedAccount for EVM interactions
78    access(self) let coa: @EVM.CadenceOwnedAccount
79    
80    // RandomConsumer for commit-reveal randomness
81    access(self) let consumer: @RandomConsumer.Consumer
82    
83    // Receipt resource for prize draw commit-reveal
84    access(all) resource PrizeDrawReceipt {
85        access(all) let prizeAmount: UFix64
86        access(self) var request: @RandomConsumer.Request?
87        
88        init(prizeAmount: UFix64, request: @RandomConsumer.Request) {
89            self.prizeAmount = prizeAmount
90            self.request <- request
91        }
92        
93        // Get the block height at which randomness was committed
94        access(all) view fun getRequestBlock(): UInt64? {
95            return self.request?.block
96        }
97        
98        // Pop the request for fulfillment (can only be called once)
99        access(contract) fun popRequest(): @RandomConsumer.Request {
100            let request <- self.request <- nil
101            return <- request!
102        }
103    }
104    
105    // Admin resource to manage contract configuration
106    access(all) resource Admin {
107        // Commit phase: Start a prize draw (returns receipt to be used in reveal)
108        access(all) fun commitPrizeDraw(prizeAmount: UFix64): @PrizeDrawReceipt {
109            return <- PrizeVaultV3.commitPrize(amount: prizeAmount)
110        }
111        
112        // Reveal phase: Complete the prize draw using the receipt
113        access(all) fun revealPrizeDraw(receipt: @PrizeDrawReceipt) {
114            PrizeVaultV3.revealPrize(receipt: <- receipt)
115        }
116        
117        // Update the blocks per month interval (for adjusting draw frequency)
118        // Default: 2592000 blocks ≈ 30 days (at ~1 second block time on Flow)
119        access(all) fun setBlocksPerMonth(newBlocksPerMonth: UInt64) {
120            assert(newBlocksPerMonth > 0, message: "Blocks per month must be greater than 0")
121            assert(newBlocksPerMonth >= 10000, message: "Minimum 10,000 blocks (~3 hours) to prevent spam")
122            assert(newBlocksPerMonth <= 5184000, message: "Maximum 5,184,000 blocks (~60 days)")
123            
124            let oldValue = PrizeVaultV3.blocksPerMonth
125            PrizeVaultV3.blocksPerMonth = newBlocksPerMonth
126            
127            emit BlocksPerMonthUpdated(oldValue: oldValue, newValue: newBlocksPerMonth)
128        }
129    }
130    
131    // Public interface that users can expose
132    access(all) resource interface DepositReceiverPublic {
133        access(all) fun deposit(from: @{FungibleToken.Vault})
134        access(all) fun requestDepositWithdrawal(amount: UFix64)
135        access(all) fun requestPrizeWithdrawal(amount: UFix64)
136        access(all) fun completeWithdrawal(): @{FungibleToken.Vault}
137        access(all) fun getBalance(): UFix64  // Total balance (deposits + prizes)
138        access(all) fun getDepositBalance(): UFix64  // Deposits only
139        access(all) fun getPrizeBalance(): UFix64  // Prizes only
140        access(all) fun getPendingWithdrawal(): UFix64
141    }
142    
143    // DepositReceiver resource that users create to interact with the vault
144    access(all) resource DepositReceiver: DepositReceiverPublic {
145        // Track user's balance
146        access(self) var balance: UFix64
147        
148        init() {
149            self.balance = 0.0
150        }
151        
152        // Deposit FLOW tokens into the vault
153        access(all) fun deposit(from: @{FungibleToken.Vault}) {
154            // Get the owner's address
155            let ownerAddress = self.owner?.address ?? panic("No owner address")
156            
157            // Cast to FlowToken.Vault
158            let flowVault <- from as! @FlowToken.Vault
159            let amount = flowVault.balance
160            
161            // Enforce minimum deposit to prevent dust/sybil attacks
162            assert(amount >= PrizeVaultV3.minimumDeposit, 
163                   message: "Minimum deposit is ".concat(PrizeVaultV3.minimumDeposit.toString()).concat(" FLOW"))
164            
165            // Deposit into main vault
166            let _ = PrizeVaultV3.vault.deposit(from: <- flowVault)
167            
168            // Stake the tokens via EVM
169            PrizeVaultV3.stakeTokens(amount: amount)
170            
171            // Update user's balance
172            self.balance = self.balance + amount
173            
174            // Update contract state
175            PrizeVaultV3.userDeposits[ownerAddress] = self.balance
176            PrizeVaultV3.totalDeposited = PrizeVaultV3.totalDeposited + amount
177            
178            emit Deposited(address: ownerAddress, amount: amount)
179            emit DepositReceiverCreated(address: ownerAddress)
180        }
181        
182        // Request deposit withdrawal: Initiates unstaking from Ankr
183        // This requires an unstaking period (7-14 days) before funds are available
184        access(all) fun requestDepositWithdrawal(amount: UFix64) {
185            let ownerAddress = self.owner?.address ?? panic("No owner address")
186            
187            let userDeposit = PrizeVaultV3.userDeposits[ownerAddress] ?? 0.0
188            assert(userDeposit >= amount, message: "Insufficient deposit balance. Your deposit: ".concat(userDeposit.toString()))
189            
190            let existingPending = PrizeVaultV3.pendingWithdrawals[ownerAddress] ?? 0.0
191            assert(existingPending == 0.0, message: "You already have a pending withdrawal. Complete it first.")
192            
193            // Verify totalStaked accounting (safety check to prevent underflow)
194            assert(PrizeVaultV3.totalStaked >= amount, message: "Internal error: insufficient totalStaked")
195            
196            // Update user's deposit balance
197            PrizeVaultV3.userDeposits[ownerAddress] = userDeposit - amount
198            
199            // Update totals
200            PrizeVaultV3.totalDeposited = PrizeVaultV3.totalDeposited - amount
201            PrizeVaultV3.totalStaked = PrizeVaultV3.totalStaked - amount
202            PrizeVaultV3.pendingWithdrawals[ownerAddress] = amount
203            
204            // Track this withdrawal in COA counter
205            PrizeVaultV3.totalPendingWithdrawalInCOA = PrizeVaultV3.totalPendingWithdrawalInCOA + amount
206            
207            // Update local balance
208            self.balance = PrizeVaultV3.userDeposits[ownerAddress]! + (PrizeVaultV3.userPrizes[ownerAddress] ?? 0.0)
209            
210            // Initiate unstaking from Ankr
211            PrizeVaultV3.unstakeTokens(amount: amount)
212            
213            emit WithdrawalRequested(address: ownerAddress, amount: amount)
214        }
215        
216        // Request prize withdrawal: Withdraws prizes without unstaking
217        // This is instant since prizes are already liquid in the COA (from harvested rewards)
218        access(all) fun requestPrizeWithdrawal(amount: UFix64) {
219            let ownerAddress = self.owner?.address ?? panic("No owner address")
220            
221            let userPrize = PrizeVaultV3.userPrizes[ownerAddress] ?? 0.0
222            assert(userPrize >= amount, message: "Insufficient prize balance. Your prizes: ".concat(userPrize.toString()))
223            
224            let existingPending = PrizeVaultV3.pendingWithdrawals[ownerAddress] ?? 0.0
225            assert(existingPending == 0.0, message: "You already have a pending withdrawal. Complete it first.")
226            
227            // Update user's prize balance
228            PrizeVaultV3.userPrizes[ownerAddress] = userPrize - amount
229            PrizeVaultV3.pendingWithdrawals[ownerAddress] = amount
230            
231            // Track this withdrawal in COA counter
232            PrizeVaultV3.totalPendingWithdrawalInCOA = PrizeVaultV3.totalPendingWithdrawalInCOA + amount
233            
234            // Update local balance
235            self.balance = PrizeVaultV3.userDeposits[ownerAddress]! + (PrizeVaultV3.userPrizes[ownerAddress] ?? 0.0)
236            
237            // No unstaking needed - prizes are already liquid in the COA
238            
239            emit WithdrawalRequested(address: ownerAddress, amount: amount)
240        }
241        
242        // Complete withdrawal: Transfer FLOW from COA to user after unstaking period
243        access(all) fun completeWithdrawal(): @{FungibleToken.Vault} {
244            let ownerAddress = self.owner?.address ?? panic("No owner address")
245            
246            let pendingAmount = PrizeVaultV3.pendingWithdrawals[ownerAddress]
247                ?? panic("No pending withdrawal found")
248            
249            assert(pendingAmount > 0.0, message: "No pending withdrawal")
250            
251            // Check COA has enough FLOW from unstaking
252            let coaBalance = PrizeVaultV3.getCOABalance()
253            assert(coaBalance >= pendingAmount, message: "Insufficient FLOW in COA. Unstaking may not be complete yet. Current COA balance: ".concat(coaBalance.toString()))
254            
255            // Move FLOW from COA to vault
256            PrizeVaultV3.withdrawFromCOA(amount: pendingAmount)
257            
258            // Withdraw from vault to user
259            let withdrawn <- PrizeVaultV3.withdrawFromVault(amount: pendingAmount)
260            
261            // Note: totalStaked was already decremented in requestWithdrawal()
262            // when the shares were burned from Ankr
263            
264            // Decrement COA pending withdrawal counter (withdrawal complete)
265            PrizeVaultV3.totalPendingWithdrawalInCOA = PrizeVaultV3.totalPendingWithdrawalInCOA - pendingAmount
266            
267            // Clear pending withdrawal
268            PrizeVaultV3.pendingWithdrawals[ownerAddress] = 0.0
269            
270            emit Withdrawn(address: ownerAddress, amount: pendingAmount)
271            
272            return <- withdrawn
273        }
274        
275        // Get total balance (deposits + prizes) for display
276        access(all) fun getBalance(): UFix64 {
277            let ownerAddress = self.owner?.address ?? panic("No owner address")
278            let deposit = PrizeVaultV3.userDeposits[ownerAddress] ?? 0.0
279            let prize = PrizeVaultV3.userPrizes[ownerAddress] ?? 0.0
280            return deposit + prize
281        }
282        
283        // Get deposit balance only
284        access(all) fun getDepositBalance(): UFix64 {
285            let ownerAddress = self.owner?.address ?? panic("No owner address")
286            return PrizeVaultV3.userDeposits[ownerAddress] ?? 0.0
287        }
288        
289        // Get prize balance only
290        access(all) fun getPrizeBalance(): UFix64 {
291            let ownerAddress = self.owner?.address ?? panic("No owner address")
292            return PrizeVaultV3.userPrizes[ownerAddress] ?? 0.0
293        }
294        
295        access(all) fun getPendingWithdrawal(): UFix64 {
296            let ownerAddress = self.owner?.address ?? panic("No owner address")
297            return PrizeVaultV3.pendingWithdrawals[ownerAddress] ?? 0.0
298        }
299    }
300    
301    // Create a new DepositReceiver for users
302    access(all) fun createDepositReceiver(): @DepositReceiver {
303        return <- create DepositReceiver()
304    }
305    
306    // Internal function to withdraw from vault
307    access(contract) fun withdrawFromVault(amount: UFix64): @{FungibleToken.Vault} {
308        let withdrawn <- self.vault.withdraw(amount: amount)
309        return <- withdrawn
310    }
311    
312    // Internal function to stake tokens via EVM (Ankr)
313    access(contract) fun stakeTokens(amount: UFix64) {
314        // Withdraw FLOW from vault as FlowToken.Vault
315        let tokensToStake <- self.vault.withdraw(amount: amount) as! @FlowToken.Vault
316        
317        // Deposit FLOW into COA - COA needs balance to send value in EVM calls
318        let coaRef = (&self.coa as auth(EVM.Call) &EVM.CadenceOwnedAccount)!
319        let _ = coaRef.deposit(from: <- tokensToStake)
320        
321        // Prepare EVM call to stakeCerts() - function signature: 0xac76d450 (from working MetaMask tx)
322        let stakeCertsCalldata: [UInt8] = [0xac, 0x76, 0xd4, 0x50]
323        
324        // Create balance value to send with call
325        let value = EVM.Balance(attoflow: 0)
326        value.setFLOW(flow: amount)
327        
328        // Call stakeCerts on EVM staking pool
329        // MetaMask uses ~155k gas, so we set 300k to be safe
330        let callResult = coaRef.call(
331            to: self.evmStakingPoolAddress,
332            data: stakeCertsCalldata,
333            gasLimit: 300000,
334            value: value
335        )
336        
337        assert(
338            callResult.status == EVM.Status.successful,
339            message: "EVM staking call failed: ".concat(callResult.errorMessage)
340        )
341        
342        // Update total staked
343        self.totalStaked = self.totalStaked + amount
344        
345        emit Staked(amount: amount)
346    }
347    
348    // Internal function to unstake tokens from Ankr EVM
349    access(contract) fun unstakeTokens(amount: UFix64) {
350        let coaRef = (&self.coa as auth(EVM.Call) &EVM.CadenceOwnedAccount)!
351        
352        // Prepare EVM call to unstakeCerts(uint256 shares)
353        // Function selector for unstakeCerts(uint256): 0x0d904ce2 (from working FlowScan tx)
354        // We need to encode: selector (4 bytes) + shares parameter (32 bytes)
355        
356        // Calculate ankrFLOW shares to unstake based on proportional ownership
357        // User's share of pool = (amount / totalDeposited) * totalAnkrFLOWBalance
358        // This ensures we never try to unstake more ankrFLOW than we actually have
359        let totalAnkrBalance = self.getAnkrFLOWEVMBalance()
360        let userProportion = amount / self.totalDeposited
361        let sharesToUnstake = userProportion * totalAnkrBalance
362        
363        // Convert shares to attoflow (wei) for EVM call
364        let balance = EVM.Balance(attoflow: 0)
365        balance.setFLOW(flow: sharesToUnstake)
366        let shares = balance.attoflow // This is the amount in attoflow (18 decimals)
367        
368        // Encode function call: selector + uint256 parameter (32 bytes, big-endian)
369        var calldata: [UInt8] = [0x0d, 0x90, 0x4c, 0xe2] // unstakeCerts(uint256) selector
370        
371        // Encode shares as 32-byte big-endian uint256
372        var sharesBytes: [UInt8] = []
373        var remaining = shares
374        
375        // Convert shares to bytes (big-endian) - build in reverse then reverse
376        var tempBytes: [UInt8] = []
377        while remaining > 0 {
378            tempBytes.append(UInt8(remaining % 256))
379            remaining = remaining / 256
380        }
381        
382        // Pad with zeros to 32 bytes and reverse to big-endian
383        let paddingNeeded = 32 - tempBytes.length
384        var i = 0
385        while i < paddingNeeded {
386            sharesBytes.append(0)
387            i = i + 1
388        }
389        
390        // Append the actual value bytes in reverse (to get big-endian)
391        var k = tempBytes.length
392        while k > 0 {
393            k = k - 1
394            sharesBytes.append(tempBytes[k])
395        }
396        
397        // Append shares bytes to calldata
398        calldata = calldata.concat(sharesBytes)
399        
400        // Call unstakeCerts on Ankr EVM contract (no value sent)
401        let callResult = coaRef.call(
402            to: self.evmStakingPoolAddress,
403            data: calldata,
404            gasLimit: 300000,
405            value: EVM.Balance(attoflow: 0)
406        )
407        
408        assert(
409            callResult.status == EVM.Status.successful,
410            message: "EVM unstaking call failed: ".concat(callResult.errorMessage)
411        )
412        
413        emit Unstaked(amount: amount)
414    }
415    
416    // Internal function to withdraw FLOW from COA back to vault
417    access(contract) fun withdrawFromCOA(amount: UFix64) {
418        let coaRef = (&self.coa as auth(EVM.Withdraw) &EVM.CadenceOwnedAccount)!
419        
420        // Create balance to withdraw
421        let balance = EVM.Balance(attoflow: 0)
422        balance.setFLOW(flow: amount)
423        
424        // Withdraw from COA
425        let withdrawn <- coaRef.withdraw(balance: balance)
426        
427        // Deposit into vault
428        self.vault.deposit(from: <- withdrawn)
429    }
430    
431    // Query ankrFLOWEVM token balance from EVM
432    access(contract) fun getAnkrFLOWEVMBalance(): UFix64 {
433        let coaRef = (&self.coa as auth(EVM.Call) &EVM.CadenceOwnedAccount)!
434        
435        // ERC20 balanceOf(address) function signature: 0x70a08231
436        var calldata: [UInt8] = [0x70, 0xa0, 0x82, 0x31]
437        
438        // Encode COA address as parameter (20 bytes padded to 32 bytes)
439        let coaAddress = coaRef.address()
440        let addressBytes = coaAddress.bytes
441        
442        // Pad address to 32 bytes (12 zeros + 20 address bytes)
443        var j = 0
444        while j < 12 {
445            calldata.append(0)
446            j = j + 1
447        }
448        for byte in addressBytes {
449            calldata.append(byte)
450        }
451        
452        // Call balanceOf on ankrFLOWEVM token
453        let callResult = coaRef.call(
454            to: self.ankrFlowTokenAddress,
455            data: calldata,
456            gasLimit: 100000,
457            value: EVM.Balance(attoflow: 0)
458        )
459        
460        assert(
461            callResult.status == EVM.Status.successful,
462            message: "Failed to query ankrFLOWEVM balance: ".concat(callResult.errorMessage)
463        )
464        
465        // Decode uint256 from return data (32 bytes, big-endian)
466        let returnData = callResult.data
467        assert(returnData.length >= 32, message: "Invalid return data length")
468        
469        // Convert bytes to UInt (last 32 bytes are the balance in attoflow)
470        var balance: UInt = 0
471        var i = 0
472        while i < 32 {
473            balance = balance * 256 + UInt(returnData[i])
474            i = i + 1
475        }
476        
477        // Convert attoflow to FLOW
478        let evmBalance = EVM.Balance(attoflow: balance)
479        return evmBalance.inFLOW()
480    }
481    
482    // Query exchange rate from Ankr ratio feed (how much FLOW per ankrFLOWEVM)
483    access(contract) fun getAnkrExchangeRate(): UFix64 {
484        let coaRef = (&self.coa as auth(EVM.Call) &EVM.CadenceOwnedAccount)!
485        
486        // getRatioFor(address) function signature: 0xa1f1d48d
487        var calldata: [UInt8] = [0xa1, 0xf1, 0xd4, 0x8d]
488        
489        // Encode the ankrFLOWEVM token address as parameter (32 bytes, padded)
490        let tokenAddressBytes = self.ankrFlowTokenAddress.bytes
491        
492        // Pad with 12 zeros (32 bytes total - 20 bytes address = 12 bytes padding)
493        var padIndex = 0
494        while padIndex < 12 {
495            calldata.append(0)
496            padIndex = padIndex + 1
497        }
498        
499        // Append the 20-byte token address
500        for byte in tokenAddressBytes {
501            calldata.append(byte)
502        }
503        
504        // Call getRatioFor(address) on AnkrRatioFeed
505        let callResult = coaRef.call(
506            to: self.ankrRatioFeedAddress,
507            data: calldata,
508            gasLimit: 100000,
509            value: EVM.Balance(attoflow: 0)
510        )
511        
512        assert(
513            callResult.status == EVM.Status.successful,
514            message: "Failed to query Ankr exchange rate: ".concat(callResult.errorMessage)
515        )
516        
517        // Decode uint256 from return data (ratio in 18 decimals)
518        let returnData = callResult.data
519        assert(returnData.length >= 32, message: "Invalid return data length")
520        
521        // Convert bytes to UInt
522        var ratio: UInt = 0
523        var i = 0
524        while i < 32 {
525            ratio = ratio * 256 + UInt(returnData[i])
526            i = i + 1
527        }
528        
529        // Convert ratio (18 decimals) to UFix64 (8 decimals)
530        // ratio is in format: 1.05 * 10^18 = 1050000000000000000
531        // We need to convert to UFix64: 1.05 = 105000000 (8 decimals)
532        let rateBalance = EVM.Balance(attoflow: ratio)
533        return rateBalance.inFLOW()
534    }
535    
536    // Calculate available rewards to harvest
537    access(all) fun calculateAvailableRewards(): UFix64 {
538        // Get ankrFLOWEVM balance
539        let ankrBalance = self.getAnkrFLOWEVMBalance()
540        
541        // Get exchange rate
542        let rate = self.getAnkrExchangeRate()
543        
544        // Calculate total FLOW value of ankrFLOWEVM tokens
545        let totalValue = ankrBalance * rate
546        
547        // Available rewards = total value - originally staked amount
548        let rewards = totalValue - self.totalStaked
549        
550        return rewards > 0.0 ? rewards : 0.0
551    }
552    
553    // Harvest staking rewards by unstaking the profit portion
554    access(contract) fun harvestStakingRewards() {
555        let availableRewards = self.calculateAvailableRewards()
556        
557        assert(availableRewards > 0.0, message: "No rewards to harvest")
558        
559        // Unstake the rewards amount (this will be in ankrFLOWEVM tokens)
560        // We need to convert FLOW amount to ankrFLOWEVM shares
561        let rate = self.getAnkrExchangeRate()
562        let sharesToUnstake = availableRewards / rate
563        
564        // Unstake the shares
565        self.unstakeTokens(amount: sharesToUnstake)
566        
567        // After unstaking completes (a few minutes), the FLOW will be in COA
568        // Admin can then move it to vault for prize distribution
569        
570        self.totalRewardsHarvested = self.totalRewardsHarvested + availableRewards
571    }
572    
573    // PUBLIC FUNCTION: Start monthly prize draw (anyone can call after sufficient blocks have passed)
574    access(all) fun startMonthlyDraw() {
575        // Check if enough blocks have passed since last draw
576        let currentBlock = getCurrentBlock().height
577        let blocksSinceLastDraw = currentBlock - self.lastDrawBlock
578        assert(
579            self.canDrawNow(), 
580            message: "Not enough blocks have passed since last draw. Current block: "
581                .concat(currentBlock.toString())
582                .concat(", Last draw block: ")
583                .concat(self.lastDrawBlock.toString())
584                .concat(", Blocks since: ")
585                .concat(blocksSinceLastDraw.toString())
586                .concat(", Required: ")
587                .concat(self.blocksPerMonth.toString())
588        )
589        
590        assert(self.monthlyDrawReceipt == nil, message: "Previous monthly draw not completed. Call completeMonthlyDraw() first.")
591        
592        // Calculate available rewards to distribute
593        let availableRewards = self.calculateAvailableRewards()
594        assert(availableRewards > 0.0, message: "No rewards available for distribution")
595        
596        // Harvest the rewards (unstake from Ankr)
597        // After unstaking completes (~5 minutes), rewards will be in COA
598        // Prizes stay in COA (they're not withdrawable anyway)
599        self.harvestStakingRewards()
600        
601        // Note: After calling this, wait ~5 minutes for unstaking to complete,
602        // then call completeMonthlyDraw() to finish the draw and award the prize
603        
604        // Update last draw block height
605        self.lastDrawBlock = currentBlock
606        
607        // Commit the prize draw (this will be completed later)
608        let receipt <- self.commitPrize(amount: availableRewards)
609        self.monthlyDrawReceipt <-! receipt
610    }
611    
612    
613    // PUBLIC FUNCTION: Complete monthly prize draw (anyone can call after commitment)
614    access(all) fun completeMonthlyDraw() {
615        assert(self.monthlyDrawReceipt != nil, message: "No monthly draw in progress. Call startMonthlyDraw() first.")
616        
617        // Get the receipt
618        let receipt <- self.monthlyDrawReceipt <- nil
619        
620        // Complete the prize reveal and award
621        self.revealPrize(receipt: <- receipt!)
622    }
623    
624    // Commit phase: Lock in the prize amount and request randomness
625    access(contract) fun commitPrize(amount: UFix64): @PrizeDrawReceipt {
626        assert(self.userDeposits.length > 0, message: "No users with deposits")
627        
628        // Check solvency: prizes stay in COA, so check both vault and available COA rewards
629        let totalAvailableForPrizes = self.vault.balance + self.getAvailableRewardsInCOA()
630        assert(totalAvailableForPrizes >= amount, 
631               message: "Insufficient balance for prize. Available: ".concat(totalAvailableForPrizes.toString())
632                       .concat(" FLOW, Prize: ").concat(amount.toString()).concat(" FLOW"))
633        
634        // Request randomness from RandomConsumer
635        let request <- self.consumer.requestRandomness()
636        
637        // Create receipt with prize amount and random request
638        let receipt <- create PrizeDrawReceipt(
639            prizeAmount: amount,
640            request: <- request
641        )
642        
643        let commitBlock = receipt.getRequestBlock()!
644        
645        emit PrizeDrawCommitted(prizeAmount: amount, commitBlock: commitBlock, receiptID: receipt.uuid)
646        
647        return <- receipt
648    }
649    
650    // Reveal phase: Use receipt to get random number and award prize
651    access(contract) fun revealPrize(receipt: @PrizeDrawReceipt) {
652        let prizeAmount = receipt.prizeAmount
653        let commitBlock = receipt.getRequestBlock()!
654        let receiptID = receipt.uuid
655        
656        // Fulfill the random request to get the random value
657        let request <- receipt.popRequest()
658        let randomNumber = self.consumer.fulfillRandomRequest(<-request)
659        
660        // Destroy the receipt
661        destroy receipt
662        
663        // Select winner using weighted random selection
664        let winnerAddress = self.selectWeightedWinner(randomNumber: randomNumber)
665        
666        // Award the prize by increasing winner's prize balance
667        let currentPrizes = self.userPrizes[winnerAddress] ?? 0.0
668        self.userPrizes[winnerAddress] = currentPrizes + prizeAmount
669        
670        // Update prize tracking
671        self.prizeRound = self.prizeRound + 1
672        self.totalPrizesDistributed = self.totalPrizesDistributed + prizeAmount
673        self.prizeHistory[self.prizeRound] = winnerAddress
674        
675        // Emit prize awarded event
676        emit PrizeAwarded(
677            winner: winnerAddress, 
678            amount: prizeAmount, 
679            round: self.prizeRound, 
680            commitBlock: commitBlock, 
681            receiptID: receiptID
682        )
683    }
684    
685    // Weighted random selection: Select winner based on deposit amount
686    // Each FLOW token = 1 "ticket" in the lottery
687    // User with 100 FLOW has 100x better chance than user with 1 FLOW
688    access(contract) fun selectWeightedWinner(randomNumber: UInt64): Address {
689        let depositors = self.userDeposits.keys
690        assert(depositors.length > 0, message: "No depositors")
691        
692        // Handle single depositor case
693        if depositors.length == 1 {
694            return depositors[0]
695        }
696        
697        // Build cumulative sum array
698        // Example: [10, 70, 20] -> cumulative: [10, 80, 100]
699        var cumulativeSum: [UFix64] = []
700        var runningTotal: UFix64 = 0.0
701        
702        for addr in depositors {
703            let amount = self.userDeposits[addr]!
704            runningTotal = runningTotal + amount
705            cumulativeSum.append(runningTotal)
706        }
707        
708        // Convert UInt64 random number to proportional value in [0, totalDeposited)
709        // We use modulo to get a value in range [0, runningTotal)
710        // For better distribution with large deposits, we scale appropriately
711        let randomValue = UFix64(randomNumber % UInt64(runningTotal * 100000000.0)) / 100000000.0
712        
713        // Find winner using cumulative sum (similar to binary search)
714        // The random value will fall into one depositor's range
715        var winnerIndex = 0
716        for i, cumSum in cumulativeSum {
717            if randomValue < cumSum {
718                winnerIndex = i
719                break
720            }
721        }
722        
723        return depositors[winnerIndex]
724    }
725    
726    // Public getters
727    access(all) fun getTotalDeposited(): UFix64 {
728        return self.totalDeposited
729    }
730    
731    access(all) fun getTotalStaked(): UFix64 {
732        return self.totalStaked
733    }
734    
735    access(all) fun getTotalRewardsHarvested(): UFix64 {
736        return self.totalRewardsHarvested
737    }
738    
739    access(all) fun getTotalPrizesDistributed(): UFix64 {
740        return self.totalPrizesDistributed
741    }
742    
743    access(all) fun getCurrentPrizeRound(): UInt64 {
744        return self.prizeRound
745    }
746    
747    access(all) fun getVaultBalance(): UFix64 {
748        return self.vault.balance
749    }
750    
751    access(all) fun getUserDeposit(address: Address): UFix64 {
752        let deposit = self.userDeposits[address] ?? 0.0
753        let prize = self.userPrizes[address] ?? 0.0
754        return deposit + prize
755    }
756    
757    access(all) fun getUserDepositOnly(address: Address): UFix64 {
758        return self.userDeposits[address] ?? 0.0
759    }
760    
761    access(all) fun getUserPrizes(address: Address): UFix64 {
762        return self.userPrizes[address] ?? 0.0
763    }
764    
765    access(all) fun getUserPendingWithdrawal(address: Address): UFix64 {
766        return self.pendingWithdrawals[address] ?? 0.0
767    }
768    
769    access(all) fun getPrizeWinner(round: UInt64): Address? {
770        return self.prizeHistory[round]
771    }
772    
773    access(all) fun getLastDrawBlock(): UInt64 {
774        return self.lastDrawBlock
775    }
776    
777    access(all) fun getBlocksPerMonth(): UInt64 {
778        return self.blocksPerMonth
779    }
780    
781    access(all) fun getCurrentBlock(): UInt64 {
782        return getCurrentBlock().height
783    }
784    
785    access(all) fun getBlocksSinceLastDraw(): UInt64 {
786        return getCurrentBlock().height - self.lastDrawBlock
787    }
788    
789    access(all) fun getBlocksUntilNextDraw(): UInt64 {
790        let blocksSince = self.getBlocksSinceLastDraw()
791        if blocksSince >= self.blocksPerMonth {
792            return 0
793        }
794        return self.blocksPerMonth - blocksSince
795    }
796    
797    access(all) fun canDrawNow(): Bool {
798        return self.getBlocksSinceLastDraw() >= self.blocksPerMonth
799    }
800    
801    access(all) fun isMonthlyDrawInProgress(): Bool {
802        return self.monthlyDrawReceipt != nil
803    }
804    
805    // Get COA EVM balance in FLOW
806    access(all) fun getCOABalance(): UFix64 {
807        let coaRef = &self.coa as &EVM.CadenceOwnedAccount
808        let balance = coaRef.balance()
809        // Use the built-in conversion method
810        return balance.inFLOW()
811    }
812    
813    // Get COA EVM address as hex string
814    access(all) fun getCOAAddress(): String {
815        let coaRef = &self.coa as &EVM.CadenceOwnedAccount
816        return coaRef.address().toString()
817    }
818    
819    // Get the Ankr staking pool EVM address
820    access(all) fun getStakingPoolAddress(): String {
821        return self.evmStakingPoolAddress.toString()
822    }
823    
824    // Get minimum deposit requirement
825    access(all) fun getMinimumDeposit(): UFix64 {
826        return self.minimumDeposit
827    }
828    
829    // Calculate user's winning probability (returns value between 0.0 and 1.0)
830    // Example: 0.05 = 5% chance to win
831    access(all) fun getUserWinningChance(address: Address): UFix64 {
832        let userDeposit = self.userDeposits[address] ?? 0.0
833        
834        if userDeposit == 0.0 || self.totalDeposited == 0.0 {
835            return 0.0
836        }
837        
838        return userDeposit / self.totalDeposited
839    }
840    
841    // Get total pending withdrawals in COA (for transparency)
842    access(all) fun getTotalPendingWithdrawalInCOA(): UFix64 {
843        return self.totalPendingWithdrawalInCOA
844    }
845    
846    // Get available rewards in COA (safe to move to vault)
847    access(all) fun getAvailableRewardsInCOA(): UFix64 {
848        let coaBalance = self.getCOABalance()
849        let availableRewards = coaBalance - self.totalPendingWithdrawalInCOA
850        return availableRewards > 0.0 ? availableRewards : 0.0
851    }
852    
853    // Public function to get ankrFLOWEVM balance
854    access(all) fun getAnkrFLOWBalance(): UFix64 {
855        return self.getAnkrFLOWEVMBalance()
856    }
857    
858    // Public function to get Ankr exchange rate (FLOW per ankrFLOW)
859    access(all) fun getAnkrRatio(): UFix64 {
860        return self.getAnkrExchangeRate()
861    }
862    
863    // Get ankrFLOWEVM token address
864    access(all) fun getAnkrTokenAddress(): String {
865        return self.ankrFlowTokenAddress.toString()
866    }
867    
868    // Get total value locked (TVL) in FLOW
869    access(all) fun getTotalValueLocked(): UFix64 {
870        // TVL = vault balance + staked amount (already in FLOW terms)
871        return self.getVaultBalance() + self.totalStaked
872    }
873    
874    init(evmStakingPoolAddressHex: String) {
875        // Initialize constants
876        self.minimumDeposit = 1.0  // 1 FLOW minimum to prevent dust/sybil attacks
877        
878        // Initialize paths
879        self.DepositReceiverStoragePath = /storage/PrizeVaultV3DepositReceiver
880        self.DepositReceiverPublicPath = /public/PrizeVaultV3DepositReceiver
881        self.VaultStoragePath = /storage/PrizeVaultV3MainVault
882        self.AdminStoragePath = /storage/PrizeVaultV3Admin
883        self.PrizeDrawReceiptStoragePath = /storage/PrizeVaultV3DrawReceipt
884        
885        // Initialize state
886        self.totalDeposited = 0.0
887        self.totalStaked = 0.0
888        self.totalRewardsHarvested = 0.0
889        self.totalPrizesDistributed = 0.0
890        self.prizeRound = 0
891        self.lastDrawBlock = 0  // Allow first draw immediately (0 means never drawn before)
892        self.blocksPerMonth = 2592000  // ~30 days at 1 second per block on Flow
893        self.monthlyDrawReceipt <- nil
894        self.totalPendingWithdrawalInCOA = 0.0  // No pending withdrawals initially
895        self.userDeposits = {}
896        self.userPrizes = {}
897        self.pendingWithdrawals = {}
898        self.prizeHistory = {}
899        
900        // Set EVM contract addresses
901        self.evmStakingPoolAddress = EVM.addressFromString(evmStakingPoolAddressHex)
902        
903        // ankrFLOWEVM token address on Flow EVM
904        self.ankrFlowTokenAddress = EVM.addressFromString("1b97100eA1D7126C4d60027e231EA4CB25314bdb")
905        
906        // Ankr ratio feed address on Flow EVM
907        self.ankrRatioFeedAddress = EVM.addressFromString("32015e1Bd4bAAC9b959b100B0ca253BD131dE38F")
908        
909        // Create the main vault to hold all deposits
910        self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
911        
912        // Create COA for EVM interactions
913        self.coa <- EVM.createCadenceOwnedAccount()
914        
915        // Initialize RandomConsumer for commit-reveal randomness
916        self.consumer <- RandomConsumer.createConsumer()
917        
918        // Create and save Admin resource
919        self.account.storage.save(<- create Admin(), to: self.AdminStoragePath)
920    }
921}
922
923