Smart Contract

PrizeVault

A.262cf58c0b9fbcff.PrizeVault

Valid From

130,224,845

Deployed

1w ago
Feb 21, 2026, 02:11:17 PM UTC

Dependents

3 imports
1/*
2This smart contract is designed to function similarly to the PoolTogether app:
3
4Users can deposit FLOW tokens into the contract.
5The deposited tokens are integrated with Ankr's StakingManager on the Flow blockchain (Cadence).
6Once deposited, the tokens are staked to generate yield.
7Periodically, the yield is harvested from Ankr's staking contract.
8The accumulated yield is then randomly distributed as prizes to selected users.
9Prize winners will see their balances increase, and funds can be withdrawn at any time.
10
11In essence, users retain ownership of their principal deposits while participating 
12in periodic prize draws funded by the staking rewards — creating a lossless lottery model.
13*/
14
15import FungibleToken from 0xf233dcee88fe0abe
16import FlowToken from 0x1654653399040a61
17import FlowStakingCollection from 0x8d0e87b65159ae63
18import StakingManager from 0x70466175fbacfb56
19import RandomConsumer from 0x45caec600164c9e6
20
21access(all) contract PrizeVault {
22    
23    // Events
24    access(all) event Deposited(address: Address, amount: UFix64)
25    access(all) event WithdrawalRequested(address: Address, amount: UFix64)
26    access(all) event Withdrawn(address: Address, amount: UFix64)
27    access(all) event Staked(amount: UFix64, delegationTargetIdx: UInt)
28    access(all) event Unstaked(amount: UFix64, delegationTargetIdx: UInt)
29    access(all) event RewardsHarvested(amount: UFix64, delegationTargetIdx: UInt)
30    access(all) event PrizeDrawCommitted(prizeAmount: UFix64, commitBlock: UInt64, receiptID: UInt64)
31    access(all) event PrizeAwarded(winner: Address, amount: UFix64, round: UInt64, commitBlock: UInt64, receiptID: UInt64)
32    access(all) event DepositReceiverCreated(address: Address)
33    access(all) event DelegationTargetSet(delegationTargetIdx: UInt)
34    
35    // Storage Paths
36    access(all) let DepositReceiverStoragePath: StoragePath
37    access(all) let DepositReceiverPublicPath: PublicPath
38    access(all) let VaultStoragePath: StoragePath
39    access(all) let AdminStoragePath: StoragePath
40    access(all) let PrizeDrawReceiptStoragePath: StoragePath
41    
42    // Total deposited across all users
43    access(all) var totalDeposited: UFix64
44    
45    // Total staked amount
46    access(all) var totalStaked: UFix64
47    
48    // Total rewards harvested from staking
49    access(all) var totalRewardsHarvested: UFix64
50    
51    // Total prizes distributed to users
52    access(all) var totalPrizesDistributed: UFix64
53    
54    // Current prize round number
55    access(all) var prizeRound: UInt64
56    
57    // Mapping of user addresses to their deposit amounts
58    access(self) let userDeposits: {Address: UFix64}
59    
60    // Mapping of user addresses to their pending withdrawal amounts
61    access(self) let pendingWithdrawals: {Address: UFix64}
62    
63    // Prize history: round number -> winner address
64    access(self) let prizeHistory: {UInt64: Address}
65    
66    // Main vault to hold all deposited FLOW tokens
67    access(self) let vault: @FlowToken.Vault
68    
69    // StakingManager account address
70    access(self) let stakingManagerAddress: Address
71    
72    // Capability to interact with StakingManager (optional until set up)
73    access(self) var stakingManagerCap: Capability<auth(StakingManager.StakingOperator) &StakingManager.Staker>?
74    
75    // Delegation target index to use for staking
76    access(self) var delegationTargetIdx: UInt
77    
78    // RandomConsumer for commit-reveal randomness
79    access(self) let consumer: @RandomConsumer.Consumer
80    
81    // Receipt resource for prize draw commit-reveal
82    access(all) resource PrizeDrawReceipt {
83        access(all) let prizeAmount: UFix64
84        access(self) var request: @RandomConsumer.Request?
85        
86        init(prizeAmount: UFix64, request: @RandomConsumer.Request) {
87            self.prizeAmount = prizeAmount
88            self.request <- request
89        }
90        
91        // Get the block height at which randomness was committed
92        access(all) view fun getRequestBlock(): UInt64? {
93            return self.request?.block
94        }
95        
96        // Pop the request for fulfillment (can only be called once)
97        access(contract) fun popRequest(): @RandomConsumer.Request {
98            let request <- self.request <- nil
99            return <- request!
100        }
101    }
102    
103    // Admin resource to manage contract configuration
104    access(all) resource Admin {
105        // Set the delegation target index for staking
106        access(all) fun setDelegationTarget(idx: UInt) {
107            PrizeVault.delegationTargetIdx = idx
108            emit DelegationTargetSet(delegationTargetIdx: idx)
109        }
110        
111        // Harvest staking rewards from StakingManager
112        access(all) fun harvestRewards(amount: UFix64) {
113            PrizeVault.collectRewards(amount: amount)
114        }
115        
116        // Commit phase: Start a prize draw (returns receipt to be used in reveal)
117        access(all) fun commitPrizeDraw(prizeAmount: UFix64): @PrizeDrawReceipt {
118            return <- PrizeVault.commitPrize(amount: prizeAmount)
119        }
120        
121        // Reveal phase: Complete the prize draw using the receipt
122        access(all) fun revealPrizeDraw(receipt: @PrizeDrawReceipt) {
123            PrizeVault.revealPrize(receipt: <- receipt)
124        }
125    }
126    
127    // Public interface that users can expose
128    access(all) resource interface DepositReceiverPublic {
129        access(all) fun deposit(from: @{FungibleToken.Vault})
130        access(all) fun requestWithdrawal(amount: UFix64)
131        access(all) fun completeWithdrawal(): @{FungibleToken.Vault}
132        access(all) fun getBalance(): UFix64
133        access(all) fun getPendingWithdrawal(): UFix64
134    }
135    
136    // Resource that users store in their account to deposit and track their balance
137    access(all) resource DepositReceiver: DepositReceiverPublic {
138        
139        // User's deposited amount
140        access(self) var balance: UFix64
141        
142        init() {
143            self.balance = 0.0
144        }
145        
146        // Deposit FLOW tokens into the prize vault
147        access(all) fun deposit(from: @{FungibleToken.Vault}) {
148            let amount = from.balance
149            
150            // Get the owner's address
151            let ownerAddress = self.owner?.address ?? panic("No owner address")
152            
153            // Cast to FlowToken.Vault to ensure it's FLOW tokens
154            let flowVault <- from as! @FlowToken.Vault
155            
156            // Update user's balance
157            self.balance = self.balance + amount
158            
159            // Update contract state
160            PrizeVault.userDeposits[ownerAddress] = self.balance
161            PrizeVault.totalDeposited = PrizeVault.totalDeposited + amount
162            
163            // Deposit tokens into the main vault
164            PrizeVault.vault.deposit(from: <-flowVault)
165            
166            // Emit deposit event
167            emit Deposited(address: ownerAddress, amount: amount)
168            
169            // Automatically stake the deposited tokens
170            PrizeVault.stakeTokens(amount: amount)
171        }
172        
173        // Withdraw FLOW tokens - immediate if not staked, or request unstaking if staked
174        access(all) fun requestWithdrawal(amount: UFix64) {
175            // Get the owner's address
176            let ownerAddress = self.owner?.address ?? panic("No owner address")
177            
178            // Check user has sufficient balance
179            assert(self.balance >= amount, message: "Insufficient balance to withdraw")
180            
181            // Check user doesn't already have a pending withdrawal
182            let existingPending = PrizeVault.pendingWithdrawals[ownerAddress] ?? 0.0
183            assert(existingPending == 0.0, message: "You already have a pending withdrawal. Complete it first.")
184            
185            // If staking is not enabled, allow immediate withdrawal
186            if PrizeVault.stakingManagerCap == nil {
187                // Immediate withdrawal (no staking active)
188                let withdrawn <- PrizeVault.withdrawFromVault(amount: amount)
189                
190                // Update user's balance
191                self.balance = self.balance - amount
192                PrizeVault.userDeposits[ownerAddress] = self.balance
193                PrizeVault.totalDeposited = PrizeVault.totalDeposited - amount
194                
195                // Deposit to user's vault
196                let userVault = getAccount(ownerAddress).capabilities
197                    .get<&FlowToken.Vault>(/public/flowTokenReceiver)
198                    .borrow() ?? panic("Could not borrow user's FlowToken receiver")
199                userVault.deposit(from: <- withdrawn)
200                
201                emit Withdrawn(address: ownerAddress, amount: amount)
202            } else {
203                // Staking enabled - request unstaking (2 epoch wait)
204                PrizeVault.requestUnstaking(amount: amount)
205                
206                // Update user's balance (funds now in pending state)
207                self.balance = self.balance - amount
208                PrizeVault.userDeposits[ownerAddress] = self.balance
209                PrizeVault.totalDeposited = PrizeVault.totalDeposited - amount
210                PrizeVault.pendingWithdrawals[ownerAddress] = amount
211                
212                emit WithdrawalRequested(address: ownerAddress, amount: amount)
213            }
214        }
215        
216        // Complete withdrawal: After unstaking period, withdraw the funds
217        access(all) fun completeWithdrawal(): @{FungibleToken.Vault} {
218            // Get the owner's address
219            let ownerAddress = self.owner?.address ?? panic("No owner address")
220            
221            // Get pending withdrawal amount
222            let pendingAmount = PrizeVault.pendingWithdrawals[ownerAddress] 
223                ?? panic("No pending withdrawal found")
224            
225            assert(pendingAmount > 0.0, message: "No pending withdrawal")
226            
227            // Withdraw unstaked tokens from StakingManager to vault
228            PrizeVault.withdrawUnstaked(amount: pendingAmount)
229            
230            // Now withdraw from vault
231            let withdrawn <- PrizeVault.withdrawFromVault(amount: pendingAmount)
232            
233            // Clear pending withdrawal
234            PrizeVault.pendingWithdrawals[ownerAddress] = 0.0
235            
236            // Emit withdrawal complete event
237            emit Withdrawn(address: ownerAddress, amount: pendingAmount)
238            
239            return <- withdrawn
240        }
241        
242        // Get the user's current deposited balance
243        access(all) fun getBalance(): UFix64 {
244            return self.balance
245        }
246        
247        // Get the user's pending withdrawal amount
248        access(all) fun getPendingWithdrawal(): UFix64 {
249            let ownerAddress = self.owner?.address ?? panic("No owner address")
250            return PrizeVault.pendingWithdrawals[ownerAddress] ?? 0.0
251        }
252    }
253    
254    // Create a new DepositReceiver for a user
255    access(all) fun createDepositReceiver(): @DepositReceiver {
256        return <- create DepositReceiver()
257    }
258    
259    // Internal function to withdraw from the vault
260    access(contract) fun withdrawFromVault(amount: UFix64): @{FungibleToken.Vault} {
261        // Check if vault has sufficient balance
262        assert(
263            self.vault.balance >= amount, 
264            message: "Insufficient vault balance. Available: ".concat(self.vault.balance.toString())
265                .concat(", Requested: ").concat(amount.toString())
266                .concat(". Unstaking period may not be complete yet (~2 epochs).")
267        )
268        
269        // Withdraw from vault
270        let withdrawn <- self.vault.withdraw(amount: amount)
271        
272        return <- withdrawn
273    }
274    
275    // Internal function to stake tokens through StakingManager
276    access(contract) fun stakeTokens(amount: UFix64) {
277        // Skip staking if capability not set up yet
278        if self.stakingManagerCap == nil {
279            return
280        }
281        
282        // Transfer tokens from PrizeVault to StakingManager vault
283        let tokensToStake <- self.vault.withdraw(amount: amount)
284        
285        // Get StakingManager's vault and deposit tokens
286        let stakingManagerAccount = getAccount(self.stakingManagerAddress)
287        let stakingVaultCap = stakingManagerAccount.capabilities
288            .get<&FlowToken.Vault>(/public/stakingManagerVault)
289        let stakingVault = stakingVaultCap.borrow() 
290            ?? panic("Could not borrow StakingManager vault")
291        stakingVault.deposit(from: <- tokensToStake)
292        
293        // Stake the tokens using StakingManager
294        let stakerRef = self.stakingManagerCap!.borrow() 
295            ?? panic("Could not borrow StakingManager Staker capability")
296        stakerRef.Stake(amount: amount, delegationTargetIdx: self.delegationTargetIdx)
297        
298        // Update total staked
299        self.totalStaked = self.totalStaked + amount
300        
301        emit Staked(amount: amount, delegationTargetIdx: self.delegationTargetIdx)
302    }
303    
304    // Internal function to collect staking rewards
305    access(contract) fun collectRewards(amount: UFix64) {
306        assert(self.stakingManagerCap != nil, message: "StakingManager capability not set up")
307        
308        // Call WithdrawRewarded on StakingManager
309        // This withdraws rewards from staking to the StakingManager vault (after fee deduction)
310        let stakerRef = self.stakingManagerCap!.borrow() 
311            ?? panic("Could not borrow StakingManager Staker capability")
312        stakerRef.WithdrawRewarded(amount: amount, delegationTargetIdx: self.delegationTargetIdx)
313        
314        // Get StakingManager's vault with withdraw capability
315        let stakingManagerAccount = getAccount(self.stakingManagerAddress)
316        let stakingVaultCap = stakingManagerAccount.capabilities
317            .get<auth(FungibleToken.Withdraw) &FlowToken.Vault>(/public/stakingManagerVault)
318        let stakingVault = stakingVaultCap.borrow() 
319            ?? panic("Could not borrow StakingManager vault with withdraw capability")
320        
321        // Withdraw rewards from StakingManager vault to PrizeVault
322        let rewards <- stakingVault.withdraw(amount: amount)
323        self.vault.deposit(from: <- rewards)
324        
325        // Track total rewards harvested
326        self.totalRewardsHarvested = self.totalRewardsHarvested + amount
327        
328        emit RewardsHarvested(amount: amount, delegationTargetIdx: self.delegationTargetIdx)
329    }
330    
331    // Internal function to request unstaking (for user withdrawals)
332    access(contract) fun requestUnstaking(amount: UFix64) {
333        assert(self.stakingManagerCap != nil, message: "StakingManager capability not set up")
334        
335        let stakerRef = self.stakingManagerCap!.borrow() 
336            ?? panic("Could not borrow StakingManager Staker capability")
337        
338        // Request unstaking - tokens will be available after 2 epochs
339        stakerRef.RequestUnstake(amount: amount, delegationTargetIdx: self.delegationTargetIdx)
340        
341        // Update total staked
342        self.totalStaked = self.totalStaked - amount
343        
344        emit Unstaked(amount: amount, delegationTargetIdx: self.delegationTargetIdx)
345    }
346    
347    // Internal function to withdraw unstaked tokens
348    access(contract) fun withdrawUnstaked(amount: UFix64) {
349        assert(self.stakingManagerCap != nil, message: "StakingManager capability not set up")
350        
351        let stakerRef = self.stakingManagerCap!.borrow() 
352            ?? panic("Could not borrow StakingManager Staker capability")
353        
354        // Withdraw unstaked tokens to StakingManager vault
355        stakerRef.WithdrawUnstaked(amount: amount, delegationTargetIdx: self.delegationTargetIdx)
356        
357        // Get StakingManager's vault with withdraw capability
358        let stakingManagerAccount = getAccount(self.stakingManagerAddress)
359        let stakingVaultCap = stakingManagerAccount.capabilities
360            .get<auth(FungibleToken.Withdraw) &FlowToken.Vault>(/public/stakingManagerVault)
361        let stakingVault = stakingVaultCap.borrow() 
362            ?? panic("Could not borrow StakingManager vault with withdraw capability")
363        
364        // Withdraw from StakingManager vault to PrizeVault
365        let unstaked <- stakingVault.withdraw(amount: amount)
366        self.vault.deposit(from: <- unstaked)
367    }
368    
369    // Commit phase: Request randomness and create receipt
370    access(contract) fun commitPrize(amount: UFix64): @PrizeDrawReceipt {
371        // Ensure there are users with deposits
372        assert(self.userDeposits.length > 0, message: "No users with deposits")
373        
374        // Ensure vault has enough balance for the prize
375        assert(self.vault.balance >= amount, message: "Insufficient balance in vault for prize")
376        
377        // Request randomness from RandomConsumer
378        let request <- self.consumer.requestRandomness()
379        
380        // Create receipt with prize amount and random request
381        let receipt <- create PrizeDrawReceipt(
382            prizeAmount: amount,
383            request: <- request
384        )
385        
386        let commitBlock = receipt.getRequestBlock()!
387        
388        emit PrizeDrawCommitted(prizeAmount: amount, commitBlock: commitBlock, receiptID: receipt.uuid)
389        
390        return <- receipt
391    }
392    
393    // Reveal phase: Use receipt to get random number and award prize
394    access(contract) fun revealPrize(receipt: @PrizeDrawReceipt) {
395        let prizeAmount = receipt.prizeAmount
396        let commitBlock = receipt.getRequestBlock()!
397        let receiptID = receipt.uuid
398        
399        // Get all depositor addresses
400        let depositors: [Address] = self.userDeposits.keys
401        
402        // Fulfill the random request to get the random value
403        let request <- receipt.popRequest()
404        let randomNumber = self.consumer.fulfillRandomRequest(<-request)
405        
406        // Destroy the receipt
407        destroy receipt
408        
409        // Select random winner index
410        let winnerIndex = randomNumber % UInt64(depositors.length)
411        let winnerAddress = depositors[winnerIndex]
412        
413        // Award the prize by increasing winner's deposit balance
414        let currentBalance = self.userDeposits[winnerAddress]!
415        self.userDeposits[winnerAddress] = currentBalance + prizeAmount
416        
417        // Update prize tracking
418        self.prizeRound = self.prizeRound + 1
419        self.totalPrizesDistributed = self.totalPrizesDistributed + prizeAmount
420        self.prizeHistory[self.prizeRound] = winnerAddress
421        
422        // Emit prize awarded event
423        emit PrizeAwarded(
424            winner: winnerAddress, 
425            amount: prizeAmount, 
426            round: self.prizeRound, 
427            commitBlock: commitBlock, 
428            receiptID: receiptID
429        )
430    }
431    
432    // Get a user's total deposited amount
433    access(all) fun getUserDeposit(address: Address): UFix64 {
434        return self.userDeposits[address] ?? 0.0
435    }
436    
437    // Get a user's pending withdrawal amount
438    access(all) fun getUserPendingWithdrawal(address: Address): UFix64 {
439        return self.pendingWithdrawals[address] ?? 0.0
440    }
441    
442    // Get total deposited in the vault
443    access(all) fun getTotalDeposited(): UFix64 {
444        return self.totalDeposited
445    }
446    
447    // Get total staked amount
448    access(all) fun getTotalStaked(): UFix64 {
449        return self.totalStaked
450    }
451    
452    // Get total rewards harvested
453    access(all) fun getTotalRewardsHarvested(): UFix64 {
454        return self.totalRewardsHarvested
455    }
456    
457    // Get total prizes distributed
458    access(all) fun getTotalPrizesDistributed(): UFix64 {
459        return self.totalPrizesDistributed
460    }
461    
462    // Get current prize round number
463    access(all) fun getCurrentPrizeRound(): UInt64 {
464        return self.prizeRound
465    }
466    
467    // Get prize winner for a specific round
468    access(all) fun getPrizeWinner(round: UInt64): Address? {
469        return self.prizeHistory[round]
470    }
471    
472    // Get the vault balance (includes unharvested principal and harvested rewards)
473    access(all) fun getVaultBalance(): UFix64 {
474        return self.vault.balance
475    }
476    
477    // Query available rewards from StakingManager's StakingCollection
478    // NOTE: This is a placeholder that returns 0.0
479    // Admin should track rewards through StakingManager events or off-chain monitoring
480    // The actual rewards can be queried directly from FlowStakingCollection if needed
481    access(all) fun getAvailableRewards(): UFix64 {
482        // TODO: Implement proper rewards querying based on FlowStakingCollection API
483        // For now, admin should monitor rewards through events and off-chain tools
484        return 0.0
485    }
486
487    init(
488        stakingManagerAddress: Address,
489        delegationTargetIdx: UInt
490    ) {
491        // Initialize paths
492        self.DepositReceiverStoragePath = /storage/PrizeVaultDepositReceiver
493        self.DepositReceiverPublicPath = /public/PrizeVaultDepositReceiver
494        self.VaultStoragePath = /storage/PrizeVaultMainVault
495        self.AdminStoragePath = /storage/PrizeVaultAdmin
496        self.PrizeDrawReceiptStoragePath = /storage/PrizeVaultDrawReceipt
497        
498        // Initialize state
499        self.totalDeposited = 0.0
500        self.totalStaked = 0.0
501        self.totalRewardsHarvested = 0.0
502        self.totalPrizesDistributed = 0.0
503        self.prizeRound = 0
504        self.userDeposits = {}
505        self.pendingWithdrawals = {}
506        self.prizeHistory = {}
507        self.delegationTargetIdx = delegationTargetIdx
508        self.stakingManagerAddress = stakingManagerAddress
509        
510        // Create the main vault to hold all deposits
511        self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
512        
513        // Initialize RandomConsumer for commit-reveal randomness
514        self.consumer <- RandomConsumer.createConsumer()
515        
516        // Try to claim the StakingManager Staker capability from inbox (optional)
517        self.stakingManagerCap = self.account.inbox.claim<auth(StakingManager.StakingOperator) &StakingManager.Staker>(
518            "StakingManagerOperatorCap",
519            provider: stakingManagerAddress
520        )
521        
522        // Create and save Admin resource
523        self.account.storage.save(<- create Admin(), to: self.AdminStoragePath)
524    }
525}