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