Smart Contract

PrizeVaultV2

A.262cf58c0b9fbcff.PrizeVaultV2

Valid From

130,249,502

Deployed

5d ago
Feb 21, 2026, 02:11:16 PM UTC

Dependents

1 imports
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