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