Smart Contract
PrizeVaultV3
A.262cf58c0b9fbcff.PrizeVaultV3
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