Smart Contract
PrizeVaultV2
A.262cf58c0b9fbcff.PrizeVaultV2
1/*
2PrizeVault V2 - 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 PrizeVaultV2 {
26
27 // Events
28 access(all) event Deposited(address: Address, amount: UFix64)
29 access(all) event WithdrawalRequested(address: Address, amount: UFix64)
30 access(all) event Withdrawn(address: Address, amount: UFix64)
31 access(all) event Staked(amount: UFix64)
32 access(all) event PrizeDrawCommitted(prizeAmount: UFix64, commitBlock: UInt64, receiptID: UInt64)
33 access(all) event PrizeAwarded(winner: Address, amount: UFix64, round: UInt64, commitBlock: UInt64, receiptID: UInt64)
34 access(all) event DepositReceiverCreated(address: Address)
35
36 // Paths
37 access(all) let DepositReceiverStoragePath: StoragePath
38 access(all) let DepositReceiverPublicPath: PublicPath
39 access(all) let VaultStoragePath: StoragePath
40 access(all) let AdminStoragePath: StoragePath
41 access(all) let PrizeDrawReceiptStoragePath: StoragePath
42
43 // State
44 access(all) var totalDeposited: UFix64
45 access(all) var totalStaked: UFix64
46 access(all) var totalRewardsHarvested: UFix64
47 access(all) var totalPrizesDistributed: UFix64
48 access(all) var prizeRound: UInt64
49
50 // Mappings
51 access(self) let userDeposits: {Address: UFix64}
52 access(self) let pendingWithdrawals: {Address: UFix64}
53 access(self) let prizeHistory: {UInt64: Address}
54
55 // Main vault to hold all deposited FLOW tokens
56 access(self) let vault: @FlowToken.Vault
57
58 // EVM staking pool contract address (Ankr on Flow EVM)
59 access(self) let evmStakingPoolAddress: EVM.EVMAddress
60
61 // CadenceOwnedAccount for EVM interactions
62 access(self) let coa: @EVM.CadenceOwnedAccount
63
64 // RandomConsumer for commit-reveal randomness
65 access(self) let consumer: @RandomConsumer.Consumer
66
67 // Receipt resource for prize draw commit-reveal
68 access(all) resource PrizeDrawReceipt {
69 access(all) let prizeAmount: UFix64
70 access(self) var request: @RandomConsumer.Request?
71
72 init(prizeAmount: UFix64, request: @RandomConsumer.Request) {
73 self.prizeAmount = prizeAmount
74 self.request <- request
75 }
76
77 // Get the block height at which randomness was committed
78 access(all) view fun getRequestBlock(): UInt64? {
79 return self.request?.block
80 }
81
82 // Pop the request for fulfillment (can only be called once)
83 access(contract) fun popRequest(): @RandomConsumer.Request {
84 let request <- self.request <- nil
85 return <- request!
86 }
87 }
88
89 // Admin resource to manage contract configuration
90 access(all) resource Admin {
91 // Commit phase: Start a prize draw (returns receipt to be used in reveal)
92 access(all) fun commitPrizeDraw(prizeAmount: UFix64): @PrizeDrawReceipt {
93 return <- PrizeVaultV2.commitPrize(amount: prizeAmount)
94 }
95
96 // Reveal phase: Complete the prize draw using the receipt
97 access(all) fun revealPrizeDraw(receipt: @PrizeDrawReceipt) {
98 PrizeVaultV2.revealPrize(receipt: <- receipt)
99 }
100 }
101
102 // Public interface that users can expose
103 access(all) resource interface DepositReceiverPublic {
104 access(all) fun deposit(from: @{FungibleToken.Vault})
105 access(all) fun requestWithdrawal(amount: UFix64)
106 access(all) fun completeWithdrawal(): @{FungibleToken.Vault}
107 access(all) fun getBalance(): UFix64
108 access(all) fun getPendingWithdrawal(): UFix64
109 }
110
111 // DepositReceiver resource that users create to interact with the vault
112 access(all) resource DepositReceiver: DepositReceiverPublic {
113 // Track user's balance
114 access(self) var balance: UFix64
115
116 init() {
117 self.balance = 0.0
118 }
119
120 // Deposit FLOW tokens into the vault
121 access(all) fun deposit(from: @{FungibleToken.Vault}) {
122 // Get the owner's address
123 let ownerAddress = self.owner?.address ?? panic("No owner address")
124
125 // Cast to FlowToken.Vault
126 let flowVault <- from as! @FlowToken.Vault
127 let amount = flowVault.balance
128
129 // Deposit into main vault
130 PrizeVaultV2.vault.deposit(from: <- flowVault)
131
132 // Stake the tokens via EVM
133 PrizeVaultV2.stakeTokens(amount: amount)
134
135 // Update user's balance
136 self.balance = self.balance + amount
137
138 // Update contract state
139 PrizeVaultV2.userDeposits[ownerAddress] = self.balance
140 PrizeVaultV2.totalDeposited = PrizeVaultV2.totalDeposited + amount
141
142 emit Deposited(address: ownerAddress, amount: amount)
143 emit DepositReceiverCreated(address: ownerAddress)
144 }
145
146 // Request withdrawal: Initiates unstaking if needed
147 access(all) fun requestWithdrawal(amount: UFix64) {
148 let ownerAddress = self.owner?.address ?? panic("No owner address")
149
150 assert(self.balance >= amount, message: "Insufficient balance to withdraw")
151
152 let existingPending = PrizeVaultV2.pendingWithdrawals[ownerAddress] ?? 0.0
153 assert(existingPending == 0.0, message: "You already have a pending withdrawal. Complete it first.")
154
155 // Update user's balance (funds now in pending state)
156 self.balance = self.balance - amount
157
158 // Update contract state
159 PrizeVaultV2.userDeposits[ownerAddress] = self.balance
160 PrizeVaultV2.totalDeposited = PrizeVaultV2.totalDeposited - amount
161 PrizeVaultV2.pendingWithdrawals[ownerAddress] = amount
162
163 emit WithdrawalRequested(address: ownerAddress, amount: amount)
164 }
165
166 // Complete withdrawal: After any unstaking period, withdraw the funds
167 access(all) fun completeWithdrawal(): @{FungibleToken.Vault} {
168 let ownerAddress = self.owner?.address ?? panic("No owner address")
169
170 let pendingAmount = PrizeVaultV2.pendingWithdrawals[ownerAddress]
171 ?? panic("No pending withdrawal found")
172
173 assert(pendingAmount > 0.0, message: "No pending withdrawal")
174
175 // Withdraw from vault
176 let withdrawn <- PrizeVaultV2.withdrawFromVault(amount: pendingAmount)
177
178 // Clear pending withdrawal
179 PrizeVaultV2.pendingWithdrawals[ownerAddress] = 0.0
180
181 emit Withdrawn(address: ownerAddress, amount: pendingAmount)
182
183 return <- withdrawn
184 }
185
186 access(all) fun getBalance(): UFix64 {
187 return self.balance
188 }
189
190 access(all) fun getPendingWithdrawal(): UFix64 {
191 let ownerAddress = self.owner?.address ?? panic("No owner address")
192 return PrizeVaultV2.pendingWithdrawals[ownerAddress] ?? 0.0
193 }
194 }
195
196 // Create a new DepositReceiver for users
197 access(all) fun createDepositReceiver(): @DepositReceiver {
198 return <- create DepositReceiver()
199 }
200
201 // Internal function to withdraw from vault
202 access(contract) fun withdrawFromVault(amount: UFix64): @{FungibleToken.Vault} {
203 let withdrawn <- self.vault.withdraw(amount: amount)
204 return <- withdrawn
205 }
206
207 // Internal function to stake tokens via EVM (Ankr)
208 access(contract) fun stakeTokens(amount: UFix64) {
209 // Withdraw FLOW from vault as FlowToken.Vault
210 let tokensToStake <- self.vault.withdraw(amount: amount) as! @FlowToken.Vault
211
212 // Deposit FLOW into COA - COA needs balance to send value in EVM calls
213 let coaRef = (&self.coa as auth(EVM.Call) &EVM.CadenceOwnedAccount)!
214 coaRef.deposit(from: <- tokensToStake)
215
216 // Prepare EVM call to stakeCerts() - function signature: 0xac76d450 (from working MetaMask tx)
217 let stakeCertsCalldata: [UInt8] = [0xac, 0x76, 0xd4, 0x50]
218
219 // Create balance value to send with call
220 let value = EVM.Balance(attoflow: 0)
221 value.setFLOW(flow: amount)
222
223 // Call stakeCerts on EVM staking pool
224 // MetaMask uses ~155k gas, so we set 300k to be safe
225 let callResult = coaRef.call(
226 to: self.evmStakingPoolAddress,
227 data: stakeCertsCalldata,
228 gasLimit: 300000,
229 value: value
230 )
231
232 assert(
233 callResult.status == EVM.Status.successful,
234 message: "EVM staking call failed: ".concat(callResult.errorMessage)
235 )
236
237 // Update total staked
238 self.totalStaked = self.totalStaked + amount
239
240 emit Staked(amount: amount)
241 }
242
243 // Commit phase: Lock in the prize amount and request randomness
244 access(contract) fun commitPrize(amount: UFix64): @PrizeDrawReceipt {
245 assert(self.userDeposits.length > 0, message: "No users with deposits")
246 assert(self.vault.balance >= amount, message: "Insufficient balance in vault for prize")
247
248 // Request randomness from RandomConsumer
249 let request <- self.consumer.requestRandomness()
250
251 // Create receipt with prize amount and random request
252 let receipt <- create PrizeDrawReceipt(
253 prizeAmount: amount,
254 request: <- request
255 )
256
257 let commitBlock = receipt.getRequestBlock()!
258
259 emit PrizeDrawCommitted(prizeAmount: amount, commitBlock: commitBlock, receiptID: receipt.uuid)
260
261 return <- receipt
262 }
263
264 // Reveal phase: Use receipt to get random number and award prize
265 access(contract) fun revealPrize(receipt: @PrizeDrawReceipt) {
266 let prizeAmount = receipt.prizeAmount
267 let commitBlock = receipt.getRequestBlock()!
268 let receiptID = receipt.uuid
269
270 // Get all depositor addresses
271 let depositors: [Address] = self.userDeposits.keys
272
273 // Fulfill the random request to get the random value
274 let request <- receipt.popRequest()
275 let randomNumber = self.consumer.fulfillRandomRequest(<-request)
276
277 // Destroy the receipt
278 destroy receipt
279
280 // Select random winner index
281 let winnerIndex = randomNumber % UInt64(depositors.length)
282 let winnerAddress = depositors[winnerIndex]
283
284 // Award the prize by increasing winner's deposit balance
285 let currentBalance = self.userDeposits[winnerAddress]!
286 self.userDeposits[winnerAddress] = currentBalance + prizeAmount
287
288 // Update prize tracking
289 self.prizeRound = self.prizeRound + 1
290 self.totalPrizesDistributed = self.totalPrizesDistributed + prizeAmount
291 self.prizeHistory[self.prizeRound] = winnerAddress
292
293 // Emit prize awarded event
294 emit PrizeAwarded(
295 winner: winnerAddress,
296 amount: prizeAmount,
297 round: self.prizeRound,
298 commitBlock: commitBlock,
299 receiptID: receiptID
300 )
301 }
302
303 // Public getters
304 access(all) fun getTotalDeposited(): UFix64 {
305 return self.totalDeposited
306 }
307
308 access(all) fun getTotalStaked(): UFix64 {
309 return self.totalStaked
310 }
311
312 access(all) fun getTotalRewardsHarvested(): UFix64 {
313 return self.totalRewardsHarvested
314 }
315
316 access(all) fun getTotalPrizesDistributed(): UFix64 {
317 return self.totalPrizesDistributed
318 }
319
320 access(all) fun getCurrentPrizeRound(): UInt64 {
321 return self.prizeRound
322 }
323
324 access(all) fun getVaultBalance(): UFix64 {
325 return self.vault.balance
326 }
327
328 access(all) fun getUserDeposit(address: Address): UFix64 {
329 return self.userDeposits[address] ?? 0.0
330 }
331
332 access(all) fun getUserPendingWithdrawal(address: Address): UFix64 {
333 return self.pendingWithdrawals[address] ?? 0.0
334 }
335
336 access(all) fun getPrizeWinner(round: UInt64): Address? {
337 return self.prizeHistory[round]
338 }
339
340 // Get COA EVM balance in FLOW
341 access(all) fun getCOABalance(): UFix64 {
342 let coaRef = &self.coa as &EVM.CadenceOwnedAccount
343 let balance = coaRef.balance()
344 // Use the built-in conversion method
345 return balance.inFLOW()
346 }
347
348 // Get COA EVM address as hex string
349 access(all) fun getCOAAddress(): String {
350 let coaRef = &self.coa as &EVM.CadenceOwnedAccount
351 return coaRef.address().toString()
352 }
353
354 // Get the Ankr staking pool EVM address
355 access(all) fun getStakingPoolAddress(): String {
356 return self.evmStakingPoolAddress.toString()
357 }
358
359 init(evmStakingPoolAddressHex: String) {
360 // Initialize paths
361 self.DepositReceiverStoragePath = /storage/PrizeVaultV2DepositReceiver
362 self.DepositReceiverPublicPath = /public/PrizeVaultV2DepositReceiver
363 self.VaultStoragePath = /storage/PrizeVaultV2MainVault
364 self.AdminStoragePath = /storage/PrizeVaultV2Admin
365 self.PrizeDrawReceiptStoragePath = /storage/PrizeVaultV2DrawReceipt
366
367 // Initialize state
368 self.totalDeposited = 0.0
369 self.totalStaked = 0.0
370 self.totalRewardsHarvested = 0.0
371 self.totalPrizesDistributed = 0.0
372 self.prizeRound = 0
373 self.userDeposits = {}
374 self.pendingWithdrawals = {}
375 self.prizeHistory = {}
376
377 // Set EVM staking pool address
378 self.evmStakingPoolAddress = EVM.addressFromString(evmStakingPoolAddressHex)
379
380 // Create the main vault to hold all deposits
381 self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault
382
383 // Create COA for EVM interactions
384 self.coa <- EVM.createCadenceOwnedAccount()
385
386 // Initialize RandomConsumer for commit-reveal randomness
387 self.consumer <- RandomConsumer.createConsumer()
388
389 // Create and save Admin resource
390 self.account.storage.save(<- create Admin(), to: self.AdminStoragePath)
391 }
392}
393
394