Smart Contract

PrizeVaultModular

A.262cf58c0b9fbcff.PrizeVaultModular

Valid From

133,633,737

Deployed

6d ago
Feb 21, 2026, 07:16:56 PM UTC

Dependents

5 imports
1/*
2PrizeVault - Prize-Linked Savings Protocol
3
4A no-loss lottery system where users deposit tokens to earn both guaranteed savings 
5interest and lottery prizes. Rewards are automatically compounded into deposits.
6
7Architecture:
8- Modular yield sources via DeFi Actions interface
9- Configurable distribution strategies (savings/lottery/treasury split)
10- O(1) gas complexity for interest distribution using accumulator pattern
11- Emergency mode with multi-sig support, configurable withdrawal limits, and auto-recovery
12- NFT prize support with pending claims system
13- Direct funding capabilities for external sponsors
14- Batch event emission for scalability (100k+ users)
15
16Core Components:
17- SavingsDistributor: Manages proportional interest distribution with O(1) gas
18- LotteryDistributor: Prize pool management and winner payouts
19- TreasuryDistributor: Protocol reserves, fee collection, and rounding dust
20- FundingPolicy: Rate limits and caps for direct funding
21
22Security Features:
23- Contract-only authorized pool access (prevents unauthorized manipulation)
24- Dust handling to prevent yield source imbalance
25- Emergency mode with configurable policies per pool
26- Scalable compounding with batch event summaries
27*/
28
29import FungibleToken from 0xf233dcee88fe0abe
30import NonFungibleToken from 0x1d7e57aa55817448
31import RandomConsumer from 0x45caec600164c9e6
32import DeFiActions from 0x92195d814edf9cb0
33import DeFiActionsUtils from 0x92195d814edf9cb0
34import PrizeWinnerTracker from 0x262cf58c0b9fbcff
35import Xorshift128plus from 0x262cf58c0b9fbcff
36
37access(all) contract PrizeVaultModular {
38    
39    access(all) let PoolPositionCollectionStoragePath: StoragePath
40    access(all) let PoolPositionCollectionPublicPath: PublicPath
41    
42    access(all) event PoolCreated(poolID: UInt64, assetType: String, strategy: String)
43    access(all) event Deposited(poolID: UInt64, receiverID: UInt64, amount: UFix64)
44    access(all) event Withdrawn(poolID: UInt64, receiverID: UInt64, amount: UFix64)
45    
46    access(all) event RewardsProcessed(poolID: UInt64, totalAmount: UFix64, savingsAmount: UFix64, lotteryAmount: UFix64)
47    
48    access(all) event SavingsInterestDistributed(poolID: UInt64, amount: UFix64, interestPerShare: UFix64)
49    access(all) event SavingsInterestCompounded(poolID: UInt64, receiverID: UInt64, amount: UFix64)
50    access(all) event SavingsInterestCompoundedBatch(poolID: UInt64, userCount: Int, totalAmount: UFix64, avgAmount: UFix64)
51    access(all) event SavingsRoundingDustToTreasury(poolID: UInt64, amount: UFix64)
52    
53    access(all) event PrizeDrawCommitted(poolID: UInt64, prizeAmount: UFix64, commitBlock: UInt64)
54    access(all) event PrizesAwarded(poolID: UInt64, winners: [UInt64], amounts: [UFix64], round: UInt64)
55    access(all) event LotteryPrizePoolFunded(poolID: UInt64, amount: UFix64, source: String)
56    
57    access(all) event DistributionStrategyUpdated(poolID: UInt64, oldStrategy: String, newStrategy: String, updatedBy: Address)
58    access(all) event WinnerSelectionStrategyUpdated(poolID: UInt64, oldStrategy: String, newStrategy: String, updatedBy: Address)
59    access(all) event WinnerTrackerUpdated(poolID: UInt64, hasOldTracker: Bool, hasNewTracker: Bool, updatedBy: Address)
60    access(all) event DrawIntervalUpdated(poolID: UInt64, oldInterval: UFix64, newInterval: UFix64, updatedBy: Address)
61    access(all) event MinimumDepositUpdated(poolID: UInt64, oldMinimum: UFix64, newMinimum: UFix64, updatedBy: Address)
62    access(all) event PoolCreatedByAdmin(poolID: UInt64, assetType: String, strategy: String, createdBy: Address)
63    
64    access(all) event PoolPaused(poolID: UInt64, pausedBy: Address, reason: String)
65    access(all) event PoolUnpaused(poolID: UInt64, unpausedBy: Address)
66    access(all) event TreasuryFunded(poolID: UInt64, amount: UFix64, source: String)
67    access(all) event TreasuryWithdrawn(poolID: UInt64, withdrawnBy: Address, amount: UFix64, purpose: String, remainingBalance: UFix64)
68    
69    access(all) event BonusLotteryWeightSet(poolID: UInt64, receiverID: UInt64, bonusWeight: UFix64, reason: String, setBy: Address, timestamp: UFix64)
70    access(all) event BonusLotteryWeightAdded(poolID: UInt64, receiverID: UInt64, additionalWeight: UFix64, newTotalBonus: UFix64, reason: String, addedBy: Address, timestamp: UFix64)
71    access(all) event BonusLotteryWeightRemoved(poolID: UInt64, receiverID: UInt64, previousBonus: UFix64, removedBy: Address, timestamp: UFix64)
72    
73    access(all) event NFTPrizeDeposited(poolID: UInt64, nftID: UInt64, nftType: String, depositedBy: Address)
74    access(all) event NFTPrizeAwarded(poolID: UInt64, receiverID: UInt64, nftID: UInt64, nftType: String, round: UInt64)
75    access(all) event NFTPrizeStored(poolID: UInt64, receiverID: UInt64, nftID: UInt64, nftType: String, reason: String)
76    access(all) event NFTPrizeClaimed(poolID: UInt64, receiverID: UInt64, nftID: UInt64, nftType: String)
77    access(all) event NFTPrizeWithdrawn(poolID: UInt64, nftID: UInt64, nftType: String, withdrawnBy: Address)
78    
79    access(all) event PoolEmergencyEnabled(poolID: UInt64, reason: String, enabledBy: Address, timestamp: UFix64)
80    access(all) event PoolEmergencyDisabled(poolID: UInt64, disabledBy: Address, timestamp: UFix64)
81    access(all) event PoolPartialModeEnabled(poolID: UInt64, reason: String, setBy: Address, timestamp: UFix64)
82    access(all) event EmergencyModeAutoTriggered(poolID: UInt64, reason: String, healthScore: UFix64, timestamp: UFix64)
83    access(all) event EmergencyModeAutoRecovered(poolID: UInt64, reason: String, healthScore: UFix64?, duration: UFix64?, timestamp: UFix64)
84    access(all) event EmergencyConfigUpdated(poolID: UInt64, updatedBy: Address)
85    access(all) event WithdrawalFailure(poolID: UInt64, receiverID: UInt64, amount: UFix64, consecutiveFailures: Int, yieldAvailable: UFix64)
86    
87    access(all) event DirectFundingReceived(poolID: UInt64, destination: UInt8, destinationName: String, amount: UFix64, sponsor: Address, purpose: String, metadata: {String: String})
88    
89    access(self) var pools: @{UInt64: Pool}
90    access(self) var nextPoolID: UInt64
91    
92    access(all) enum PoolEmergencyState: UInt8 {
93        access(all) case Normal
94        access(all) case Paused
95        access(all) case EmergencyMode
96        access(all) case PartialMode
97    }
98    
99    access(all) enum PoolFundingDestination: UInt8 {
100        access(all) case Savings
101        access(all) case Lottery
102        access(all) case Treasury
103    }
104    
105    access(all) struct BonusWeightRecord {
106        access(all) let bonusWeight: UFix64
107        access(all) let reason: String
108        access(all) let addedAt: UFix64
109        access(all) let addedBy: Address
110        
111        access(contract) init(bonusWeight: UFix64, reason: String, addedBy: Address) {
112            self.bonusWeight = bonusWeight
113            self.reason = reason
114            self.addedAt = getCurrentBlock().timestamp
115            self.addedBy = addedBy
116        }
117    }
118    
119    /// Emergency configuration for pool safety mechanisms
120    /// Authorization and multi-sig are handled at the Flow account level via Admin resource
121    access(all) struct EmergencyConfig {
122        access(all) let maxEmergencyDuration: UFix64?        // Max time pool can stay in emergency mode
123        access(all) let autoRecoveryEnabled: Bool             // Auto-recover to normal after duration
124        access(all) let minYieldSourceHealth: UFix64          // Health threshold (0.0-1.0)
125        access(all) let maxWithdrawFailures: Int              // Max consecutive withdraw failures before emergency
126        access(all) let partialModeDepositLimit: UFix64?      // Max deposit allowed in partial mode
127        access(all) let minBalanceThreshold: UFix64           // Min balance as % of totalStaked (0.8-1.0, default 0.95)
128        
129        init(
130            maxEmergencyDuration: UFix64?,
131            autoRecoveryEnabled: Bool,
132            minYieldSourceHealth: UFix64,
133            maxWithdrawFailures: Int,
134            partialModeDepositLimit: UFix64?,
135            minBalanceThreshold: UFix64
136        ) {
137            pre {
138                minYieldSourceHealth >= 0.0 && minYieldSourceHealth <= 1.0: "Health must be between 0.0 and 1.0"
139                maxWithdrawFailures > 0: "Must allow at least 1 withdrawal failure"
140                minBalanceThreshold >= 0.8 && minBalanceThreshold <= 1.0: "Balance threshold must be between 0.8 and 1.0"
141            }
142            self.maxEmergencyDuration = maxEmergencyDuration
143            self.autoRecoveryEnabled = autoRecoveryEnabled
144            self.minYieldSourceHealth = minYieldSourceHealth
145            self.maxWithdrawFailures = maxWithdrawFailures
146            self.partialModeDepositLimit = partialModeDepositLimit
147            self.minBalanceThreshold = minBalanceThreshold
148        }
149    }
150    
151    access(all) struct FundingPolicy {
152        access(all) let maxDirectLottery: UFix64?
153        access(all) let maxDirectTreasury: UFix64?
154        access(all) let maxDirectSavings: UFix64?
155        access(all) var totalDirectLottery: UFix64
156        access(all) var totalDirectTreasury: UFix64
157        access(all) var totalDirectSavings: UFix64
158        
159        init(maxDirectLottery: UFix64?, maxDirectTreasury: UFix64?, maxDirectSavings: UFix64?) {
160            self.maxDirectLottery = maxDirectLottery
161            self.maxDirectTreasury = maxDirectTreasury
162            self.maxDirectSavings = maxDirectSavings
163            self.totalDirectLottery = 0.0
164            self.totalDirectTreasury = 0.0
165            self.totalDirectSavings = 0.0
166        }
167        
168        access(contract) fun recordDirectFunding(destination: PoolFundingDestination, amount: UFix64) {
169            switch destination {
170                case PoolFundingDestination.Lottery:
171                    self.totalDirectLottery = self.totalDirectLottery + amount
172                    if self.maxDirectLottery != nil {
173                        assert(self.totalDirectLottery <= self.maxDirectLottery!, message: "Direct lottery funding limit exceeded")
174                    }
175                case PoolFundingDestination.Treasury:
176                    self.totalDirectTreasury = self.totalDirectTreasury + amount
177                    if self.maxDirectTreasury != nil {
178                        assert(self.totalDirectTreasury <= self.maxDirectTreasury!, message: "Direct treasury funding limit exceeded")
179                    }
180                case PoolFundingDestination.Savings:
181                    self.totalDirectSavings = self.totalDirectSavings + amount
182                    if self.maxDirectSavings != nil {
183                        assert(self.totalDirectSavings <= self.maxDirectSavings!, message: "Direct savings funding limit exceeded")
184                    }
185            }
186        }
187    }
188    
189    /// Creates default emergency config with reasonable safety limits
190    /// Authorization is handled by Admin resource ownership (Flow account level)
191    access(all) fun createDefaultEmergencyConfig(): EmergencyConfig {
192        return EmergencyConfig(
193            maxEmergencyDuration: 86400.0,      // 24 hours
194            autoRecoveryEnabled: true,
195            minYieldSourceHealth: 0.5,          // 50% health threshold
196            maxWithdrawFailures: 3,
197            partialModeDepositLimit: 100.0,
198            minBalanceThreshold: 0.95           // 95% of totalStaked
199        )
200    }
201    
202    access(all) fun createDefaultFundingPolicy(): FundingPolicy {
203        return FundingPolicy(
204            maxDirectLottery: nil,
205            maxDirectTreasury: nil,
206            maxDirectSavings: nil
207        )
208    }
209    
210    access(all) resource Admin {
211        access(contract) init() {}
212        
213        access(all) fun updatePoolDistributionStrategy(
214            poolID: UInt64,
215            newStrategy: {DistributionStrategy},
216            updatedBy: Address
217        ) {
218            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
219                ?? panic("Pool does not exist")
220            
221            let oldStrategyName = poolRef.getDistributionStrategyName()
222            poolRef.setDistributionStrategy(strategy: newStrategy)
223            let newStrategyName = newStrategy.getStrategyName()
224            
225            emit DistributionStrategyUpdated(
226                poolID: poolID,
227                oldStrategy: oldStrategyName,
228                newStrategy: newStrategyName,
229                updatedBy: updatedBy
230            )
231        }
232        
233        access(all) fun updatePoolWinnerSelectionStrategy(
234            poolID: UInt64,
235            newStrategy: {WinnerSelectionStrategy},
236            updatedBy: Address
237        ) {
238            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
239                ?? panic("Pool does not exist")
240            
241            let oldStrategyName = poolRef.getWinnerSelectionStrategyName()
242            poolRef.setWinnerSelectionStrategy(strategy: newStrategy)
243            let newStrategyName = newStrategy.getStrategyName()
244            
245            emit WinnerSelectionStrategyUpdated(
246                poolID: poolID,
247                oldStrategy: oldStrategyName,
248                newStrategy: newStrategyName,
249                updatedBy: updatedBy
250            )
251        }
252        
253        access(all) fun updatePoolWinnerTracker(
254            poolID: UInt64,
255            newTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?,
256            updatedBy: Address
257        ) {
258            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
259                ?? panic("Pool does not exist")
260            
261            let hasOldTracker = poolRef.hasWinnerTracker()
262            poolRef.setWinnerTrackerCap(cap: newTrackerCap)
263            let hasNewTracker = newTrackerCap != nil
264            
265            emit WinnerTrackerUpdated(
266                poolID: poolID,
267                hasOldTracker: hasOldTracker,
268                hasNewTracker: hasNewTracker,
269                updatedBy: updatedBy
270            )
271        }
272        
273        access(all) fun updatePoolDrawInterval(
274            poolID: UInt64,
275            newInterval: UFix64,
276            updatedBy: Address
277        ) {
278            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
279                ?? panic("Pool does not exist")
280            
281            let oldInterval = poolRef.getConfig().drawIntervalSeconds
282            poolRef.setDrawIntervalSeconds(interval: newInterval)
283            
284            emit DrawIntervalUpdated(
285                poolID: poolID,
286                oldInterval: oldInterval,
287                newInterval: newInterval,
288                updatedBy: updatedBy
289            )
290        }
291        
292        access(all) fun updatePoolMinimumDeposit(
293            poolID: UInt64,
294            newMinimum: UFix64,
295            updatedBy: Address
296        ) {
297            pre {
298                newMinimum >= 0.0: "Minimum deposit cannot be negative"
299            }
300            
301            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
302                ?? panic("Pool does not exist")
303            
304            let oldMinimum = poolRef.getConfig().minimumDeposit
305            poolRef.setMinimumDeposit(minimum: newMinimum)
306            
307            emit MinimumDepositUpdated(
308                poolID: poolID,
309                oldMinimum: oldMinimum,
310                newMinimum: newMinimum,
311                updatedBy: updatedBy
312            )
313        }
314        
315        /// Enable emergency mode for a pool
316        /// Authorization is enforced by Admin resource ownership (Flow account multi-sig)
317        access(all) fun enableEmergencyMode(poolID: UInt64, reason: String, enabledBy: Address) {
318            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID) ?? panic("Pool does not exist")
319            poolRef.setEmergencyMode(reason: reason)
320            emit PoolEmergencyEnabled(poolID: poolID, reason: reason, enabledBy: enabledBy, timestamp: getCurrentBlock().timestamp)
321        }
322        
323        /// Disable emergency mode for a pool
324        /// Authorization is enforced by Admin resource ownership (Flow account multi-sig)
325        access(all) fun disableEmergencyMode(poolID: UInt64, disabledBy: Address) {
326            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID) ?? panic("Pool does not exist")
327            poolRef.clearEmergencyMode()
328            emit PoolEmergencyDisabled(poolID: poolID, disabledBy: disabledBy, timestamp: getCurrentBlock().timestamp)
329        }
330        
331        /// Set pool to partial emergency mode (deposits limited, withdrawals work)
332        /// Authorization is enforced by Admin resource ownership (Flow account multi-sig)
333        access(all) fun setEmergencyPartialMode(poolID: UInt64, reason: String, setBy: Address) {
334            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID) ?? panic("Pool does not exist")
335            poolRef.setPartialMode(reason: reason)
336            emit PoolPartialModeEnabled(poolID: poolID, reason: reason, setBy: setBy, timestamp: getCurrentBlock().timestamp)
337        }
338        
339        access(all) fun updateEmergencyConfig(poolID: UInt64, newConfig: EmergencyConfig, updatedBy: Address) {
340            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID) ?? panic("Pool does not exist")
341            poolRef.setEmergencyConfig(config: newConfig)
342            emit EmergencyConfigUpdated(poolID: poolID, updatedBy: updatedBy)
343        }
344        
345        access(all) fun fundPoolDirect(
346            poolID: UInt64,
347            destination: PoolFundingDestination,
348            from: @{FungibleToken.Vault},
349            sponsor: Address,
350            purpose: String,
351            metadata: {String: String}?
352        ) {
353            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID) ?? panic("Pool does not exist")
354            let amount = from.balance
355            poolRef.fundDirectInternal(destination: destination, from: <- from, sponsor: sponsor, purpose: purpose, metadata: metadata ?? {})
356            
357            emit DirectFundingReceived(
358                poolID: poolID,
359                destination: destination.rawValue,
360                destinationName: self.getDestinationName(destination),
361                amount: amount,
362                sponsor: sponsor,
363                purpose: purpose,
364                metadata: metadata ?? {}
365            )
366        }
367        
368        access(self) fun getDestinationName(_ destination: PoolFundingDestination): String {
369            switch destination {
370                case PoolFundingDestination.Savings: return "Savings"
371                case PoolFundingDestination.Lottery: return "Lottery"
372                case PoolFundingDestination.Treasury: return "Treasury"
373                default: return "Unknown"
374            }
375        }
376        
377        // Yield connector immutable per pool for security - create new pool for different yield protocol
378        access(all) fun createPool(
379            config: PoolConfig,
380            emergencyConfig: EmergencyConfig?,
381            fundingPolicy: FundingPolicy?,
382            createdBy: Address
383        ): UInt64 {
384            // Use provided configs or create defaults
385            let finalEmergencyConfig = emergencyConfig 
386                ?? PrizeVaultModular.createDefaultEmergencyConfig()
387            let finalFundingPolicy = fundingPolicy 
388                ?? PrizeVaultModular.createDefaultFundingPolicy()
389            
390            let poolID = PrizeVaultModular.createPool(
391                config: config,
392                emergencyConfig: finalEmergencyConfig,
393                fundingPolicy: finalFundingPolicy
394            )
395            
396            emit PoolCreatedByAdmin(
397                poolID: poolID,
398                assetType: config.assetType.identifier,
399                strategy: config.distributionStrategy.getStrategyName(),
400                createdBy: createdBy
401            )
402            
403            return poolID
404        }
405        
406        access(all) fun processPoolRewards(poolID: UInt64) {
407            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
408                ?? panic("Pool does not exist")
409            
410            poolRef.processRewards()
411        }
412        
413        access(all) fun setPoolState(poolID: UInt64, state: PoolEmergencyState, reason: String?, setBy: Address) {
414            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
415                ?? panic("Pool does not exist")
416            
417            poolRef.setState(state: state, reason: reason)
418            
419            switch state {
420                case PoolEmergencyState.Normal:
421                    emit PoolUnpaused(poolID: poolID, unpausedBy: setBy)
422                case PoolEmergencyState.Paused:
423                    emit PoolPaused(poolID: poolID, pausedBy: setBy, reason: reason ?? "Manual pause")
424                case PoolEmergencyState.EmergencyMode:
425                    emit PoolEmergencyEnabled(poolID: poolID, reason: reason ?? "Emergency", enabledBy: setBy, timestamp: getCurrentBlock().timestamp)
426                case PoolEmergencyState.PartialMode:
427                    emit PoolPartialModeEnabled(poolID: poolID, reason: reason ?? "Partial mode", setBy: setBy, timestamp: getCurrentBlock().timestamp)
428            }
429        }
430        
431        // All treasury withdrawals recorded on-chain for transparency
432        access(all) fun withdrawPoolTreasury(
433            poolID: UInt64,
434            amount: UFix64,
435            purpose: String,
436            withdrawnBy: Address
437        ): @{FungibleToken.Vault} {
438            pre {
439                purpose.length > 0: "Purpose must be specified for treasury withdrawal"
440            }
441            
442            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
443                ?? panic("Pool does not exist")
444            
445            let treasuryVault <- poolRef.withdrawTreasury(
446                amount: amount,
447                withdrawnBy: withdrawnBy,
448                purpose: purpose
449            )
450            
451            emit TreasuryWithdrawn(
452                poolID: poolID,
453                withdrawnBy: withdrawnBy,
454                amount: amount,
455                purpose: purpose,
456                remainingBalance: poolRef.getTreasuryBalance()
457            )
458            
459            return <- treasuryVault
460        }
461        
462        // Bonus lottery weight management
463        access(all) fun setBonusLotteryWeight(
464            poolID: UInt64,
465            receiverID: UInt64,
466            bonusWeight: UFix64,
467            reason: String,
468            setBy: Address
469        ) {
470            pre {
471                bonusWeight >= 0.0: "Bonus weight cannot be negative"
472            }
473            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
474                ?? panic("Pool does not exist")
475            
476            poolRef.setBonusWeight(receiverID: receiverID, bonusWeight: bonusWeight, reason: reason, setBy: setBy)
477        }
478        
479        access(all) fun addBonusLotteryWeight(
480            poolID: UInt64,
481            receiverID: UInt64,
482            additionalWeight: UFix64,
483            reason: String,
484            addedBy: Address
485        ) {
486            pre {
487                additionalWeight > 0.0: "Additional weight must be positive"
488            }
489            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
490                ?? panic("Pool does not exist")
491            
492            poolRef.addBonusWeight(receiverID: receiverID, additionalWeight: additionalWeight, reason: reason, addedBy: addedBy)
493        }
494        
495        access(all) fun removeBonusLotteryWeight(
496            poolID: UInt64,
497            receiverID: UInt64,
498            removedBy: Address
499        ) {
500            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
501                ?? panic("Pool does not exist")
502            
503            poolRef.removeBonusWeight(receiverID: receiverID, removedBy: removedBy)
504        }
505        
506        // NFT Prize Management
507        access(all) fun depositNFTPrize(
508            poolID: UInt64,
509            nft: @{NonFungibleToken.NFT},
510            depositedBy: Address
511        ) {
512            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
513                ?? panic("Pool does not exist")
514            
515            let nftID = nft.uuid
516            let nftType = nft.getType().identifier
517            
518            poolRef.depositNFTPrize(nft: <- nft)
519            
520            emit NFTPrizeDeposited(
521                poolID: poolID,
522                nftID: nftID,
523                nftType: nftType,
524                depositedBy: depositedBy
525            )
526        }
527        
528        access(all) fun withdrawNFTPrize(
529            poolID: UInt64,
530            nftID: UInt64,
531            withdrawnBy: Address
532        ): @{NonFungibleToken.NFT} {
533            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
534                ?? panic("Pool does not exist")
535            
536            let nft <- poolRef.withdrawNFTPrize(nftID: nftID)
537            let nftType = nft.getType().identifier
538            
539            emit NFTPrizeWithdrawn(
540                poolID: poolID,
541                nftID: nftID,
542                nftType: nftType,
543                withdrawnBy: withdrawnBy
544            )
545            
546            return <- nft
547        }
548    }
549    
550    access(all) let AdminStoragePath: StoragePath
551    access(all) let AdminPublicPath: PublicPath
552    
553    access(all) struct DistributionPlan {
554        access(all) let savingsAmount: UFix64
555        access(all) let lotteryAmount: UFix64
556        access(all) let treasuryAmount: UFix64
557        
558        init(savings: UFix64, lottery: UFix64, treasury: UFix64) {
559            self.savingsAmount = savings
560            self.lotteryAmount = lottery
561            self.treasuryAmount = treasury
562        }
563    }
564    
565    access(all) struct interface DistributionStrategy {
566        access(all) fun calculateDistribution(totalAmount: UFix64): DistributionPlan
567        access(all) fun getStrategyName(): String
568    }
569    
570    access(all) struct FixedPercentageStrategy: DistributionStrategy {
571        access(all) let savingsPercent: UFix64
572        access(all) let lotteryPercent: UFix64
573        access(all) let treasuryPercent: UFix64
574        
575        init(savings: UFix64, lottery: UFix64, treasury: UFix64) {
576            pre {
577                savings + lottery + treasury == 1.0: "Percentages must sum to 1.0"
578                savings >= 0.0 && lottery >= 0.0 && treasury >= 0.0: "Must be non-negative"
579            }
580            self.savingsPercent = savings
581            self.lotteryPercent = lottery
582            self.treasuryPercent = treasury
583        }
584        
585        access(all) fun calculateDistribution(totalAmount: UFix64): DistributionPlan {
586            return DistributionPlan(
587                savings: totalAmount * self.savingsPercent,
588                lottery: totalAmount * self.lotteryPercent,
589                treasury: totalAmount * self.treasuryPercent
590            )
591        }
592        
593        access(all) fun getStrategyName(): String {
594            return "Fixed: "
595                .concat(self.savingsPercent.toString())
596                .concat(" savings, ")
597                .concat(self.lotteryPercent.toString())
598                .concat(" lottery")
599        }
600    }
601    
602    // Savings Distributor - O(1) time-weighted distribution
603    access(all) resource SavingsDistributor {
604        access(self) let PRECISION: UFix64
605        access(self) var accumulatedInterestPerShare: UFix64
606        access(self) let userClaimedAmount: {UInt64: UFix64}
607        access(self) var interestVault: @{FungibleToken.Vault}
608        access(all) var totalDistributed: UFix64
609        
610        init(vaultType: Type) {
611            // PRECISION = 100 enables billion-scale distributions (up to ~1.84B FLOW per tx)
612            // Provides 2 decimal places of precision (0.01 FLOW minimum)
613            // Max safe amount = UFix64.max / PRECISION ≈ 1.84 billion FLOW
614            self.PRECISION = 100.0
615            self.accumulatedInterestPerShare = 0.0
616            self.userClaimedAmount = {}
617            self.interestVault <- DeFiActionsUtils.getEmptyVault(vaultType)
618            self.totalDistributed = 0.0
619        }
620        
621        /// Distribute interest and reinvest into yield source to continue generating yield
622        access(contract) fun distributeInterestAndReinvest(
623            vault: @{FungibleToken.Vault}, 
624            totalDeposited: UFix64,
625            yieldSink: &{DeFiActions.Sink}
626        ): UFix64 {
627            let amount = vault.balance
628            
629            if amount == 0.0 || totalDeposited == 0.0 {
630                // Even if zero, reinvest to keep vault clean
631                yieldSink.depositCapacity(from: &vault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
632                destroy vault
633                return 0.0
634            }
635            
636            // Overflow protection: Check multiplication safety
637            let maxSafeAmount = UFix64.max / self.PRECISION
638            assert(amount <= maxSafeAmount, message: "Reward amount too large - would cause overflow")
639            
640            let interestPerShare = (amount * self.PRECISION) / totalDeposited
641            
642            self.accumulatedInterestPerShare = self.accumulatedInterestPerShare + interestPerShare
643            
644            // Reinvest into yield source instead of storing in interestVault
645            yieldSink.depositCapacity(from: &vault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
646            destroy vault
647            self.totalDistributed = self.totalDistributed + amount
648            
649            return interestPerShare
650        }
651        
652        access(contract) fun initializeReceiver(receiverID: UInt64, deposit: UFix64) {
653            // Initialize or re-initialize to prevent claiming past interest
654            self.userClaimedAmount[receiverID] = (deposit * self.accumulatedInterestPerShare) / self.PRECISION
655        }
656        
657        access(all) fun calculatePendingInterest(receiverID: UInt64, deposit: UFix64): UFix64 {
658            if deposit == 0.0 {
659                return 0.0
660            }
661            
662            let totalEarned = (deposit * self.accumulatedInterestPerShare) / self.PRECISION
663            let alreadyClaimed = self.userClaimedAmount[receiverID] ?? 0.0
664            
665            return totalEarned > alreadyClaimed ? totalEarned - alreadyClaimed : 0.0
666        }
667        
668        access(contract) fun claimInterest(receiverID: UInt64, deposit: UFix64): UFix64 {
669            let pending = self.calculatePendingInterest(receiverID: receiverID, deposit: deposit)
670            
671            if pending > 0.0 {
672                let totalEarned = (deposit * self.accumulatedInterestPerShare) / self.PRECISION
673                self.userClaimedAmount[receiverID] = totalEarned
674            }
675            
676            return pending
677        }
678        
679        /// Withdraw interest, using yield source if interestVault is insufficient (for reinvested savings)
680        access(contract) fun withdrawInterestWithYieldSource(
681            amount: UFix64,
682            yieldSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}
683        ): @{FungibleToken.Vault} {
684            if amount == 0.0 {
685                let vaultType = self.interestVault.getType()
686                return <- DeFiActionsUtils.getEmptyVault(vaultType)
687            }
688            
689            var remaining = amount
690            let vaultType = self.interestVault.getType()
691            var resultVault <- DeFiActionsUtils.getEmptyVault(vaultType)
692            
693            // First, try to withdraw from interestVault (for any non-reinvested interest)
694            if self.interestVault.balance > 0.0 {
695                let fromVault = remaining < self.interestVault.balance ? remaining : self.interestVault.balance
696                resultVault.deposit(from: <- self.interestVault.withdraw(amount: fromVault))
697                remaining = remaining - fromVault
698            }
699            
700            // If still need more, withdraw from yield source (reinvested savings)
701            if remaining > 0.0 {
702                let availableFromYield = yieldSource.minimumAvailable()
703                let fromYield = remaining < availableFromYield ? remaining : availableFromYield
704                if fromYield > 0.0 {
705                    resultVault.deposit(from: <- yieldSource.withdrawAvailable(maxAmount: fromYield))
706                    remaining = remaining - fromYield
707                }
708            }
709            
710            assert(remaining == 0.0, message: "Insufficient interest available (vault + yield source)")
711            return <- resultVault
712        }
713        
714        access(contract) fun updateAfterBalanceChange(receiverID: UInt64, newDeposit: UFix64) {
715            self.userClaimedAmount[receiverID] = (newDeposit * self.accumulatedInterestPerShare) / self.PRECISION
716        }
717        
718        access(all) fun getInterestVaultBalance(): UFix64 {
719            return self.interestVault.balance
720        }
721        
722        access(all) fun getTotalDistributed(): UFix64 {
723            return self.totalDistributed
724        }
725    }
726    
727    // Lottery Distributor
728    access(all) resource LotteryDistributor {
729        access(self) var prizeVault: @{FungibleToken.Vault}
730        access(self) var nftPrizeVault: @{UInt64: {NonFungibleToken.NFT}}
731        access(self) var pendingNFTClaims: @{UInt64: [{NonFungibleToken.NFT}]}
732        access(self) var _prizeRound: UInt64
733        access(all) var totalPrizesDistributed: UFix64
734        
735        access(all) fun getPrizeRound(): UInt64 {
736            return self._prizeRound
737        }
738        
739        access(contract) fun setPrizeRound(round: UInt64) {
740            self._prizeRound = round
741        }
742        
743        init(vaultType: Type) {
744            self.prizeVault <- DeFiActionsUtils.getEmptyVault(vaultType)
745            self.nftPrizeVault <- {}
746            self.pendingNFTClaims <- {}
747            self._prizeRound = 0
748            self.totalPrizesDistributed = 0.0
749        }
750        
751        access(contract) fun fundPrizePool(vault: @{FungibleToken.Vault}) {
752            self.prizeVault.deposit(from: <- vault)
753        }
754        
755        access(all) fun getPrizePoolBalance(): UFix64 {
756            return self.prizeVault.balance
757        }
758        
759        access(contract) fun awardPrize(receiverID: UInt64, amount: UFix64, yieldSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}?): @{FungibleToken.Vault} {
760            self.totalPrizesDistributed = self.totalPrizesDistributed + amount
761            
762            var result <- DeFiActionsUtils.getEmptyVault(self.prizeVault.getType())
763            
764            // If yield source is provided (lottery is reinvested), try to withdraw from it first
765            if yieldSource != nil {
766                let available = yieldSource!.minimumAvailable()
767                if available >= amount {
768                    result.deposit(from: <- yieldSource!.withdrawAvailable(maxAmount: amount))
769                    return <- result
770                } else if available > 0.0 {
771                    // Partial withdrawal from yield source
772                    result.deposit(from: <- yieldSource!.withdrawAvailable(maxAmount: available))
773                }
774            }
775            
776            // Withdraw remainder from internal vault
777            if result.balance < amount {
778                let remaining = amount - result.balance
779                assert(self.prizeVault.balance >= remaining, message: "Insufficient prize pool")
780                result.deposit(from: <- self.prizeVault.withdraw(amount: remaining))
781            }
782            
783            return <- result
784        }
785        
786        // NFT Prize Management
787        access(contract) fun depositNFTPrize(nft: @{NonFungibleToken.NFT}) {
788            let nftID = nft.uuid
789            self.nftPrizeVault[nftID] <-! nft
790        }
791        
792        access(contract) fun withdrawNFTPrize(nftID: UInt64): @{NonFungibleToken.NFT} {
793            let nft <- self.nftPrizeVault.remove(key: nftID)
794            if nft == nil {
795                panic("NFT not found in prize vault")
796            }
797            return <- nft!
798        }
799        
800        access(contract) fun storePendingNFT(receiverID: UInt64, nft: @{NonFungibleToken.NFT}) {
801            if self.pendingNFTClaims[receiverID] == nil {
802                self.pendingNFTClaims[receiverID] <-! []
803            }
804            // Get mutable reference and append
805            let arrayRef = &self.pendingNFTClaims[receiverID] as auth(Mutate) &[{NonFungibleToken.NFT}]?
806            if arrayRef != nil {
807                arrayRef!.append(<- nft)
808            } else {
809                destroy nft
810                panic("Failed to store NFT in pending claims")
811            }
812        }
813        
814        access(all) fun getPendingNFTCount(receiverID: UInt64): Int {
815            return self.pendingNFTClaims[receiverID]?.length ?? 0
816        }
817        
818        access(all) fun getPendingNFTIDs(receiverID: UInt64): [UInt64] {
819            let nfts = &self.pendingNFTClaims[receiverID] as &[{NonFungibleToken.NFT}]?
820            if nfts == nil {
821                return []
822            }
823            
824            let ids: [UInt64] = []
825            for nft in nfts! {
826                ids.append(nft.uuid)
827            }
828            return ids
829        }
830        
831        access(all) fun getAvailableNFTPrizeIDs(): [UInt64] {
832            return self.nftPrizeVault.keys
833        }
834        
835        access(all) fun borrowNFTPrize(nftID: UInt64): &{NonFungibleToken.NFT}? {
836            return &self.nftPrizeVault[nftID]
837        }
838
839        access(contract) fun claimPendingNFT(receiverID: UInt64, nftIndex: Int): @{NonFungibleToken.NFT} {
840            pre {
841                self.pendingNFTClaims[receiverID] != nil: "No pending NFTs for this receiver"
842                nftIndex < self.pendingNFTClaims[receiverID]?.length!: "Invalid NFT index"
843            }
844            return <- self.pendingNFTClaims[receiverID]?.remove(at: nftIndex)!
845        }
846    }
847    
848    // Treasury Distributor
849    access(all) resource TreasuryDistributor {
850        access(self) var treasuryVault: @{FungibleToken.Vault}
851        access(all) var totalCollected: UFix64
852        access(all) var totalWithdrawn: UFix64
853        access(self) var withdrawalHistory: [{String: AnyStruct}]
854        
855        init(vaultType: Type) {
856            self.treasuryVault <- DeFiActionsUtils.getEmptyVault(vaultType)
857            self.totalCollected = 0.0
858            self.totalWithdrawn = 0.0
859            self.withdrawalHistory = []
860        }
861        
862        access(contract) fun deposit(vault: @{FungibleToken.Vault}) {
863            let amount = vault.balance
864            self.totalCollected = self.totalCollected + amount
865            self.treasuryVault.deposit(from: <- vault)
866        }
867        
868        access(all) fun getBalance(): UFix64 {
869            return self.treasuryVault.balance
870        }
871        
872        access(all) fun getTotalCollected(): UFix64 {
873            return self.totalCollected
874        }
875        
876        access(all) fun getTotalWithdrawn(): UFix64 {
877            return self.totalWithdrawn
878        }
879        
880        access(all) fun getWithdrawalHistory(): [{String: AnyStruct}] {
881            return self.withdrawalHistory
882        }
883        
884        access(contract) fun withdraw(amount: UFix64, withdrawnBy: Address, purpose: String): @{FungibleToken.Vault} {
885            pre {
886                self.treasuryVault.balance >= amount: "Insufficient treasury balance"
887                amount > 0.0: "Withdrawal amount must be positive"
888                purpose.length > 0: "Purpose must be specified"
889            }
890            
891            self.totalWithdrawn = self.totalWithdrawn + amount
892            
893            self.withdrawalHistory.append({
894                "address": withdrawnBy,
895                "amount": amount,
896                "timestamp": getCurrentBlock().timestamp,
897                "purpose": purpose
898            })
899            
900            return <- self.treasuryVault.withdraw(amount: amount)
901        }
902    }
903    
904    access(all) resource PrizeDrawReceipt {
905        access(all) let prizeAmount: UFix64
906        access(self) var request: @RandomConsumer.Request?
907        access(all) let timeWeightedStakes: {UInt64: UFix64}  // Snapshot of stakes at draw start
908        
909        init(prizeAmount: UFix64, request: @RandomConsumer.Request, timeWeightedStakes: {UInt64: UFix64}) {
910            self.prizeAmount = prizeAmount
911            self.request <- request
912            self.timeWeightedStakes = timeWeightedStakes
913        }
914        
915        access(all) view fun getRequestBlock(): UInt64? {
916            return self.request?.block
917        }
918        
919        access(contract) fun popRequest(): @RandomConsumer.Request {
920            let request <- self.request <- nil
921            return <- request!
922        }
923        
924        access(all) fun getTimeWeightedStakes(): {UInt64: UFix64} {
925            return self.timeWeightedStakes
926        }
927    }
928    
929    access(all) struct WinnerSelectionResult {
930        access(all) let winners: [UInt64]
931        access(all) let amounts: [UFix64]
932        access(all) let nftIDs: [[UInt64]]
933        
934        init(winners: [UInt64], amounts: [UFix64], nftIDs: [[UInt64]]) {
935            pre {
936                winners.length == amounts.length: "Winners and amounts must have same length"
937                winners.length == nftIDs.length: "Winners and nftIDs must have same length"
938            }
939            self.winners = winners
940            self.amounts = amounts
941            self.nftIDs = nftIDs
942        }
943    }
944    
945    /// Winner Selection Strategy Interface
946    /// Note: receiverDeposits contains time-weighted lottery stakes (deposit + pending interest)
947    /// This ensures users who have been in the pool longer have proportionally higher lottery chances
948    access(all) struct interface WinnerSelectionStrategy {
949        access(all) fun selectWinners(
950            randomNumber: UInt64,
951            receiverDeposits: {UInt64: UFix64},  // Time-weighted stakes: deposit + pending interest
952            totalPrizeAmount: UFix64
953        ): WinnerSelectionResult
954        access(all) fun getStrategyName(): String
955    }
956    
957    /// Weighted random selection - single winner
958    /// Selection is weighted by time-weighted stake (deposit + pending interest)
959    /// Users with larger deposits and/or longer time in pool have higher winning probability
960    access(all) struct WeightedSingleWinner: WinnerSelectionStrategy {
961        access(all) let nftIDs: [UInt64]
962        
963        init(nftIDs: [UInt64]) {
964            self.nftIDs = nftIDs
965        }
966        
967        access(all) fun selectWinners(
968            randomNumber: UInt64,
969            receiverDeposits: {UInt64: UFix64},
970            totalPrizeAmount: UFix64
971        ): WinnerSelectionResult {
972            let receiverIDs = receiverDeposits.keys
973            
974            if receiverIDs.length == 0 {
975                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
976            }
977            
978            if receiverIDs.length == 1 {
979                return WinnerSelectionResult(
980                    winners: [receiverIDs[0]],
981                    amounts: [totalPrizeAmount],
982                    nftIDs: [self.nftIDs]
983                )
984            }
985            
986            var cumulativeSum: [UFix64] = []
987            var runningTotal: UFix64 = 0.0
988            
989            for receiverID in receiverIDs {
990                runningTotal = runningTotal + receiverDeposits[receiverID]!
991                cumulativeSum.append(runningTotal)
992            }
993            
994            // Safety check: prevent overflow if runningTotal is 0
995            if runningTotal == 0.0 {
996                return WinnerSelectionResult(
997                    winners: [receiverIDs[0]],
998                    amounts: [totalPrizeAmount],
999                    nftIDs: [self.nftIDs]
1000                )
1001            }
1002            
1003            // Scale random number to [0, runningTotal) without overflow
1004            // Use modulo with safe upper bound (1 billion) for precision
1005            let scaledRandom = UFix64(randomNumber % 1_000_000_000) / 1_000_000_000.0
1006            let randomValue = scaledRandom * runningTotal
1007            
1008            var winnerIndex = 0
1009            for i, cumSum in cumulativeSum {
1010                if randomValue < cumSum {
1011                    winnerIndex = i
1012                    break
1013                }
1014            }
1015            
1016            return WinnerSelectionResult(
1017                winners: [receiverIDs[winnerIndex]],
1018                amounts: [totalPrizeAmount],
1019                nftIDs: [self.nftIDs]
1020            )
1021        }
1022        
1023        access(all) fun getStrategyName(): String {
1024            return "Weighted Single Winner"
1025        }
1026    }
1027    
1028    /// Multi-winner with split prizes
1029    /// Selection is weighted by time-weighted stake (deposit + pending interest)
1030    /// Users with larger deposits and/or longer time in pool have higher winning probability
1031    access(all) struct MultiWinnerSplit: WinnerSelectionStrategy {
1032        access(all) let winnerCount: Int
1033        access(all) let prizeSplits: [UFix64]  // e.g., [0.6, 0.3, 0.1] for 60%, 30%, 10%
1034        access(all) let nftIDsPerWinner: [[UInt64]]  // NFT IDs for each winner position, e.g., [[123], [456, 789], []]
1035        
1036        init(winnerCount: Int, prizeSplits: [UFix64], nftIDsPerWinner: [UInt64]) {
1037            pre {
1038                winnerCount > 0: "Must have at least one winner"
1039                prizeSplits.length == winnerCount: "Prize splits must match winner count"
1040            }
1041            
1042            // Validate all splits
1043            var total: UFix64 = 0.0
1044            for split in prizeSplits {
1045                assert(split >= 0.0 && split <= 1.0, message: "Each split must be between 0 and 1")
1046                total = total + split
1047            }
1048            
1049            assert(total == 1.0, message: "Prize splits must sum to 1.0")
1050            
1051            self.winnerCount = winnerCount
1052            self.prizeSplits = prizeSplits
1053            
1054            // Distribute NFT IDs across winners
1055            // If nftIDsPerWinner is provided, assign them to winners in order
1056            // Otherwise, create empty arrays for each winner
1057            var nftArray: [[UInt64]] = []
1058            var nftIndex = 0
1059            var winnerIdx = 0
1060            while winnerIdx < winnerCount {
1061                if nftIndex < nftIDsPerWinner.length {
1062                    // Assign one NFT per winner in order
1063                    nftArray.append([nftIDsPerWinner[nftIndex]])
1064                    nftIndex = nftIndex + 1
1065                } else {
1066                    // No more NFTs, assign empty array
1067                    nftArray.append([])
1068                }
1069                winnerIdx = winnerIdx + 1
1070            }
1071            self.nftIDsPerWinner = nftArray
1072        }
1073        
1074        access(all) fun selectWinners(
1075            randomNumber: UInt64,
1076            receiverDeposits: {UInt64: UFix64},
1077            totalPrizeAmount: UFix64
1078        ): WinnerSelectionResult {
1079            let receiverIDs = receiverDeposits.keys
1080            let depositorCount = receiverIDs.length
1081            
1082            if depositorCount == 0 {
1083                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1084            }
1085            
1086            assert(self.winnerCount <= depositorCount, message: "More winners than depositors")
1087            
1088            if depositorCount == 1 {
1089                // If only one depositor, they get all NFTs assigned to first winner position
1090                let nftIDsForFirst: [UInt64] = self.nftIDsPerWinner.length > 0 ? self.nftIDsPerWinner[0] : []
1091                return WinnerSelectionResult(
1092                    winners: [receiverIDs[0]],
1093                    amounts: [totalPrizeAmount],
1094                    nftIDs: [nftIDsForFirst]
1095                )
1096            }
1097            
1098            var cumulativeSum: [UFix64] = []
1099            var runningTotal: UFix64 = 0.0
1100            var depositsList: [UFix64] = []
1101            
1102            for receiverID in receiverIDs {
1103                let deposit = receiverDeposits[receiverID]!
1104                depositsList.append(deposit)
1105                runningTotal = runningTotal + deposit
1106                cumulativeSum.append(runningTotal)
1107            }
1108            
1109            var selectedWinners: [UInt64] = []
1110            var selectedIndices: {Int: Bool} = {}
1111            var remainingDeposits = depositsList
1112            var remainingIDs = receiverIDs
1113            var remainingCumSum = cumulativeSum
1114            var remainingTotal = runningTotal
1115            
1116            // Initialize Xorshift128plus PRG with the random beacon value
1117            // Use empty salt as the randomNumber already comes from a secure source (Flow's random beacon)
1118            // Pad to 16 bytes (PRG requires at least 16 bytes)
1119            var randomBytes = randomNumber.toBigEndianBytes()
1120            // Pad by duplicating the bytes to reach 16 bytes
1121            while randomBytes.length < 16 {
1122                randomBytes.appendAll(randomNumber.toBigEndianBytes())
1123            }
1124            // Take only first 16 bytes if we have more
1125            var paddedBytes: [UInt8] = []
1126            var padIdx = 0
1127            while padIdx < 16 {
1128                paddedBytes.append(randomBytes[padIdx % randomBytes.length])
1129                padIdx = padIdx + 1
1130            }
1131            
1132            let prg = Xorshift128plus.PRG(
1133                sourceOfRandomness: paddedBytes,
1134                salt: []
1135            )
1136            
1137            var winnerIndex = 0
1138                while winnerIndex < self.winnerCount && remainingIDs.length > 0 && remainingTotal > 0.0 {
1139                    let rng = prg.nextUInt64()
1140                    // Scale RNG to [0, remainingTotal) without overflow
1141                    // Use modulo with safe upper bound (1 billion) for precision
1142                    let scaledRandom = UFix64(rng % 1_000_000_000) / 1_000_000_000.0
1143                    let randomValue = scaledRandom * remainingTotal
1144                
1145                var selectedIdx = 0
1146                for i, cumSum in remainingCumSum {
1147                    if randomValue < cumSum {
1148                        selectedIdx = i
1149                        break
1150                    }
1151                }
1152                
1153                selectedWinners.append(remainingIDs[selectedIdx])
1154                selectedIndices[selectedIdx] = true
1155                var newRemainingIDs: [UInt64] = []
1156                var newRemainingDeposits: [UFix64] = []
1157                var newCumSum: [UFix64] = []
1158                var newRunningTotal: UFix64 = 0.0
1159                
1160                var idx = 0
1161                while idx < remainingIDs.length {
1162                    if idx != selectedIdx {
1163                        newRemainingIDs.append(remainingIDs[idx])
1164                        newRemainingDeposits.append(remainingDeposits[idx])
1165                        newRunningTotal = newRunningTotal + remainingDeposits[idx]
1166                        newCumSum.append(newRunningTotal)
1167                    }
1168                    idx = idx + 1
1169                }
1170                
1171                remainingIDs = newRemainingIDs
1172                remainingDeposits = newRemainingDeposits
1173                remainingCumSum = newCumSum
1174                remainingTotal = newRunningTotal
1175                winnerIndex = winnerIndex + 1
1176            }
1177            
1178            var prizeAmounts: [UFix64] = []
1179            var calculatedSum: UFix64 = 0.0
1180            var idx = 0
1181            
1182            while idx < selectedWinners.length - 1 {
1183                let split = self.prizeSplits[idx]
1184                let amount = totalPrizeAmount * split
1185                prizeAmounts.append(amount)
1186                calculatedSum = calculatedSum + amount
1187                idx = idx + 1
1188            }
1189            
1190            // Last prize gets remainder to ensure exact sum
1191            let lastPrize = totalPrizeAmount - calculatedSum
1192            prizeAmounts.append(lastPrize)
1193            
1194            var finalSum: UFix64 = 0.0
1195            for amount in prizeAmounts {
1196                finalSum = finalSum + amount
1197            }
1198            assert(finalSum == totalPrizeAmount, message: "Prize amounts must sum to total prize pool")
1199            
1200            let expectedLast = totalPrizeAmount * self.prizeSplits[selectedWinners.length - 1]
1201            let deviation = lastPrize > expectedLast ? lastPrize - expectedLast : expectedLast - lastPrize
1202            let maxDeviation = totalPrizeAmount * 0.01 // 1% tolerance
1203            assert(deviation <= maxDeviation, message: "Last prize deviation too large - check splits")
1204            
1205            // Assign NFT IDs to winners based on their position
1206            // First winner gets first NFT set, second winner gets second NFT set, etc.
1207            var nftIDsArray: [[UInt64]] = []
1208            var idx2 = 0
1209            while idx2 < selectedWinners.length {
1210                if idx2 < self.nftIDsPerWinner.length {
1211                    nftIDsArray.append(self.nftIDsPerWinner[idx2])
1212                } else {
1213                    nftIDsArray.append([])
1214                }
1215                idx2 = idx2 + 1
1216            }
1217            
1218            return WinnerSelectionResult(
1219                winners: selectedWinners,
1220                amounts: prizeAmounts,
1221                nftIDs: nftIDsArray
1222            )
1223        }
1224        
1225        access(all) fun getStrategyName(): String {
1226            var name = "Multi-Winner (".concat(self.winnerCount.toString()).concat(" winners): ")
1227            var idx = 0
1228            while idx < self.prizeSplits.length {
1229                if idx > 0 {
1230                    name = name.concat(", ")
1231                }
1232                name = name.concat((self.prizeSplits[idx] * 100.0).toString()).concat("%")
1233                idx = idx + 1
1234            }
1235            return name
1236        }
1237    }
1238    
1239    /// Prize tier for fixed-amount lottery
1240    access(all) struct PrizeTier {
1241        access(all) let prizeAmount: UFix64
1242        access(all) let winnerCount: Int
1243        access(all) let name: String
1244        access(all) let nftIDs: [UInt64]
1245        
1246        init(amount: UFix64, count: Int, name: String, nftIDs: [UInt64]) {
1247            pre {
1248                amount > 0.0: "Prize amount must be positive"
1249                count > 0: "Winner count must be positive"
1250                nftIDs.length <= count: "Cannot have more NFTs than winners in tier"
1251            }
1252            self.prizeAmount = amount
1253            self.winnerCount = count
1254            self.name = name
1255            self.nftIDs = nftIDs
1256        }
1257    }
1258    
1259    /// Fixed prize tiers - only draws when sufficient funds accumulated
1260    /// Best for marketing: "Win $10,000!" instead of variable percentages
1261    /// Selection is weighted by time-weighted stake (deposit + pending interest)
1262    access(all) struct FixedPrizeTiers: WinnerSelectionStrategy {
1263        access(all) let tiers: [PrizeTier]
1264        
1265        init(tiers: [PrizeTier]) {
1266            pre {
1267                tiers.length > 0: "Must have at least one prize tier"
1268            }
1269            self.tiers = tiers
1270        }
1271        
1272        access(all) fun selectWinners(
1273            randomNumber: UInt64,
1274            receiverDeposits: {UInt64: UFix64},
1275            totalPrizeAmount: UFix64
1276        ): WinnerSelectionResult {
1277            let receiverIDs = receiverDeposits.keys
1278            let depositorCount = receiverIDs.length
1279            
1280            if depositorCount == 0 {
1281                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1282            }
1283            
1284            // Calculate total needed for all tiers
1285            var totalNeeded: UFix64 = 0.0
1286            var totalWinnersNeeded = 0
1287            for tier in self.tiers {
1288                totalNeeded = totalNeeded + (tier.prizeAmount * UFix64(tier.winnerCount))
1289                totalWinnersNeeded = totalWinnersNeeded + tier.winnerCount
1290            }
1291            
1292            // Not enough funds - skip draw and let prize pool accumulate
1293            if totalPrizeAmount < totalNeeded {
1294                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1295            }
1296            
1297            // Not enough depositors for all prizes
1298            if totalWinnersNeeded > depositorCount {
1299                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1300            }
1301            
1302            // Build cumulative distribution for weighted selection
1303            var cumulativeSum: [UFix64] = []
1304            var runningTotal: UFix64 = 0.0
1305            
1306            for receiverID in receiverIDs {
1307                let deposit = receiverDeposits[receiverID]!
1308                runningTotal = runningTotal + deposit
1309                cumulativeSum.append(runningTotal)
1310            }
1311            
1312            // Initialize PRG - pad to 16 bytes (PRG requires at least 16 bytes)
1313            var randomBytes = randomNumber.toBigEndianBytes()
1314            // Pad by duplicating the bytes to reach 16 bytes
1315            while randomBytes.length < 16 {
1316                randomBytes.appendAll(randomNumber.toBigEndianBytes())
1317            }
1318            // Take only first 16 bytes if we have more
1319            var paddedBytes: [UInt8] = []
1320            var padIdx2 = 0
1321            while padIdx2 < 16 {
1322                paddedBytes.append(randomBytes[padIdx2 % randomBytes.length])
1323                padIdx2 = padIdx2 + 1
1324            }
1325            
1326            let prg = Xorshift128plus.PRG(
1327                sourceOfRandomness: paddedBytes,
1328                salt: []
1329            )
1330            
1331            var allWinners: [UInt64] = []
1332            var allPrizes: [UFix64] = []
1333            var allNFTIDs: [[UInt64]] = []
1334            var usedIndices: {Int: Bool} = {}
1335            var remainingIDs = receiverIDs
1336            var remainingCumSum = cumulativeSum
1337            var remainingTotal = runningTotal
1338            
1339            // Select winners for each tier
1340            for tier in self.tiers {
1341                var tierWinnerCount = 0
1342                
1343                while tierWinnerCount < tier.winnerCount && remainingIDs.length > 0 && remainingTotal > 0.0 {
1344                    let rng = prg.nextUInt64()
1345                    // Scale RNG to [0, remainingTotal) without overflow
1346                    // Use modulo with safe upper bound (1 billion) for precision
1347                    let scaledRandom = UFix64(rng % 1_000_000_000) / 1_000_000_000.0
1348                    let randomValue = scaledRandom * remainingTotal
1349                    
1350                    var selectedIdx = 0
1351                    for i, cumSum in remainingCumSum {
1352                        if randomValue <= cumSum {
1353                            selectedIdx = i
1354                            break
1355                        }
1356                    }
1357                    
1358                    let winnerID = remainingIDs[selectedIdx]
1359                    allWinners.append(winnerID)
1360                    allPrizes.append(tier.prizeAmount)
1361                    
1362                    // Assign NFT to this winner if tier has NFTs
1363                    if tierWinnerCount < tier.nftIDs.length {
1364                        allNFTIDs.append([tier.nftIDs[tierWinnerCount]])
1365                    } else {
1366                        allNFTIDs.append([])
1367                    }
1368                    
1369                    // Remove winner from remaining pool
1370                    var newRemainingIDs: [UInt64] = []
1371                    var newRemainingCumSum: [UFix64] = []
1372                    var newRunningTotal: UFix64 = 0.0
1373                    var oldIdx = 0
1374                    
1375                    while oldIdx < remainingIDs.length {
1376                        if oldIdx != selectedIdx {
1377                            newRemainingIDs.append(remainingIDs[oldIdx])
1378                            let deposit = receiverDeposits[remainingIDs[oldIdx]]!
1379                            newRunningTotal = newRunningTotal + deposit
1380                            newRemainingCumSum.append(newRunningTotal)
1381                        }
1382                        oldIdx = oldIdx + 1
1383                    }
1384                    
1385                    remainingIDs = newRemainingIDs
1386                    remainingCumSum = newRemainingCumSum
1387                    remainingTotal = newRunningTotal
1388                    tierWinnerCount = tierWinnerCount + 1
1389                }
1390            }
1391            
1392            return WinnerSelectionResult(
1393                winners: allWinners,
1394                amounts: allPrizes,
1395                nftIDs: allNFTIDs
1396            )
1397        }
1398        
1399        access(all) fun getStrategyName(): String {
1400            var name = "Fixed Prizes ("
1401            var idx = 0
1402            while idx < self.tiers.length {
1403                if idx > 0 {
1404                    name = name.concat(", ")
1405                }
1406                let tier = self.tiers[idx]
1407                name = name.concat(tier.winnerCount.toString())
1408                    .concat("x ")
1409                    .concat(tier.prizeAmount.toString())
1410                idx = idx + 1
1411            }
1412            return name.concat(")")
1413        }
1414    }
1415    
1416    // Pool Configuration
1417    access(all) struct PoolConfig {
1418        access(all) let assetType: Type
1419        access(all) let yieldConnector: {DeFiActions.Sink, DeFiActions.Source}
1420        access(all) var minimumDeposit: UFix64
1421        access(all) var drawIntervalSeconds: UFix64
1422        access(all) var distributionStrategy: {DistributionStrategy}
1423        access(all) var winnerSelectionStrategy: {WinnerSelectionStrategy}
1424        access(all) var winnerTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?
1425        
1426        init(
1427            assetType: Type,
1428            yieldConnector: {DeFiActions.Sink, DeFiActions.Source},
1429            minimumDeposit: UFix64,
1430            drawIntervalSeconds: UFix64,
1431            distributionStrategy: {DistributionStrategy},
1432            winnerSelectionStrategy: {WinnerSelectionStrategy},
1433            winnerTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?
1434        ) {
1435            self.assetType = assetType
1436            self.yieldConnector = yieldConnector
1437            self.minimumDeposit = minimumDeposit
1438            self.drawIntervalSeconds = drawIntervalSeconds
1439            self.distributionStrategy = distributionStrategy
1440            self.winnerSelectionStrategy = winnerSelectionStrategy
1441            self.winnerTrackerCap = winnerTrackerCap
1442        }
1443        
1444        access(contract) fun setDistributionStrategy(strategy: {DistributionStrategy}) {
1445            self.distributionStrategy = strategy
1446        }
1447        
1448        access(contract) fun setWinnerSelectionStrategy(strategy: {WinnerSelectionStrategy}) {
1449            self.winnerSelectionStrategy = strategy
1450        }
1451        
1452        access(contract) fun setWinnerTrackerCap(cap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?) {
1453            self.winnerTrackerCap = cap
1454        }
1455        
1456        access(contract) fun setDrawIntervalSeconds(interval: UFix64) {
1457            pre {
1458                interval >= 1.0: "Draw interval must be at least 1 hour (1 seconds)"
1459            }
1460            self.drawIntervalSeconds = interval
1461        }
1462        
1463        access(contract) fun setMinimumDeposit(minimum: UFix64) {
1464            pre {
1465                minimum >= 0.0: "Minimum deposit cannot be negative"
1466            }
1467            self.minimumDeposit = minimum
1468        }
1469    }
1470    
1471    // Pool Resource
1472    access(all) resource Pool {
1473        access(self) var config: PoolConfig
1474        access(self) var poolID: UInt64
1475        
1476        access(self) var emergencyState: PoolEmergencyState
1477        access(self) var emergencyReason: String?
1478        access(self) var emergencyActivatedAt: UFix64?
1479        access(self) var emergencyConfig: EmergencyConfig
1480        access(self) var consecutiveWithdrawFailures: Int
1481        
1482        access(self) var fundingPolicy: FundingPolicy
1483        
1484        access(contract) fun setPoolID(id: UInt64) {
1485            self.poolID = id
1486        }
1487        
1488        // User tracking
1489        access(self) let receiverDeposits: {UInt64: UFix64}
1490        access(self) let receiverTotalEarnedSavings: {UInt64: UFix64}
1491        access(self) let receiverTotalEarnedPrizes: {UInt64: UFix64}
1492        access(self) let receiverPrizes: {UInt64: UFix64}
1493        access(self) let registeredReceivers: {UInt64: Bool}
1494        access(self) let receiverBonusWeights: {UInt64: BonusWeightRecord}
1495        access(all) var totalDeposited: UFix64
1496        access(all) var totalStaked: UFix64
1497        access(all) var lotteryStaked: UFix64
1498        access(all) var lastDrawTimestamp: UFix64
1499        access(self) let savingsDistributor: @SavingsDistributor
1500        access(self) let lotteryDistributor: @LotteryDistributor
1501        access(self) let treasuryDistributor: @TreasuryDistributor
1502        
1503        // Liquid vault and draw state
1504        access(self) var liquidVault: @{FungibleToken.Vault}
1505        access(self) var pendingDrawReceipt: @PrizeDrawReceipt?
1506        access(self) let randomConsumer: @RandomConsumer.Consumer
1507        
1508        init(
1509            config: PoolConfig, 
1510            initialVault: @{FungibleToken.Vault},
1511            emergencyConfig: EmergencyConfig?,
1512            fundingPolicy: FundingPolicy?
1513        ) {
1514            pre {
1515                initialVault.getType() == config.assetType: "Vault type mismatch"
1516                initialVault.balance == 0.0: "Initial vault must be empty"
1517            }
1518            
1519            self.config = config
1520            self.poolID = 0
1521            
1522            self.emergencyState = PoolEmergencyState.Normal
1523            self.emergencyReason = nil
1524            self.emergencyActivatedAt = nil
1525            self.emergencyConfig = emergencyConfig ?? PrizeVaultModular.createDefaultEmergencyConfig()
1526            self.consecutiveWithdrawFailures = 0
1527            
1528            self.fundingPolicy = fundingPolicy ?? PrizeVaultModular.createDefaultFundingPolicy()
1529            
1530            self.receiverDeposits = {}
1531            self.receiverTotalEarnedSavings = {}
1532            self.receiverTotalEarnedPrizes = {}
1533            self.receiverPrizes = {}
1534            self.registeredReceivers = {}
1535            self.receiverBonusWeights = {}
1536            self.totalDeposited = 0.0
1537            self.totalStaked = 0.0
1538            self.lotteryStaked = 0.0
1539            self.lastDrawTimestamp = 0.0
1540            
1541            self.savingsDistributor <- create SavingsDistributor(vaultType: config.assetType)
1542            self.lotteryDistributor <- create LotteryDistributor(vaultType: config.assetType)
1543            self.treasuryDistributor <- create TreasuryDistributor(vaultType: config.assetType)
1544            
1545            self.liquidVault <- initialVault
1546            self.pendingDrawReceipt <- nil
1547            self.randomConsumer <- RandomConsumer.createConsumer()
1548        }
1549        
1550        access(all) fun registerReceiver(receiverID: UInt64) {
1551            pre {
1552                self.registeredReceivers[receiverID] == nil: "Receiver already registered"
1553            }
1554            self.registeredReceivers[receiverID] = true
1555        }
1556        
1557        
1558        access(all) view fun getEmergencyState(): PoolEmergencyState {
1559            return self.emergencyState
1560        }
1561        
1562        access(all) view fun getEmergencyConfig(): EmergencyConfig {
1563            return self.emergencyConfig
1564        }
1565        
1566        access(contract) fun setState(state: PoolEmergencyState, reason: String?) {
1567            self.emergencyState = state
1568            if state != PoolEmergencyState.Normal {
1569                self.emergencyReason = reason
1570                self.emergencyActivatedAt = getCurrentBlock().timestamp
1571            } else {
1572                self.emergencyReason = nil
1573                self.emergencyActivatedAt = nil
1574                self.consecutiveWithdrawFailures = 0
1575            }
1576        }
1577        
1578        access(contract) fun setEmergencyMode(reason: String) {
1579            self.emergencyState = PoolEmergencyState.EmergencyMode
1580            self.emergencyReason = reason
1581            self.emergencyActivatedAt = getCurrentBlock().timestamp
1582        }
1583        
1584        access(contract) fun setPartialMode(reason: String) {
1585            self.emergencyState = PoolEmergencyState.PartialMode
1586            self.emergencyReason = reason
1587            self.emergencyActivatedAt = getCurrentBlock().timestamp
1588        }
1589        
1590        access(contract) fun clearEmergencyMode() {
1591            self.emergencyState = PoolEmergencyState.Normal
1592            self.emergencyReason = nil
1593            self.emergencyActivatedAt = nil
1594            self.consecutiveWithdrawFailures = 0
1595        }
1596        
1597        access(contract) fun setEmergencyConfig(config: EmergencyConfig) {
1598            self.emergencyConfig = config
1599        }
1600        
1601        access(all) view fun isEmergencyMode(): Bool {
1602            return self.emergencyState == PoolEmergencyState.EmergencyMode
1603        }
1604        
1605        access(all) view fun isPartialMode(): Bool {
1606            return self.emergencyState == PoolEmergencyState.PartialMode
1607        }
1608        
1609        access(all) fun getEmergencyInfo(): {String: AnyStruct}? {
1610            if self.emergencyState != PoolEmergencyState.Normal {
1611                let duration = getCurrentBlock().timestamp - (self.emergencyActivatedAt ?? 0.0)
1612                let health = self.checkYieldSourceHealth()
1613                return {
1614                    "state": self.emergencyState.rawValue,
1615                    "reason": self.emergencyReason ?? "Unknown",
1616                    "activatedAt": self.emergencyActivatedAt ?? 0.0,
1617                    "durationSeconds": duration,
1618                    "yieldSourceHealth": health,
1619                    "canAutoRecover": self.emergencyConfig.autoRecoveryEnabled,
1620                    "maxDuration": self.emergencyConfig.maxEmergencyDuration
1621                }
1622            }
1623            return nil
1624        }
1625        
1626        access(contract) fun checkYieldSourceHealth(): UFix64 {
1627            let yieldSource = &self.config.yieldConnector as &{DeFiActions.Source}
1628            let balance = yieldSource.minimumAvailable()
1629            let threshold = self.getEmergencyConfig().minBalanceThreshold
1630            let balanceHealthy = balance >= self.totalStaked * threshold
1631            let withdrawSuccessRate = self.consecutiveWithdrawFailures == 0 ? 1.0 : 
1632                (1.0 / UFix64(self.consecutiveWithdrawFailures + 1))
1633            
1634            var health: UFix64 = 0.0
1635            if balanceHealthy { health = health + 0.5 }
1636            health = health + (withdrawSuccessRate * 0.5)
1637            return health
1638        }
1639        
1640        access(contract) fun checkAndAutoTriggerEmergency(): Bool {
1641            if self.emergencyState != PoolEmergencyState.Normal {
1642                return false
1643            }
1644            
1645            let health = self.checkYieldSourceHealth()
1646            if health < self.emergencyConfig.minYieldSourceHealth {
1647                self.setEmergencyMode(reason: "Auto-triggered: Yield source health below threshold (".concat(health.toString()).concat(")"))
1648                emit EmergencyModeAutoTriggered(poolID: self.poolID, reason: "Low yield source health", healthScore: health, timestamp: getCurrentBlock().timestamp)
1649                return true
1650            }
1651            
1652            if self.consecutiveWithdrawFailures >= self.emergencyConfig.maxWithdrawFailures {
1653                self.setEmergencyMode(reason: "Auto-triggered: Multiple consecutive withdrawal failures")
1654                emit EmergencyModeAutoTriggered(poolID: self.poolID, reason: "Withdrawal failures", healthScore: health, timestamp: getCurrentBlock().timestamp)
1655                return true
1656            }
1657            
1658            return false
1659        }
1660        
1661        access(contract) fun checkAndAutoRecover(): Bool {
1662            if self.emergencyState != PoolEmergencyState.EmergencyMode {
1663                return false
1664            }
1665            
1666            if !self.emergencyConfig.autoRecoveryEnabled {
1667                return false
1668            }
1669            
1670            if let maxDuration = self.emergencyConfig.maxEmergencyDuration {
1671                let duration = getCurrentBlock().timestamp - (self.emergencyActivatedAt ?? 0.0)
1672                if duration > maxDuration {
1673                    self.clearEmergencyMode()
1674                    emit EmergencyModeAutoRecovered(poolID: self.poolID, reason: "Max duration exceeded", healthScore: nil, duration: duration, timestamp: getCurrentBlock().timestamp)
1675                    return true
1676                }
1677            }
1678            
1679            let health = self.checkYieldSourceHealth()
1680            if health >= 0.9 {
1681                self.clearEmergencyMode()
1682                emit EmergencyModeAutoRecovered(poolID: self.poolID, reason: "Yield source recovered", healthScore: health, duration: nil, timestamp: getCurrentBlock().timestamp)
1683                return true
1684            }
1685            
1686            return false
1687        }
1688        
1689        
1690        /// Distribute savings interest to all users
1691        /// This is the core savings distribution logic used by both direct funding and process rewards
1692        /// 
1693        /// Flow:
1694        /// 1. Snapshot totalDeposited BEFORE compounding (prevents dilution)
1695        /// 2. Compound any existing pending interest
1696        /// 3. Distribute new interest based on snapshot
1697        /// 4. Compound the newly distributed interest
1698        /// 5. Send any rounding dust to treasury
1699        access(contract) fun distributeSavingsInterest(vault: @{FungibleToken.Vault}) {
1700            let amount = vault.balance
1701            
1702            let totalDepositedSnapshot = self.totalDeposited
1703            
1704            self.compoundAllPendingSavings()
1705            
1706            // Distribute new interest based on snapshot
1707            let interestPerShare = self.savingsDistributor.distributeInterestAndReinvest(
1708                vault: <- vault,
1709                totalDeposited: totalDepositedSnapshot,
1710                yieldSink: &self.config.yieldConnector as &{DeFiActions.Sink}
1711            )
1712            
1713            // Compound the newly distributed interest (updates totalDeposited and totalStaked)
1714            let totalCompounded = self.compoundAllPendingSavings()
1715            
1716            // Handle rounding dust: send to treasury to prevent yield source imbalance
1717            if totalCompounded < amount {
1718                let dust = amount - totalCompounded
1719                let dustVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: dust)
1720                self.treasuryDistributor.deposit(vault: <- dustVault)
1721                
1722                emit SavingsRoundingDustToTreasury(poolID: self.poolID, amount: dust)
1723            }
1724            
1725            emit SavingsInterestDistributed(poolID: self.poolID, amount: amount, interestPerShare: interestPerShare)
1726        }
1727        
1728        access(contract) fun compoundAllPendingSavings(): UFix64 {
1729            let receiverIDs = self.getRegisteredReceiverIDs()
1730            var totalCompounded: UFix64 = 0.0
1731            var usersCompounded: Int = 0
1732            
1733            for receiverID in receiverIDs {
1734                let currentDeposit = self.receiverDeposits[receiverID] ?? 0.0
1735                if currentDeposit > 0.0 {
1736                    let pending = self.savingsDistributor.claimInterest(receiverID: receiverID, deposit: currentDeposit)
1737                    
1738                    if pending > 0.0 {
1739                        let newDeposit = currentDeposit + pending
1740                        self.receiverDeposits[receiverID] = newDeposit
1741                        self.totalDeposited = self.totalDeposited + pending
1742                        totalCompounded = totalCompounded + pending
1743                        self.totalStaked = self.totalStaked + pending
1744                        self.savingsDistributor.updateAfterBalanceChange(receiverID: receiverID, newDeposit: newDeposit)
1745                        
1746                        let currentSavings = self.receiverTotalEarnedSavings[receiverID] ?? 0.0
1747                        self.receiverTotalEarnedSavings[receiverID] = currentSavings + pending
1748                        
1749                        usersCompounded = usersCompounded + 1
1750                    }
1751                }
1752            }
1753            
1754            if usersCompounded > 0 {
1755                let avgAmount = totalCompounded / UFix64(usersCompounded)
1756                emit SavingsInterestCompoundedBatch(
1757                    poolID: self.poolID,
1758                    userCount: usersCompounded,
1759                    totalAmount: totalCompounded,
1760                    avgAmount: avgAmount
1761                )
1762            }
1763            
1764            return totalCompounded
1765        }
1766        
1767        access(contract) fun fundDirectInternal(
1768            destination: PoolFundingDestination,
1769            from: @{FungibleToken.Vault},
1770            sponsor: Address,
1771            purpose: String,
1772            metadata: {String: String}
1773        ) {
1774            pre {
1775                self.emergencyState == PoolEmergencyState.Normal: "Direct funding only in normal state"
1776                from.getType() == self.config.assetType: "Invalid vault type"
1777            }
1778            
1779            let amount = from.balance
1780            self.fundingPolicy.recordDirectFunding(destination: destination, amount: amount)
1781            
1782            switch destination {
1783                case PoolFundingDestination.Lottery:
1784                    self.lotteryDistributor.fundPrizePool(vault: <- from)
1785                case PoolFundingDestination.Treasury:
1786                    self.treasuryDistributor.deposit(vault: <- from)
1787                case PoolFundingDestination.Savings:
1788                    self.distributeSavingsInterest(vault: <- from)
1789                default:
1790                    panic("Unsupported funding destination")
1791            }
1792        }
1793        
1794        access(all) fun getFundingStats(): {String: UFix64} {
1795            return {
1796                "totalDirectLottery": self.fundingPolicy.totalDirectLottery,
1797                "totalDirectTreasury": self.fundingPolicy.totalDirectTreasury,
1798                "totalDirectSavings": self.fundingPolicy.totalDirectSavings,
1799                "maxDirectLottery": self.fundingPolicy.maxDirectLottery ?? 0.0,
1800                "maxDirectTreasury": self.fundingPolicy.maxDirectTreasury ?? 0.0,
1801                "maxDirectSavings": self.fundingPolicy.maxDirectSavings ?? 0.0
1802            }
1803        }
1804        
1805        access(all) fun deposit(from: @{FungibleToken.Vault}, receiverID: UInt64) {
1806            pre {
1807                from.balance > 0.0: "Deposit amount must be positive"
1808                from.getType() == self.config.assetType: "Invalid vault type"
1809                self.registeredReceivers[receiverID] == true: "Receiver not registered"
1810            }
1811            
1812            switch self.emergencyState {
1813                case PoolEmergencyState.Normal:
1814                    assert(from.balance >= self.config.minimumDeposit, message: "Below minimum deposit of ".concat(self.config.minimumDeposit.toString()))
1815                case PoolEmergencyState.PartialMode:
1816                    let depositLimit = self.emergencyConfig.partialModeDepositLimit ?? 0.0
1817                    assert(depositLimit > 0.0, message: "Partial mode deposit limit not configured")
1818                    assert(from.balance <= depositLimit, message: "Deposit exceeds partial mode limit of ".concat(depositLimit.toString()))
1819                case PoolEmergencyState.EmergencyMode:
1820                    panic("Deposits disabled in emergency mode. Withdrawals only.")
1821                case PoolEmergencyState.Paused:
1822                    panic("Pool is paused. No operations allowed.")
1823            }
1824            
1825            let amount = from.balance
1826            let isFirstDeposit = (self.receiverDeposits[receiverID] ?? 0.0) == 0.0
1827            var pendingCompounded: UFix64 = 0.0
1828            
1829            if !isFirstDeposit {
1830                let currentDeposit = self.receiverDeposits[receiverID]!
1831                let pending = self.savingsDistributor.claimInterest(receiverID: receiverID, deposit: currentDeposit)
1832                if pending > 0.0 {
1833                    self.receiverDeposits[receiverID] = currentDeposit + pending
1834                    pendingCompounded = pending
1835                    self.totalDeposited = self.totalDeposited + pending
1836                    self.totalStaked = self.totalStaked + pending
1837                    
1838                    let currentSavings = self.receiverTotalEarnedSavings[receiverID] ?? 0.0
1839                    self.receiverTotalEarnedSavings[receiverID] = currentSavings + pending
1840                    emit SavingsInterestCompounded(poolID: self.poolID, receiverID: receiverID, amount: pending)
1841                }
1842            }
1843            
1844            let newDeposit = (self.receiverDeposits[receiverID] ?? 0.0) + amount
1845            self.receiverDeposits[receiverID] = newDeposit
1846            self.totalDeposited = self.totalDeposited + amount
1847            self.totalStaked = self.totalStaked + amount
1848            
1849            if isFirstDeposit {
1850                self.savingsDistributor.initializeReceiver(receiverID: receiverID, deposit: amount)
1851            } else {
1852                self.savingsDistributor.updateAfterBalanceChange(receiverID: receiverID, newDeposit: newDeposit)
1853            }
1854            self.config.yieldConnector.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
1855            destroy from
1856            emit Deposited(poolID: self.poolID, receiverID: receiverID, amount: amount)
1857        }
1858        
1859        access(all) fun withdraw(amount: UFix64, receiverID: UInt64): @{FungibleToken.Vault} {
1860            pre {
1861                amount > 0.0: "Withdrawal amount must be positive"
1862                self.registeredReceivers[receiverID] == true: "Receiver not registered"
1863            }
1864            
1865            assert(self.emergencyState != PoolEmergencyState.Paused, message: "Pool is paused - no operations allowed")
1866            
1867            if self.emergencyState == PoolEmergencyState.EmergencyMode {
1868                self.checkAndAutoRecover()
1869            }
1870            
1871            let receiverDeposit = self.receiverDeposits[receiverID] ?? 0.0
1872            assert(receiverDeposit >= amount, message: "Insufficient deposit. You have ".concat(receiverDeposit.toString()).concat(" but trying to withdraw ").concat(amount.toString()))
1873            
1874            if self.emergencyState == PoolEmergencyState.EmergencyMode {
1875                log("⚠️  Emergency withdrawal - skipping interest compounding")
1876            } else {
1877                let pending = self.savingsDistributor.claimInterest(receiverID: receiverID, deposit: receiverDeposit)
1878                if pending > 0.0 {
1879                    let newDepositWithPending = receiverDeposit + pending
1880                    self.receiverDeposits[receiverID] = newDepositWithPending
1881                    self.totalDeposited = self.totalDeposited + pending
1882                    self.totalStaked = self.totalStaked + pending
1883                    self.savingsDistributor.updateAfterBalanceChange(receiverID: receiverID, newDeposit: newDepositWithPending)
1884                    let currentSavings = self.receiverTotalEarnedSavings[receiverID] ?? 0.0
1885                    self.receiverTotalEarnedSavings[receiverID] = currentSavings + pending
1886                    emit SavingsInterestCompounded(poolID: self.poolID, receiverID: receiverID, amount: pending)
1887                }
1888            }
1889            
1890            let currentDeposit = self.receiverDeposits[receiverID] ?? 0.0
1891            let newDeposit = currentDeposit - amount
1892            self.receiverDeposits[receiverID] = newDeposit
1893            self.totalDeposited = self.totalDeposited - amount
1894            self.savingsDistributor.updateAfterBalanceChange(receiverID: receiverID, newDeposit: newDeposit)
1895            
1896            var withdrawn <- DeFiActionsUtils.getEmptyVault(self.config.assetType)
1897            var withdrawalFailed = false
1898            var amountFromYieldSource: UFix64 = 0.0
1899            
1900            if self.emergencyState == PoolEmergencyState.EmergencyMode {
1901                let yieldAvailable = self.config.yieldConnector.minimumAvailable()
1902                if yieldAvailable >= amount {
1903                    let yieldVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: amount)
1904                    amountFromYieldSource = yieldVault.balance
1905                    withdrawn.deposit(from: <- yieldVault)
1906                } else {
1907                    log("⚠️  Yield source insufficient in emergency, using liquid vault")
1908                    withdrawalFailed = true
1909                }
1910            } else {
1911                let yieldAvailable = self.config.yieldConnector.minimumAvailable()
1912                if yieldAvailable >= amount {
1913                    let yieldVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: amount)
1914                    amountFromYieldSource = yieldVault.balance
1915                    withdrawn.deposit(from: <- yieldVault)
1916                } else {
1917                    withdrawalFailed = true
1918                }
1919                
1920                if withdrawalFailed {
1921                    self.consecutiveWithdrawFailures = self.consecutiveWithdrawFailures + 1
1922                    emit WithdrawalFailure(poolID: self.poolID, receiverID: receiverID, amount: amount,
1923                        consecutiveFailures: self.consecutiveWithdrawFailures, yieldAvailable: yieldAvailable)
1924                    self.checkAndAutoTriggerEmergency()
1925                } else {
1926                    self.consecutiveWithdrawFailures = 0
1927                }
1928            }
1929            
1930            // Use liquid vault if needed
1931            if withdrawn.balance < amount {
1932                let remaining = amount - withdrawn.balance
1933                withdrawn.deposit(from: <- self.liquidVault.withdraw(amount: remaining))
1934            }
1935            
1936            // Only decrease totalStaked by amount withdrawn from yield source (not liquid vault fallback)
1937            self.totalStaked = self.totalStaked - amountFromYieldSource
1938            
1939            emit Withdrawn(poolID: self.poolID, receiverID: receiverID, amount: amount)
1940            return <- withdrawn
1941        }
1942        
1943        access(contract) fun processRewards() {
1944            let yieldBalance = self.config.yieldConnector.minimumAvailable()
1945            let availableYield = yieldBalance > self.totalStaked ? yieldBalance - self.totalStaked : 0.0
1946            
1947            if availableYield == 0.0 {
1948                return
1949            }
1950            
1951            let yieldRewards <- self.config.yieldConnector.withdrawAvailable(maxAmount: availableYield)
1952            let totalRewards = yieldRewards.balance
1953            let plan = self.config.distributionStrategy.calculateDistribution(totalAmount: totalRewards)
1954            
1955            if plan.savingsAmount > 0.0 {
1956                // Process rewards: collect from yield source, invest and distribute
1957                let savingsVault <- yieldRewards.withdraw(amount: plan.savingsAmount)
1958                self.distributeSavingsInterest(vault: <- savingsVault)
1959            }
1960            
1961            if plan.lotteryAmount > 0.0 {
1962                let lotteryVault <- yieldRewards.withdraw(amount: plan.lotteryAmount)
1963                self.lotteryDistributor.fundPrizePool(vault: <- lotteryVault)
1964                emit LotteryPrizePoolFunded(
1965                    poolID: self.poolID,
1966                    amount: plan.lotteryAmount,
1967                    source: "yield"
1968                )
1969            }
1970            
1971            if plan.treasuryAmount > 0.0 {
1972                let treasuryVault <- yieldRewards.withdraw(amount: plan.treasuryAmount)
1973                self.treasuryDistributor.deposit(vault: <- treasuryVault)
1974                emit TreasuryFunded(
1975                    poolID: self.poolID,
1976                    amount: plan.treasuryAmount,
1977                    source: "yield"
1978                )
1979            }
1980            
1981            destroy yieldRewards
1982            
1983            emit RewardsProcessed(
1984                poolID: self.poolID,
1985                totalAmount: totalRewards,
1986                savingsAmount: plan.savingsAmount,
1987                lotteryAmount: plan.lotteryAmount
1988            )
1989        }
1990        
1991        access(all) fun startDraw() {
1992            pre {
1993                self.emergencyState == PoolEmergencyState.Normal: "Draws disabled - pool state: ".concat(self.emergencyState.rawValue.toString())
1994                self.pendingDrawReceipt == nil: "Draw already in progress"
1995            }
1996            
1997            assert(self.canDrawNow(), message: "Not enough blocks since last draw")
1998            
1999            self.checkAndAutoTriggerEmergency()
2000            
2001            let timeWeightedStakes: {UInt64: UFix64} = {}
2002            for receiverID in self.receiverDeposits.keys {
2003                let deposit = self.receiverDeposits[receiverID]!
2004                let bonusWeight = self.getBonusWeight(receiverID: receiverID)
2005                let stake = deposit + bonusWeight
2006                
2007                if stake > 0.0 {
2008                    timeWeightedStakes[receiverID] = stake
2009                }
2010            }
2011            
2012            let prizeAmount = self.lotteryDistributor.getPrizePoolBalance()
2013            assert(prizeAmount > 0.0, message: "No prize pool funds")
2014            
2015            let randomRequest <- self.randomConsumer.requestRandomness()
2016            let receipt <- create PrizeDrawReceipt(
2017                prizeAmount: prizeAmount,
2018                request: <- randomRequest,
2019                timeWeightedStakes: timeWeightedStakes
2020            )
2021            emit PrizeDrawCommitted(
2022                poolID: self.poolID,
2023                prizeAmount: prizeAmount,
2024                commitBlock: receipt.getRequestBlock()!
2025            )
2026            
2027            self.pendingDrawReceipt <-! receipt
2028            self.lastDrawTimestamp = getCurrentBlock().timestamp
2029        }
2030        
2031        access(all) fun completeDraw() {
2032            pre {
2033                self.pendingDrawReceipt != nil: "No draw in progress"
2034            }
2035            
2036            let receipt <- self.pendingDrawReceipt <- nil
2037            let unwrappedReceipt <- receipt!
2038            let totalPrizeAmount = unwrappedReceipt.prizeAmount
2039            
2040            let timeWeightedStakes = unwrappedReceipt.getTimeWeightedStakes()
2041            
2042            let request <- unwrappedReceipt.popRequest()
2043            let randomNumber = self.randomConsumer.fulfillRandomRequest(<- request)
2044            destroy unwrappedReceipt
2045            
2046            let selectionResult = self.config.winnerSelectionStrategy.selectWinners(
2047                randomNumber: randomNumber,
2048                receiverDeposits: timeWeightedStakes,
2049                totalPrizeAmount: totalPrizeAmount
2050            )
2051            
2052            let winners = selectionResult.winners
2053            let prizeAmounts = selectionResult.amounts
2054            let nftIDsPerWinner = selectionResult.nftIDs
2055            
2056            if winners.length == 0 {
2057                emit PrizesAwarded(
2058                    poolID: self.poolID,
2059                    winners: [],
2060                    amounts: [],
2061                    round: self.lotteryDistributor.getPrizeRound()
2062                )
2063                log("⚠️  Draw completed with no depositors - ".concat(totalPrizeAmount.toString()).concat(" FLOW stays for next draw"))
2064                return
2065            }
2066            
2067            assert(winners.length == prizeAmounts.length, message: "Winners and prize amounts must match")
2068            assert(winners.length == nftIDsPerWinner.length, message: "Winners and NFT IDs must match")
2069            
2070            let currentRound = self.lotteryDistributor.getPrizeRound() + 1
2071            self.lotteryDistributor.setPrizeRound(round: currentRound)
2072            var totalAwarded: UFix64 = 0.0
2073            var i = 0
2074            while i < winners.length {
2075                let winnerID = winners[i]
2076                let prizeAmount = prizeAmounts[i]
2077                let nftIDsForWinner = nftIDsPerWinner[i]
2078                
2079                let prizeVault <- self.lotteryDistributor.awardPrize(
2080                    receiverID: winnerID,
2081                    amount: prizeAmount,
2082                    yieldSource: nil
2083                )
2084                
2085                let currentDeposit = self.receiverDeposits[winnerID] ?? 0.0
2086                let pendingSavings = self.savingsDistributor.claimInterest(receiverID: winnerID, deposit: currentDeposit)
2087                var newDeposit = currentDeposit
2088                
2089                if pendingSavings > 0.0 {
2090                    newDeposit = newDeposit + pendingSavings
2091                    self.totalDeposited = self.totalDeposited + pendingSavings
2092                    self.totalStaked = self.totalStaked + pendingSavings
2093                    
2094                    let currentSavings = self.receiverTotalEarnedSavings[winnerID] ?? 0.0
2095                    self.receiverTotalEarnedSavings[winnerID] = currentSavings + pendingSavings
2096                }
2097                
2098                newDeposit = newDeposit + prizeAmount
2099                self.receiverDeposits[winnerID] = newDeposit
2100                self.totalDeposited = self.totalDeposited + prizeAmount
2101                self.totalStaked = self.totalStaked + prizeAmount
2102                self.savingsDistributor.updateAfterBalanceChange(receiverID: winnerID, newDeposit: newDeposit)
2103                self.config.yieldConnector.depositCapacity(from: &prizeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
2104                destroy prizeVault
2105                
2106                let totalPrizes = self.receiverTotalEarnedPrizes[winnerID] ?? 0.0
2107                self.receiverTotalEarnedPrizes[winnerID] = totalPrizes + prizeAmount
2108                
2109                for nftID in nftIDsForWinner {
2110                    let availableNFTs = self.lotteryDistributor.getAvailableNFTPrizeIDs()
2111                    var nftFound = false
2112                    for availableID in availableNFTs {
2113                        if availableID == nftID {
2114                            nftFound = true
2115                            break
2116                        }
2117                    }
2118                    
2119                    if !nftFound {
2120                        continue
2121                    }
2122                    
2123                    let nft <- self.lotteryDistributor.withdrawNFTPrize(nftID: nftID)
2124                    let nftType = nft.getType().identifier
2125                    
2126                    // Store as pending claim - winner claims via separate transaction
2127                    self.lotteryDistributor.storePendingNFT(receiverID: winnerID, nft: <- nft)
2128                    
2129                    emit NFTPrizeStored(
2130                        poolID: self.poolID,
2131                        receiverID: winnerID,
2132                        nftID: nftID,
2133                        nftType: nftType,
2134                        reason: "Lottery win - round ".concat(currentRound.toString())
2135                    )
2136                    
2137                    emit NFTPrizeAwarded(
2138                        poolID: self.poolID,
2139                        receiverID: winnerID,
2140                        nftID: nftID,
2141                        nftType: nftType,
2142                        round: currentRound
2143                    )
2144                }
2145                
2146                totalAwarded = totalAwarded + prizeAmount
2147                i = i + 1
2148            }
2149            
2150            if let trackerCap = self.config.winnerTrackerCap {
2151                if let trackerRef = trackerCap.borrow() {
2152                    var idx = 0
2153                    while idx < winners.length {
2154                        trackerRef.recordWinner(
2155                            poolID: self.poolID,
2156                            round: currentRound,
2157                            winnerReceiverID: winners[idx],
2158                            amount: prizeAmounts[idx],
2159                            nftIDs: nftIDsPerWinner[idx]
2160                        )
2161                        idx = idx + 1
2162                    }
2163                }
2164            }
2165            
2166            emit PrizesAwarded(
2167                poolID: self.poolID,
2168                winners: winners,
2169                amounts: prizeAmounts,
2170                round: currentRound
2171            )
2172        }
2173        
2174        access(all) fun compoundSavingsInterest(receiverID: UInt64): UFix64 {
2175            pre {
2176                self.registeredReceivers[receiverID] == true: "Receiver not registered"
2177            }
2178            
2179            let currentDeposit = self.receiverDeposits[receiverID] ?? 0.0
2180            let pending = self.savingsDistributor.claimInterest(receiverID: receiverID, deposit: currentDeposit)
2181            
2182            if pending > 0.0 {
2183                // Add pending savings to deposit
2184                let newDeposit = currentDeposit + pending
2185                self.receiverDeposits[receiverID] = newDeposit
2186                self.totalDeposited = self.totalDeposited + pending
2187                self.totalStaked = self.totalStaked + pending
2188                
2189                // Update savings distributor to track the new balance
2190                self.savingsDistributor.updateAfterBalanceChange(receiverID: receiverID, newDeposit: newDeposit)
2191                
2192                // Track historical savings for user reference
2193                let currentSavings = self.receiverTotalEarnedSavings[receiverID] ?? 0.0
2194                self.receiverTotalEarnedSavings[receiverID] = currentSavings + pending
2195                
2196                emit SavingsInterestCompounded(poolID: self.poolID, receiverID: receiverID, amount: pending)
2197            }
2198            
2199            return pending
2200        }
2201        
2202        // Admin functions for strategy updates
2203        access(contract) fun getDistributionStrategyName(): String {
2204            return self.config.distributionStrategy.getStrategyName()
2205        }
2206        
2207        access(contract) fun setDistributionStrategy(strategy: {DistributionStrategy}) {
2208            self.config.setDistributionStrategy(strategy: strategy)
2209        }
2210        
2211        access(contract) fun getWinnerSelectionStrategyName(): String {
2212            return self.config.winnerSelectionStrategy.getStrategyName()
2213        }
2214        
2215        access(contract) fun setWinnerSelectionStrategy(strategy: {WinnerSelectionStrategy}) {
2216            self.config.setWinnerSelectionStrategy(strategy: strategy)
2217        }
2218        
2219        access(all) fun hasWinnerTracker(): Bool {
2220            return self.config.winnerTrackerCap != nil
2221        }
2222        
2223        access(contract) fun setWinnerTrackerCap(cap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?) {
2224            self.config.setWinnerTrackerCap(cap: cap)
2225        }
2226        
2227        access(contract) fun setDrawIntervalSeconds(interval: UFix64) {
2228            assert(!self.isDrawInProgress(), message: "Cannot change draw interval during an active draw")
2229            self.config.setDrawIntervalSeconds(interval: interval)
2230        }
2231        
2232        access(contract) fun setMinimumDeposit(minimum: UFix64) {
2233            self.config.setMinimumDeposit(minimum: minimum)
2234        }
2235        
2236        access(contract) fun setBonusWeight(receiverID: UInt64, bonusWeight: UFix64, reason: String, setBy: Address) {
2237            let timestamp = getCurrentBlock().timestamp
2238            let record = BonusWeightRecord(bonusWeight: bonusWeight, reason: reason, addedBy: setBy)
2239            self.receiverBonusWeights[receiverID] = record
2240            
2241            emit BonusLotteryWeightSet(
2242                poolID: self.poolID,
2243                receiverID: receiverID,
2244                bonusWeight: bonusWeight,
2245                reason: reason,
2246                setBy: setBy,
2247                timestamp: timestamp
2248            )
2249        }
2250        
2251        access(contract) fun addBonusWeight(receiverID: UInt64, additionalWeight: UFix64, reason: String, addedBy: Address) {
2252            let timestamp = getCurrentBlock().timestamp
2253            let currentBonus = self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0
2254            let newTotalBonus = currentBonus + additionalWeight
2255            
2256            let record = BonusWeightRecord(bonusWeight: newTotalBonus, reason: reason, addedBy: addedBy)
2257            self.receiverBonusWeights[receiverID] = record
2258            
2259            emit BonusLotteryWeightAdded(
2260                poolID: self.poolID,
2261                receiverID: receiverID,
2262                additionalWeight: additionalWeight,
2263                newTotalBonus: newTotalBonus,
2264                reason: reason,
2265                addedBy: addedBy,
2266                timestamp: timestamp
2267            )
2268        }
2269        
2270        access(contract) fun removeBonusWeight(receiverID: UInt64, removedBy: Address) {
2271            let timestamp = getCurrentBlock().timestamp
2272            let previousBonus = self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0
2273            
2274            let _ = self.receiverBonusWeights.remove(key: receiverID)
2275            
2276            emit BonusLotteryWeightRemoved(
2277                poolID: self.poolID,
2278                receiverID: receiverID,
2279                previousBonus: previousBonus,
2280                removedBy: removedBy,
2281                timestamp: timestamp
2282            )
2283        }
2284        
2285        access(all) fun getBonusWeight(receiverID: UInt64): UFix64 {
2286            return self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0
2287        }
2288        
2289        access(all) fun getBonusWeightRecord(receiverID: UInt64): BonusWeightRecord? {
2290            return self.receiverBonusWeights[receiverID]
2291        }
2292        
2293        access(all) fun getAllBonusWeightReceivers(): [UInt64] {
2294            return self.receiverBonusWeights.keys
2295        }
2296        
2297        // NFT Prize Management (delegated to LotteryDistributor)
2298        access(contract) fun depositNFTPrize(nft: @{NonFungibleToken.NFT}) {
2299            self.lotteryDistributor.depositNFTPrize(nft: <- nft)
2300        }
2301        
2302        access(contract) fun withdrawNFTPrize(nftID: UInt64): @{NonFungibleToken.NFT} {
2303            return <- self.lotteryDistributor.withdrawNFTPrize(nftID: nftID)
2304        }
2305        
2306        access(all) fun getAvailableNFTPrizeIDs(): [UInt64] {
2307            return self.lotteryDistributor.getAvailableNFTPrizeIDs()
2308        }
2309        
2310        access(all) fun borrowAvailableNFTPrize(nftID: UInt64): &{NonFungibleToken.NFT}? {
2311            return self.lotteryDistributor.borrowNFTPrize(nftID: nftID)
2312        }
2313        
2314        access(all) fun getPendingNFTCount(receiverID: UInt64): Int {
2315            return self.lotteryDistributor.getPendingNFTCount(receiverID: receiverID)
2316        }
2317        
2318        access(all) fun getPendingNFTIDs(receiverID: UInt64): [UInt64] {
2319            return self.lotteryDistributor.getPendingNFTIDs(receiverID: receiverID)
2320        }
2321        
2322        // Note: Pending NFTs cannot be borrowed directly due to Cadence limitations
2323        // Use getPendingNFTIDs() to see what's pending, then claim and view in wallet
2324        
2325        access(all) fun claimPendingNFT(receiverID: UInt64, nftIndex: Int): @{NonFungibleToken.NFT} {
2326            let nft <- self.lotteryDistributor.claimPendingNFT(receiverID: receiverID, nftIndex: nftIndex)
2327            let nftType = nft.getType().identifier
2328            
2329            emit NFTPrizeClaimed(
2330                poolID: self.poolID,
2331                receiverID: receiverID,
2332                nftID: nft.uuid,
2333                nftType: nftType
2334            )
2335            
2336            return <- nft
2337        }
2338        
2339        access(all) fun canDrawNow(): Bool {
2340            return (getCurrentBlock().timestamp - self.lastDrawTimestamp) >= self.config.drawIntervalSeconds
2341        }
2342        
2343        access(all) fun getReceiverDeposit(receiverID: UInt64): UFix64 {
2344            return self.receiverDeposits[receiverID] ?? 0.0
2345        }
2346        
2347        access(all) fun getReceiverTotalEarnedSavings(receiverID: UInt64): UFix64 {
2348            return self.receiverTotalEarnedSavings[receiverID] ?? 0.0
2349        }
2350        
2351        access(all) fun getReceiverTotalEarnedPrizes(receiverID: UInt64): UFix64 {
2352            return self.receiverTotalEarnedPrizes[receiverID] ?? 0.0
2353        }
2354        
2355        access(all) fun getPendingSavingsInterest(receiverID: UInt64): UFix64 {
2356            let deposit = self.receiverDeposits[receiverID] ?? 0.0
2357            return self.savingsDistributor.calculatePendingInterest(receiverID: receiverID, deposit: deposit)
2358        }
2359        
2360        access(all) fun isReceiverRegistered(receiverID: UInt64): Bool {
2361            return self.registeredReceivers[receiverID] == true
2362        }
2363        
2364        access(all) fun getRegisteredReceiverIDs(): [UInt64] {
2365            return self.registeredReceivers.keys
2366        }
2367        
2368        access(all) fun isDrawInProgress(): Bool {
2369            return self.pendingDrawReceipt != nil
2370        }
2371        
2372        access(all) fun getConfig(): PoolConfig {
2373            return self.config
2374        }
2375        
2376        access(all) fun getLiquidBalance(): UFix64 {
2377            return self.liquidVault.balance
2378        }
2379        
2380        access(all) fun getSavingsPoolBalance(): UFix64 {
2381            return self.savingsDistributor.getInterestVaultBalance()
2382        }
2383        
2384        access(all) fun getTotalSavingsDistributed(): UFix64 {
2385            return self.savingsDistributor.getTotalDistributed()
2386        }
2387        
2388        /// Calculate current amount of savings generating yield in the yield source
2389        /// This is the difference between totalStaked (all funds in yield source) and totalDeposited (user deposits)
2390        access(all) fun getCurrentReinvestedSavings(): UFix64 {
2391            if self.totalStaked > self.totalDeposited {
2392                return self.totalStaked - self.totalDeposited
2393            }
2394            return 0.0
2395        }
2396        
2397        /// Get available yield rewards ready to be collected
2398        /// This is the difference between what's in the yield source and what we've tracked as staked
2399        access(all) fun getAvailableYieldRewards(): UFix64 {
2400            let yieldSource = &self.config.yieldConnector as &{DeFiActions.Source}
2401            let available = yieldSource.minimumAvailable()
2402            if available > self.totalStaked {
2403                return available - self.totalStaked
2404            }
2405            return 0.0
2406        }
2407        
2408        access(all) fun getLotteryPoolBalance(): UFix64 {
2409            return self.lotteryDistributor.getPrizePoolBalance()
2410        }
2411        
2412        access(all) fun getTreasuryBalance(): UFix64 {
2413            return self.treasuryDistributor.getBalance()
2414        }
2415        
2416        access(all) fun getTreasuryStats(): {String: UFix64} {
2417            return {
2418                "balance": self.treasuryDistributor.getBalance(),
2419                "totalCollected": self.treasuryDistributor.getTotalCollected(),
2420                "totalWithdrawn": self.treasuryDistributor.getTotalWithdrawn()
2421            }
2422        }
2423        
2424        access(all) fun getTreasuryWithdrawalHistory(): [{String: AnyStruct}] {
2425            return self.treasuryDistributor.getWithdrawalHistory()
2426        }
2427        
2428        access(contract) fun withdrawTreasury(
2429            amount: UFix64,
2430            withdrawnBy: Address,
2431            purpose: String
2432        ): @{FungibleToken.Vault} {
2433            return <- self.treasuryDistributor.withdraw(
2434                amount: amount,
2435                withdrawnBy: withdrawnBy,
2436                purpose: purpose
2437            )
2438        }
2439        
2440    }
2441    
2442    // Pool Position Collection
2443    
2444    access(all) struct PoolBalance {
2445        access(all) let deposits: UFix64  // Total deposited (includes auto-compounded savings & prizes)
2446        access(all) let totalEarnedSavings: UFix64  // Historical: total savings earned (auto-compounded)
2447        access(all) let totalEarnedPrizes: UFix64  // Historical: total prizes earned (auto-compounded)
2448        access(all) let pendingSavings: UFix64  // Savings not yet compounded (ready to compound)
2449        access(all) let totalBalance: UFix64  // deposits + pendingSavings (total in yield sink)
2450        
2451        init(deposits: UFix64, totalEarnedSavings: UFix64, totalEarnedPrizes: UFix64, pendingSavings: UFix64) {
2452            self.deposits = deposits
2453            self.totalEarnedSavings = totalEarnedSavings
2454            self.totalEarnedPrizes = totalEarnedPrizes
2455            self.pendingSavings = pendingSavings
2456            self.totalBalance = deposits + pendingSavings
2457        }
2458    }
2459    
2460    access(all) resource interface PoolPositionCollectionPublic {
2461        access(all) fun getRegisteredPoolIDs(): [UInt64]
2462        access(all) fun isRegisteredWithPool(poolID: UInt64): Bool
2463        access(all) fun deposit(poolID: UInt64, from: @{FungibleToken.Vault})
2464        access(all) fun withdraw(poolID: UInt64, amount: UFix64): @{FungibleToken.Vault}
2465        access(all) fun compoundSavingsInterest(poolID: UInt64): UFix64
2466        access(all) fun getPoolBalance(poolID: UInt64): PoolBalance
2467    }
2468    
2469    access(all) resource PoolPositionCollection: PoolPositionCollectionPublic {
2470        access(self) let registeredPools: {UInt64: Bool}
2471        
2472        init() {
2473            self.registeredPools = {}
2474        }
2475        
2476        access(self) fun registerWithPool(poolID: UInt64) {
2477            pre {
2478                self.registeredPools[poolID] == nil: "Already registered"
2479            }
2480            
2481            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
2482                ?? panic("Pool does not exist")
2483            
2484            poolRef.registerReceiver(receiverID: self.uuid)
2485            self.registeredPools[poolID] = true
2486        }
2487        
2488        access(all) fun getRegisteredPoolIDs(): [UInt64] {
2489            return self.registeredPools.keys
2490        }
2491        
2492        access(all) fun isRegisteredWithPool(poolID: UInt64): Bool {
2493            return self.registeredPools[poolID] == true
2494        }
2495        
2496        access(all) fun deposit(poolID: UInt64, from: @{FungibleToken.Vault}) {
2497            if self.registeredPools[poolID] == nil {
2498                self.registerWithPool(poolID: poolID)
2499            }
2500            
2501            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
2502                ?? panic("Cannot borrow pool")
2503            
2504            poolRef.deposit(from: <- from, receiverID: self.uuid)
2505        }
2506        
2507        access(all) fun withdraw(poolID: UInt64, amount: UFix64): @{FungibleToken.Vault} {
2508            pre {
2509                self.registeredPools[poolID] == true: "Not registered with pool"
2510            }
2511            
2512            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
2513                ?? panic("Cannot borrow pool")
2514            
2515            return <- poolRef.withdraw(amount: amount, receiverID: self.uuid)
2516        }
2517        
2518        access(all) fun compoundSavingsInterest(poolID: UInt64): UFix64 {
2519            pre {
2520                self.registeredPools[poolID] == true: "Not registered with pool"
2521            }
2522            
2523            let poolRef = PrizeVaultModular.borrowPoolAuth(poolID: poolID)
2524                ?? panic("Cannot borrow pool")
2525            
2526            return poolRef.compoundSavingsInterest(receiverID: self.uuid)
2527        }
2528        
2529        access(all) fun getPoolBalance(poolID: UInt64): PoolBalance {
2530            if self.registeredPools[poolID] == nil {
2531                return PoolBalance(deposits: 0.0, totalEarnedSavings: 0.0, totalEarnedPrizes: 0.0, pendingSavings: 0.0)
2532            }
2533            
2534            let poolRef = PrizeVaultModular.borrowPool(poolID: poolID)
2535            if poolRef == nil {
2536                return PoolBalance(deposits: 0.0, totalEarnedSavings: 0.0, totalEarnedPrizes: 0.0, pendingSavings: 0.0)
2537            }
2538            
2539            return PoolBalance(
2540                deposits: poolRef!.getReceiverDeposit(receiverID: self.uuid),
2541                totalEarnedSavings: poolRef!.getReceiverTotalEarnedSavings(receiverID: self.uuid),
2542                totalEarnedPrizes: poolRef!.getReceiverTotalEarnedPrizes(receiverID: self.uuid),
2543                pendingSavings: poolRef!.getPendingSavingsInterest(receiverID: self.uuid)
2544            )
2545        }
2546    }
2547    
2548    // Contract Functions
2549    
2550    access(contract) fun createPool(
2551        config: PoolConfig,
2552        emergencyConfig: EmergencyConfig?,
2553        fundingPolicy: FundingPolicy?
2554    ): UInt64 {
2555        let emptyVault <- DeFiActionsUtils.getEmptyVault(config.assetType)
2556        let pool <- create Pool(
2557            config: config, 
2558            initialVault: <- emptyVault,
2559            emergencyConfig: emergencyConfig,
2560            fundingPolicy: fundingPolicy
2561        )
2562        
2563        let poolID = self.nextPoolID
2564        self.nextPoolID = self.nextPoolID + 1
2565        
2566        pool.setPoolID(id: poolID)
2567        emit PoolCreated(
2568            poolID: poolID,
2569            assetType: config.assetType.identifier,
2570            strategy: config.distributionStrategy.getStrategyName()
2571        )
2572        
2573        self.pools[poolID] <-! pool
2574        return poolID
2575    }
2576    
2577    access(all) view fun borrowPool(poolID: UInt64): &Pool? {
2578        return &self.pools[poolID]
2579    }
2580    
2581    /// Returns authorized pool reference - restricted to contract only for security
2582    /// Only Admin resource and internal contract functions can access this
2583    access(contract) fun borrowPoolAuth(poolID: UInt64): &Pool? {
2584        return &self.pools[poolID]
2585    }
2586    
2587    access(all) view fun getAllPoolIDs(): [UInt64] {
2588        return self.pools.keys
2589    }
2590    
2591    access(all) fun createPoolPositionCollection(): @PoolPositionCollection {
2592        return <- create PoolPositionCollection()
2593    }
2594    
2595    init() {
2596        self.PoolPositionCollectionStoragePath = /storage/PrizeVaultModularCollection
2597        self.PoolPositionCollectionPublicPath = /public/PrizeVaultModularCollection
2598        
2599        self.AdminStoragePath = /storage/PrizeVaultModularAdmin
2600        self.AdminPublicPath = /public/PrizeVaultModularAdmin
2601        
2602        self.pools <- {}
2603        self.nextPoolID = 0
2604        
2605        let admin <- create Admin()
2606        self.account.storage.save(<-admin, to: self.AdminStoragePath)
2607    }
2608}