Smart Contract

PrizeSavings

A.a092c4aab33daeda.PrizeSavings

Valid From

142,806,704

Deployed

1w ago
Feb 15, 2026, 11:00:46 PM UTC

Dependents

19 imports
1/*
2PrizeSavings - Prize-Linked Savings Protocol
3
4No-loss lottery where users deposit tokens to earn guaranteed savings interest and lottery prizes.
5Rewards auto-compound into deposits via ERC4626-style shares model.
6
7Architecture:
8- ERC4626-style shares with virtual offset protection (inflation attack resistant)
9- TWAB (time-weighted average balance) using balance-seconds for fair lottery weighting
10- On-chain randomness via Flow's RandomConsumer
11- Modular yield sources via DeFi Actions interface
12- Configurable distribution strategies (savings/lottery/treasury split)
13- Pluggable winner selection (weighted single, multi-winner, fixed tiers)
14- Resource-based position ownership via PoolPositionCollection
15- Emergency mode with auto-recovery and health monitoring
16- NFT prize support with pending claims
17- Direct funding for external sponsors
18- Bonus lottery weights for promotions
19- Winner tracking integration for leaderboards
20
21Lottery Fairness:
22- Uses balance-seconds (current_balance × time) for lottery weighting
23- Balance = shares × share_price, so yield automatically counts toward lottery odds
24- Users who earn more yield have proportionally higher lottery chances
25- Rewards commitment: longer deposits = more accumulated balance-seconds
26
27Core Components:
28- SavingsDistributor: Shares vault with balance-seconds tracking for lottery weights
29- LotteryDistributor: Prize pool, NFT prizes, and draw execution
30- Pool: Deposits, withdrawals, yield processing, and prize draws
31- PoolPositionCollection: User's resource for interacting with pools
32- Admin: Pool configuration and emergency operations
33*/
34
35import FungibleToken from 0xf233dcee88fe0abe
36import NonFungibleToken from 0x1d7e57aa55817448
37import RandomConsumer from 0xa092c4aab33daeda
38import DeFiActions from 0x92195d814edf9cb0
39import DeFiActionsUtils from 0x92195d814edf9cb0
40import PrizeWinnerTracker from 0xa092c4aab33daeda
41import Xorshift128plus from 0xa092c4aab33daeda
42
43access(all) contract PrizeSavings {
44    // Admin entitlements
45    access(all) entitlement ConfigOps
46    access(all) entitlement CriticalOps
47    access(all) entitlement OwnerOnly
48    
49    // User collection entitlement - protects deposit, withdraw, and claim operations
50    access(all) entitlement PositionOps
51    
52    /// Virtual offset constants for ERC4626 inflation attack protection.
53    /// These create "dead" shares/assets that prevent share price manipulation.
54    /// Using 0.0001 to minimize user dilution (~0.0001%) while maintaining security.
55    access(all) let VIRTUAL_SHARES: UFix64
56    access(all) let VIRTUAL_ASSETS: UFix64
57    
58    access(all) let PoolPositionCollectionStoragePath: StoragePath
59    access(all) let PoolPositionCollectionPublicPath: PublicPath
60    
61    access(all) event PoolCreated(poolID: UInt64, assetType: String, strategy: String)
62    access(all) event Deposited(poolID: UInt64, receiverID: UInt64, userAddress: Address, amount: UFix64)
63    access(all) event Withdrawn(poolID: UInt64, receiverID: UInt64, userAddress: Address, requestedAmount: UFix64, actualAmount: UFix64)
64    
65    access(all) event RewardsProcessed(poolID: UInt64, totalAmount: UFix64, savingsAmount: UFix64, lotteryAmount: UFix64)
66    
67    access(all) event SavingsYieldAccrued(poolID: UInt64, amount: UFix64)
68    access(all) event SavingsInterestCompounded(poolID: UInt64, receiverID: UInt64, userAddress: Address, amount: UFix64)
69    access(all) event SavingsInterestCompoundedBatch(poolID: UInt64, userCount: Int, totalAmount: UFix64, avgAmount: UFix64)
70    access(all) event SavingsRoundingDustToTreasury(poolID: UInt64, amount: UFix64)
71    
72    access(all) event PrizeDrawCommitted(poolID: UInt64, prizeAmount: UFix64, commitBlock: UInt64)
73    access(all) event PrizesAwarded(poolID: UInt64, winners: [UInt64], winnerAddresses: [Address], amounts: [UFix64], round: UInt64)
74    access(all) event LotteryPrizePoolFunded(poolID: UInt64, amount: UFix64, source: String)
75    access(all) event NewEpochStarted(poolID: UInt64, epochID: UInt64, startTime: UFix64)
76    
77    access(all) event DistributionStrategyUpdated(poolID: UInt64, oldStrategy: String, newStrategy: String, updatedBy: Address)
78    access(all) event WinnerSelectionStrategyUpdated(poolID: UInt64, oldStrategy: String, newStrategy: String, updatedBy: Address)
79    access(all) event WinnerTrackerUpdated(poolID: UInt64, hasOldTracker: Bool, hasNewTracker: Bool, updatedBy: Address)
80    access(all) event DrawIntervalUpdated(poolID: UInt64, oldInterval: UFix64, newInterval: UFix64, updatedBy: Address)
81    access(all) event MinimumDepositUpdated(poolID: UInt64, oldMinimum: UFix64, newMinimum: UFix64, updatedBy: Address)
82    access(all) event PoolCreatedByAdmin(poolID: UInt64, assetType: String, strategy: String, createdBy: Address)
83    
84    access(all) event PoolPaused(poolID: UInt64, pausedBy: Address, reason: String)
85    access(all) event PoolUnpaused(poolID: UInt64, unpausedBy: Address)
86    access(all) event TreasuryFunded(poolID: UInt64, amount: UFix64, source: String)
87    access(all) event TreasuryRecipientUpdated(poolID: UInt64, newRecipient: Address?, updatedBy: Address)
88    access(all) event TreasuryForwarded(poolID: UInt64, amount: UFix64, recipient: Address)
89    
90    access(all) event BonusLotteryWeightSet(poolID: UInt64, receiverID: UInt64, userAddress: Address, bonusWeight: UFix64, reason: String, setBy: Address, timestamp: UFix64)
91    access(all) event BonusLotteryWeightAdded(poolID: UInt64, receiverID: UInt64, userAddress: Address, additionalWeight: UFix64, newTotalBonus: UFix64, reason: String, addedBy: Address, timestamp: UFix64)
92    access(all) event BonusLotteryWeightRemoved(poolID: UInt64, receiverID: UInt64, userAddress: Address, previousBonus: UFix64, removedBy: Address, timestamp: UFix64)
93    
94    access(all) event NFTPrizeDeposited(poolID: UInt64, nftID: UInt64, nftType: String, depositedBy: Address)
95    access(all) event NFTPrizeAwarded(poolID: UInt64, receiverID: UInt64, userAddress: Address, nftID: UInt64, nftType: String, round: UInt64)
96    access(all) event NFTPrizeStored(poolID: UInt64, receiverID: UInt64, userAddress: Address, nftID: UInt64, nftType: String, reason: String)
97    access(all) event NFTPrizeClaimed(poolID: UInt64, receiverID: UInt64, userAddress: Address, nftID: UInt64, nftType: String)
98    access(all) event NFTPrizeWithdrawn(poolID: UInt64, nftID: UInt64, nftType: String, withdrawnBy: Address)
99    
100    access(all) event PoolEmergencyEnabled(poolID: UInt64, reason: String, enabledBy: Address, timestamp: UFix64)
101    access(all) event PoolEmergencyDisabled(poolID: UInt64, disabledBy: Address, timestamp: UFix64)
102    access(all) event PoolPartialModeEnabled(poolID: UInt64, reason: String, setBy: Address, timestamp: UFix64)
103    access(all) event EmergencyModeAutoTriggered(poolID: UInt64, reason: String, healthScore: UFix64, timestamp: UFix64)
104    access(all) event EmergencyModeAutoRecovered(poolID: UInt64, reason: String, healthScore: UFix64?, duration: UFix64?, timestamp: UFix64)
105    access(all) event EmergencyConfigUpdated(poolID: UInt64, updatedBy: Address)
106    access(all) event WithdrawalFailure(poolID: UInt64, receiverID: UInt64, userAddress: Address, amount: UFix64, consecutiveFailures: Int, yieldAvailable: UFix64)
107    
108    access(all) event DirectFundingReceived(poolID: UInt64, destination: UInt8, destinationName: String, amount: UFix64, sponsor: Address, purpose: String, metadata: {String: String})
109    
110    access(self) var pools: @{UInt64: Pool}
111    access(self) var nextPoolID: UInt64
112    
113    access(all) enum PoolEmergencyState: UInt8 {
114        access(all) case Normal
115        access(all) case Paused
116        access(all) case EmergencyMode
117        access(all) case PartialMode
118    }
119    
120    access(all) enum PoolFundingDestination: UInt8 {
121        access(all) case Savings
122        access(all) case Lottery
123    }
124    
125    access(all) struct BonusWeightRecord {
126        access(all) let bonusWeight: UFix64
127        access(all) let reason: String
128        access(all) let addedAt: UFix64
129        access(all) let addedBy: Address
130        
131        access(contract) init(bonusWeight: UFix64, reason: String, addedBy: Address) {
132            self.bonusWeight = bonusWeight
133            self.reason = reason
134            self.addedAt = getCurrentBlock().timestamp
135            self.addedBy = addedBy
136        }
137    }
138    
139    access(all) struct EmergencyConfig {
140        access(all) let maxEmergencyDuration: UFix64?
141        access(all) let autoRecoveryEnabled: Bool
142        access(all) let minYieldSourceHealth: UFix64
143        access(all) let maxWithdrawFailures: Int
144        access(all) let partialModeDepositLimit: UFix64?
145        access(all) let minBalanceThreshold: UFix64
146        access(all) let minRecoveryHealth: UFix64
147        
148        init(
149            maxEmergencyDuration: UFix64?,
150            autoRecoveryEnabled: Bool,
151            minYieldSourceHealth: UFix64,
152            maxWithdrawFailures: Int,
153            partialModeDepositLimit: UFix64?,
154            minBalanceThreshold: UFix64,
155            minRecoveryHealth: UFix64?
156        ) {
157            pre {
158                minYieldSourceHealth >= 0.0 && minYieldSourceHealth <= 1.0: "Health must be between 0.0 and 1.0"
159                maxWithdrawFailures > 0: "Must allow at least 1 withdrawal failure"
160                minBalanceThreshold >= 0.8 && minBalanceThreshold <= 1.0: "Balance threshold must be between 0.8 and 1.0"
161                (minRecoveryHealth ?? 0.5) >= 0.0 && (minRecoveryHealth ?? 0.5) <= 1.0: "minRecoveryHealth must be between 0.0 and 1.0"
162            }
163            self.maxEmergencyDuration = maxEmergencyDuration
164            self.autoRecoveryEnabled = autoRecoveryEnabled
165            self.minYieldSourceHealth = minYieldSourceHealth
166            self.maxWithdrawFailures = maxWithdrawFailures
167            self.partialModeDepositLimit = partialModeDepositLimit
168            self.minBalanceThreshold = minBalanceThreshold
169            self.minRecoveryHealth = minRecoveryHealth ?? 0.5
170        }
171    }
172    
173    access(all) struct FundingPolicy {
174        access(all) let maxDirectLottery: UFix64?
175        access(all) let maxDirectSavings: UFix64?
176        access(all) var totalDirectLottery: UFix64
177        access(all) var totalDirectSavings: UFix64
178        
179        init(maxDirectLottery: UFix64?, maxDirectSavings: UFix64?) {
180            self.maxDirectLottery = maxDirectLottery
181            self.maxDirectSavings = maxDirectSavings
182            self.totalDirectLottery = 0.0
183            self.totalDirectSavings = 0.0
184        }
185        
186        access(contract) fun recordDirectFunding(destination: PoolFundingDestination, amount: UFix64) {
187            switch destination {
188                case PoolFundingDestination.Lottery:
189                    self.totalDirectLottery = self.totalDirectLottery + amount
190                    if let maxDirectLottery = self.maxDirectLottery {
191                        assert(self.totalDirectLottery <= maxDirectLottery, message: "Direct lottery funding limit exceeded")
192                    }
193                case PoolFundingDestination.Savings:
194                    self.totalDirectSavings = self.totalDirectSavings + amount
195                    if let maxDirectSavings = self.maxDirectSavings {
196                        assert(self.totalDirectSavings <= maxDirectSavings, message: "Direct savings funding limit exceeded")
197                    }
198            }
199        }
200    }
201    
202    access(all) fun createDefaultEmergencyConfig(): EmergencyConfig {
203        return EmergencyConfig(
204            maxEmergencyDuration: 86400.0,
205            autoRecoveryEnabled: true,
206            minYieldSourceHealth: 0.5,
207            maxWithdrawFailures: 3,
208            partialModeDepositLimit: 100.0,
209            minBalanceThreshold: 0.95,
210            minRecoveryHealth: 0.5
211        )
212    }
213    
214    access(all) fun createDefaultFundingPolicy(): FundingPolicy {
215        return FundingPolicy(
216            maxDirectLottery: nil,
217            maxDirectSavings: nil
218        )
219    }
220    
221    access(all) resource Admin {
222        access(self) var metadata: {String: {String: AnyStruct}}
223        
224        access(contract) init() {
225            self.metadata = {}
226        }
227
228        access(CriticalOps) fun updatePoolDistributionStrategy(
229            poolID: UInt64,
230            newStrategy: {DistributionStrategy},
231            updatedBy: Address
232        ) {
233            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
234                ?? panic("Pool does not exist")
235            
236            let oldStrategyName = poolRef.getDistributionStrategyName()
237            poolRef.setDistributionStrategy(strategy: newStrategy)
238            let newStrategyName = newStrategy.getStrategyName()
239            
240            emit DistributionStrategyUpdated(
241                poolID: poolID,
242                oldStrategy: oldStrategyName,
243                newStrategy: newStrategyName,
244                updatedBy: updatedBy
245            )
246        }
247        
248        access(CriticalOps) fun updatePoolWinnerSelectionStrategy(
249            poolID: UInt64,
250            newStrategy: {WinnerSelectionStrategy},
251            updatedBy: Address
252        ) {
253            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
254                ?? panic("Pool does not exist")
255            
256            let oldStrategyName = poolRef.getWinnerSelectionStrategyName()
257            poolRef.setWinnerSelectionStrategy(strategy: newStrategy)
258            let newStrategyName = newStrategy.getStrategyName()
259            
260            emit WinnerSelectionStrategyUpdated(
261                poolID: poolID,
262                oldStrategy: oldStrategyName,
263                newStrategy: newStrategyName,
264                updatedBy: updatedBy
265            )
266        }
267        
268        access(ConfigOps) fun updatePoolWinnerTracker(
269            poolID: UInt64,
270            newTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?,
271            updatedBy: Address
272        ) {
273            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
274                ?? panic("Pool does not exist")
275            
276            let hasOldTracker = poolRef.hasWinnerTracker()
277            poolRef.setWinnerTrackerCap(cap: newTrackerCap)
278            let hasNewTracker = newTrackerCap != nil
279            
280            emit WinnerTrackerUpdated(
281                poolID: poolID,
282                hasOldTracker: hasOldTracker,
283                hasNewTracker: hasNewTracker,
284                updatedBy: updatedBy
285            )
286        }
287        
288        access(ConfigOps) fun updatePoolDrawInterval(
289            poolID: UInt64,
290            newInterval: UFix64,
291            updatedBy: Address
292        ) {
293            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
294                ?? panic("Pool does not exist")
295            
296            let oldInterval = poolRef.getConfig().drawIntervalSeconds
297            poolRef.setDrawIntervalSeconds(interval: newInterval)
298            
299            emit DrawIntervalUpdated(
300                poolID: poolID,
301                oldInterval: oldInterval,
302                newInterval: newInterval,
303                updatedBy: updatedBy
304            )
305        }
306        
307        access(ConfigOps) fun updatePoolMinimumDeposit(
308            poolID: UInt64,
309            newMinimum: UFix64,
310            updatedBy: Address
311        ) {
312            pre {
313                newMinimum >= 0.0: "Minimum deposit cannot be negative"
314            }
315            
316            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
317                ?? panic("Pool does not exist")
318            
319            let oldMinimum = poolRef.getConfig().minimumDeposit
320            poolRef.setMinimumDeposit(minimum: newMinimum)
321            
322            emit MinimumDepositUpdated(
323                poolID: poolID,
324                oldMinimum: oldMinimum,
325                newMinimum: newMinimum,
326                updatedBy: updatedBy
327            )
328        }
329        
330        access(CriticalOps) fun enableEmergencyMode(poolID: UInt64, reason: String, enabledBy: Address) {
331            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
332                ?? panic("Pool does not exist")
333            poolRef.setEmergencyMode(reason: reason)
334            emit PoolEmergencyEnabled(poolID: poolID, reason: reason, enabledBy: enabledBy, timestamp: getCurrentBlock().timestamp)
335        }
336        
337        access(CriticalOps) fun disableEmergencyMode(poolID: UInt64, disabledBy: Address) {
338            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
339                ?? panic("Pool does not exist")
340            poolRef.clearEmergencyMode()
341            emit PoolEmergencyDisabled(poolID: poolID, disabledBy: disabledBy, timestamp: getCurrentBlock().timestamp)
342        }
343        
344        access(CriticalOps) fun setEmergencyPartialMode(poolID: UInt64, reason: String, setBy: Address) {
345            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
346                ?? panic("Pool does not exist")
347            poolRef.setPartialMode(reason: reason)
348            emit PoolPartialModeEnabled(poolID: poolID, reason: reason, setBy: setBy, timestamp: getCurrentBlock().timestamp)
349        }
350        
351        access(CriticalOps) fun updateEmergencyConfig(poolID: UInt64, newConfig: EmergencyConfig, updatedBy: Address) {
352            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
353                ?? panic("Pool does not exist")
354            poolRef.setEmergencyConfig(config: newConfig)
355            emit EmergencyConfigUpdated(poolID: poolID, updatedBy: updatedBy)
356        }
357        
358        access(CriticalOps) fun fundPoolDirect(
359            poolID: UInt64,
360            destination: PoolFundingDestination,
361            from: @{FungibleToken.Vault},
362            sponsor: Address,
363            purpose: String,
364            metadata: {String: String}?
365        ) {
366            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
367                ?? panic("Pool does not exist")
368            let amount = from.balance
369            poolRef.fundDirectInternal(destination: destination, from: <- from, sponsor: sponsor, purpose: purpose, metadata: metadata ?? {})
370            
371            emit DirectFundingReceived(
372                poolID: poolID,
373                destination: destination.rawValue,
374                destinationName: self.getDestinationName(destination),
375                amount: amount,
376                sponsor: sponsor,
377                purpose: purpose,
378                metadata: metadata ?? {}
379            )
380        }
381        
382        access(self) fun getDestinationName(_ destination: PoolFundingDestination): String {
383            switch destination {
384                case PoolFundingDestination.Savings: return "Savings"
385                case PoolFundingDestination.Lottery: return "Lottery"
386                default: return "Unknown"
387            }
388        }
389        
390        access(CriticalOps) fun createPool(
391            config: PoolConfig,
392            emergencyConfig: EmergencyConfig?,
393            fundingPolicy: FundingPolicy?,
394            createdBy: Address
395        ): UInt64 {
396            let finalEmergencyConfig = emergencyConfig 
397                ?? PrizeSavings.createDefaultEmergencyConfig()
398            let finalFundingPolicy = fundingPolicy 
399                ?? PrizeSavings.createDefaultFundingPolicy()
400            
401            let poolID = PrizeSavings.createPool(
402                config: config,
403                emergencyConfig: finalEmergencyConfig,
404                fundingPolicy: finalFundingPolicy
405            )
406            
407            emit PoolCreatedByAdmin(
408                poolID: poolID,
409                assetType: config.assetType.identifier,
410                strategy: config.distributionStrategy.getStrategyName(),
411                createdBy: createdBy
412            )
413            
414            return poolID
415        }
416        
417        access(ConfigOps) fun processPoolRewards(poolID: UInt64) {
418            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
419                ?? panic("Pool does not exist")
420            
421            poolRef.processRewards()
422        }
423        
424        access(CriticalOps) fun setPoolState(poolID: UInt64, state: PoolEmergencyState, reason: String?, setBy: Address) {
425            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
426                ?? panic("Pool does not exist")
427            
428            poolRef.setState(state: state, reason: reason)
429            
430            switch state {
431                case PoolEmergencyState.Normal:
432                    emit PoolUnpaused(poolID: poolID, unpausedBy: setBy)
433                case PoolEmergencyState.Paused:
434                    emit PoolPaused(poolID: poolID, pausedBy: setBy, reason: reason ?? "Manual pause")
435                case PoolEmergencyState.EmergencyMode:
436                    emit PoolEmergencyEnabled(poolID: poolID, reason: reason ?? "Emergency", enabledBy: setBy, timestamp: getCurrentBlock().timestamp)
437                case PoolEmergencyState.PartialMode:
438                    emit PoolPartialModeEnabled(poolID: poolID, reason: reason ?? "Partial mode", setBy: setBy, timestamp: getCurrentBlock().timestamp)
439            }
440        }
441        
442        /// Set the treasury recipient for automatic forwarding.
443        /// Once set, treasury funds are auto-forwarded during processRewards().
444        /// Pass nil to disable auto-forwarding (funds stored in distributor).
445        /// 
446        /// SECURITY: Requires OwnerOnly entitlement - NEVER issue capabilities with this.
447        /// Only the account owner (via direct storage borrow with auth) can call this.
448        /// For multi-sig protection, store Admin in a multi-sig account.
449        access(OwnerOnly) fun setPoolTreasuryRecipient(
450            poolID: UInt64,
451            recipientCap: Capability<&{FungibleToken.Receiver}>?,
452            updatedBy: Address
453        ) {
454            pre {
455                recipientCap?.check() ?? true: "Treasury recipient capability must be valid"
456            }
457            
458            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
459                ?? panic("Pool does not exist")
460            
461            poolRef.setTreasuryRecipient(cap: recipientCap)
462            
463            emit TreasuryRecipientUpdated(
464                poolID: poolID,
465                newRecipient: recipientCap?.address,
466                updatedBy: updatedBy
467            )
468        }
469        
470        access(ConfigOps) fun setBonusLotteryWeight(
471            poolID: UInt64,
472            receiverID: UInt64,
473            bonusWeight: UFix64,
474            reason: String,
475            setBy: Address
476        ) {
477            pre {
478                bonusWeight >= 0.0: "Bonus weight cannot be negative"
479            }
480            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
481                ?? panic("Pool does not exist")
482            
483            poolRef.setBonusWeight(receiverID: receiverID, bonusWeight: bonusWeight, reason: reason, setBy: setBy)
484        }
485        
486        access(ConfigOps) fun addBonusLotteryWeight(
487            poolID: UInt64,
488            receiverID: UInt64,
489            additionalWeight: UFix64,
490            reason: String,
491            addedBy: Address
492        ) {
493            pre {
494                additionalWeight > 0.0: "Additional weight must be positive"
495            }
496            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
497                ?? panic("Pool does not exist")
498            
499            poolRef.addBonusWeight(receiverID: receiverID, additionalWeight: additionalWeight, reason: reason, addedBy: addedBy)
500        }
501        
502        access(ConfigOps) fun removeBonusLotteryWeight(
503            poolID: UInt64,
504            receiverID: UInt64,
505            removedBy: Address
506        ) {
507            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
508                ?? panic("Pool does not exist")
509            
510            poolRef.removeBonusWeight(receiverID: receiverID, removedBy: removedBy)
511        }
512        
513        access(ConfigOps) fun depositNFTPrize(
514            poolID: UInt64,
515            nft: @{NonFungibleToken.NFT},
516            depositedBy: Address
517        ) {
518            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
519                ?? panic("Pool does not exist")
520            
521            let nftID = nft.uuid
522            let nftType = nft.getType().identifier
523            
524            poolRef.depositNFTPrize(nft: <- nft)
525            
526            emit NFTPrizeDeposited(
527                poolID: poolID,
528                nftID: nftID,
529                nftType: nftType,
530                depositedBy: depositedBy
531            )
532        }
533        
534        access(ConfigOps) fun withdrawNFTPrize(
535            poolID: UInt64,
536            nftID: UInt64,
537            withdrawnBy: Address
538        ): @{NonFungibleToken.NFT} {
539            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
540                ?? panic("Pool does not exist")
541            
542            let nft <- poolRef.withdrawNFTPrize(nftID: nftID)
543            let nftType = nft.getType().identifier
544            
545            emit NFTPrizeWithdrawn(
546                poolID: poolID,
547                nftID: nftID,
548                nftType: nftType,
549                withdrawnBy: withdrawnBy
550            )
551            
552            return <- nft
553        }
554        
555        // Batch draw functions (for future scalability when user count grows)
556
557        access(CriticalOps) fun startPoolDrawSnapshot(poolID: UInt64) {
558            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
559                ?? panic("Pool does not exist")
560            poolRef.startDrawSnapshot()
561        }
562        
563        access(CriticalOps) fun processPoolDrawBatch(poolID: UInt64, limit: Int) {
564            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
565                ?? panic("Pool does not exist")
566            poolRef.captureStakesBatch(limit: limit)
567        }
568        
569        access(CriticalOps) fun finalizePoolDraw(poolID: UInt64) {
570            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
571                ?? panic("Pool does not exist")
572            poolRef.finalizeDrawStart()
573        }
574        
575    }
576    
577    access(all) let AdminStoragePath: StoragePath
578    
579    access(all) struct DistributionPlan {
580        access(all) let savingsAmount: UFix64
581        access(all) let lotteryAmount: UFix64
582        access(all) let treasuryAmount: UFix64
583        
584        init(savings: UFix64, lottery: UFix64, treasury: UFix64) {
585            self.savingsAmount = savings
586            self.lotteryAmount = lottery
587            self.treasuryAmount = treasury
588        }
589    }
590    
591    access(all) struct interface DistributionStrategy {
592        access(all) fun calculateDistribution(totalAmount: UFix64): DistributionPlan
593        access(all) view fun getStrategyName(): String
594    }
595    
596    access(all) struct FixedPercentageStrategy: DistributionStrategy {
597        access(all) let savingsPercent: UFix64
598        access(all) let lotteryPercent: UFix64
599        access(all) let treasuryPercent: UFix64
600        
601        /// Note: Percentages must sum to exactly 1.0 (strict equality).
602        /// Use values like 0.4, 0.4, 0.2 - not repeating decimals like 0.33333333.
603        /// If using thirds, use 0.33, 0.33, 0.34 to sum exactly to 1.0.
604        init(savings: UFix64, lottery: UFix64, treasury: UFix64) {
605            pre {
606                savings + lottery + treasury == 1.0: "Percentages must sum to exactly 1.0 (e.g., 0.4 + 0.4 + 0.2)"
607                savings >= 0.0 && lottery >= 0.0 && treasury >= 0.0: "Must be non-negative"
608            }
609            self.savingsPercent = savings
610            self.lotteryPercent = lottery
611            self.treasuryPercent = treasury
612        }
613        
614        access(all) fun calculateDistribution(totalAmount: UFix64): DistributionPlan {
615            return DistributionPlan(
616                savings: totalAmount * self.savingsPercent,
617                lottery: totalAmount * self.lotteryPercent,
618                treasury: totalAmount * self.treasuryPercent
619            )
620        }
621        
622        access(all) view fun getStrategyName(): String {
623            return "Fixed: \(self.savingsPercent) savings, \(self.lotteryPercent) lottery, \(self.treasuryPercent) treasury"
624        }
625    }
626    
627    /// ERC4626-style shares distributor with virtual offset protection against inflation attacks.
628    /// totalAssets = principal + accrued yield (should equal Pool.totalStaked)
629    access(all) resource SavingsDistributor {
630        access(self) var totalShares: UFix64
631        access(self) var totalAssets: UFix64
632        access(self) let userShares: {UInt64: UFix64}
633        access(all) var totalDistributed: UFix64
634        access(self) let vaultType: Type
635        
636        // Time-weighted balance tracking for lottery fairness (balance-seconds)
637        // Uses current balance (shares × share_price) so yield counts toward lottery odds
638        access(self) let userCumulativeBalanceSeconds: {UInt64: UFix64}
639        access(self) let userLastUpdateTime: {UInt64: UFix64}
640        access(self) let userEpochID: {UInt64: UInt64}
641        access(self) var currentEpochID: UInt64
642        access(self) var epochStartTime: UFix64
643        
644        init(vaultType: Type) {
645            self.totalShares = 0.0
646            self.totalAssets = 0.0
647            self.userShares = {}
648            self.totalDistributed = 0.0
649            self.vaultType = vaultType
650            
651            self.userCumulativeBalanceSeconds = {}
652            self.userLastUpdateTime = {}
653            self.userEpochID = {}
654            self.currentEpochID = 1
655            self.epochStartTime = getCurrentBlock().timestamp
656        }
657        
658        /// Accrue yield to the savings pool.
659        /// Returns the actual amount accrued to users (after virtual share dust is excluded).
660        /// The dust portion goes to virtual shares to prevent dilution attacks.
661        access(contract) fun accrueYield(amount: UFix64): UFix64 {
662            if amount == 0.0 || self.totalShares == 0.0 {
663                return 0.0
664            }
665            
666            // Calculate how much goes to virtual shares (dust)
667            // dustPercent = VIRTUAL_SHARES / (totalShares + VIRTUAL_SHARES)
668            let effectiveShares = self.totalShares + PrizeSavings.VIRTUAL_SHARES
669            let dustAmount = amount * PrizeSavings.VIRTUAL_SHARES / effectiveShares
670            let actualSavings = amount - dustAmount
671            
672            self.totalAssets = self.totalAssets + actualSavings
673            self.totalDistributed = self.totalDistributed + actualSavings
674            
675            return actualSavings
676        }
677        
678        /// Calculate elapsed balance-seconds since last update.
679        /// Uses current balance (shares × share_price) so yield is included in lottery weight.
680        access(all) view fun getElapsedBalanceSeconds(receiverID: UInt64): UFix64 {
681            let now = getCurrentBlock().timestamp
682            let userEpoch = self.userEpochID[receiverID] ?? 0
683            let currentBalance = self.getUserAssetValue(receiverID: receiverID)
684            
685            let effectiveLastUpdate = userEpoch < self.currentEpochID 
686                ? self.epochStartTime 
687                : (self.userLastUpdateTime[receiverID] ?? self.epochStartTime)
688            
689            let elapsed = now - effectiveLastUpdate
690            if elapsed <= 0.0 {
691                return 0.0
692            }
693            return currentBalance * elapsed
694        }
695        
696        access(all) view fun getEffectiveAccumulated(receiverID: UInt64): UFix64 {
697            let userEpoch = self.userEpochID[receiverID] ?? 0
698            if userEpoch < self.currentEpochID {
699                return 0.0  // Would be reset on next accumulation
700            }
701            return self.userCumulativeBalanceSeconds[receiverID] ?? 0.0
702        }
703        
704        access(contract) fun accumulateTime(receiverID: UInt64) {
705            let userEpoch = self.userEpochID[receiverID] ?? 0
706            
707            if userEpoch < self.currentEpochID {
708                self.userCumulativeBalanceSeconds[receiverID] = 0.0
709                self.userEpochID[receiverID] = self.currentEpochID
710                
711                let currentBalance = self.getUserAssetValue(receiverID: receiverID)
712                if currentBalance > 0.0 {
713                    self.userLastUpdateTime[receiverID] = self.epochStartTime
714                } else {
715                    self.userLastUpdateTime[receiverID] = getCurrentBlock().timestamp
716                    return
717                }
718            }
719            
720            let elapsed = self.getElapsedBalanceSeconds(receiverID: receiverID)
721            if elapsed > 0.0 {
722                let currentAccum = self.userCumulativeBalanceSeconds[receiverID] ?? 0.0
723                self.userCumulativeBalanceSeconds[receiverID] = currentAccum + elapsed
724            }
725            self.userLastUpdateTime[receiverID] = getCurrentBlock().timestamp
726        }
727        
728        /// Returns total time-weighted balance (balance-seconds) for lottery weight calculation.
729        /// Includes both accumulated and elapsed balance-seconds.
730        access(all) view fun getTimeWeightedBalance(receiverID: UInt64): UFix64 {
731            return self.getEffectiveAccumulated(receiverID: receiverID) 
732                + self.getElapsedBalanceSeconds(receiverID: receiverID)
733        }
734        
735        /// Accumulate time and return the time-weighted balance (balance-seconds).
736        /// Used by lottery draw to get final lottery weight.
737        access(contract) fun updateAndGetTimeWeightedBalance(receiverID: UInt64): UFix64 {
738            self.accumulateTime(receiverID: receiverID)
739            return self.userCumulativeBalanceSeconds[receiverID] ?? 0.0
740        }
741        
742        access(contract) fun startNewPeriod() {
743            self.currentEpochID = self.currentEpochID + 1
744            self.epochStartTime = getCurrentBlock().timestamp
745        }
746        
747        access(all) view fun getCurrentEpochID(): UInt64 {
748            return self.currentEpochID
749        }
750        
751        access(all) view fun getEpochStartTime(): UFix64 {
752            return self.epochStartTime
753        }
754        
755        access(contract) fun deposit(receiverID: UInt64, amount: UFix64) {
756            if amount == 0.0 {
757                return
758            }
759            
760            self.accumulateTime(receiverID: receiverID)
761            
762            let sharesToMint = self.convertToShares(amount)
763            let currentShares = self.userShares[receiverID] ?? 0.0
764            self.userShares[receiverID] = currentShares + sharesToMint
765            self.totalShares = self.totalShares + sharesToMint
766            self.totalAssets = self.totalAssets + amount
767        }
768        
769        access(contract) fun withdraw(receiverID: UInt64, amount: UFix64): UFix64 {
770            if amount == 0.0 {
771                return 0.0
772            }
773            
774            self.accumulateTime(receiverID: receiverID)
775            
776            let userShareBalance = self.userShares[receiverID] ?? 0.0
777            assert(userShareBalance > 0.0, message: "No shares to withdraw")
778            assert(self.totalShares > 0.0 && self.totalAssets > 0.0, message: "Invalid distributor state")
779            
780            let currentAssetValue = self.convertToAssets(userShareBalance)
781            assert(amount <= currentAssetValue, message: "Insufficient balance")
782            
783            let sharesToBurn = self.convertToShares(amount)
784            
785            self.userShares[receiverID] = userShareBalance - sharesToBurn
786            self.totalShares = self.totalShares - sharesToBurn
787            self.totalAssets = self.totalAssets - amount
788            
789            return amount
790        }
791        
792        access(all) view fun convertToShares(_ assets: UFix64): UFix64 {
793            let effectiveShares = self.totalShares + PrizeSavings.VIRTUAL_SHARES
794            let effectiveAssets = self.totalAssets + PrizeSavings.VIRTUAL_ASSETS
795            
796            // Calculate share price first to avoid overflow in assets * effectiveShares
797            // share_price = effectiveAssets / effectiveShares (close to 1.0)
798            // shares = assets / share_price
799            let sharePrice = effectiveAssets / effectiveShares
800            return assets / sharePrice
801        }
802        
803        access(all) view fun convertToAssets(_ shares: UFix64): UFix64 {
804            let effectiveShares = self.totalShares + PrizeSavings.VIRTUAL_SHARES
805            let effectiveAssets = self.totalAssets + PrizeSavings.VIRTUAL_ASSETS
806            
807            // Calculate share price first to avoid overflow in shares * effectiveAssets
808            // share_price = effectiveAssets / effectiveShares (close to 1.0)
809            // Then: assets = shares * share_price
810            let sharePrice = effectiveAssets / effectiveShares
811            return shares * sharePrice
812        }
813        
814        access(all) view fun getUserAssetValue(receiverID: UInt64): UFix64 {
815            let userShareBalance = self.userShares[receiverID] ?? 0.0
816            return self.convertToAssets(userShareBalance)
817        }
818        
819        access(all) view fun getTotalDistributed(): UFix64 {
820            return self.totalDistributed
821        }
822        
823        access(all) view fun getTotalShares(): UFix64 {
824            return self.totalShares
825        }
826        
827        access(all) view fun getTotalAssets(): UFix64 {
828            return self.totalAssets
829        }
830        
831        access(all) view fun getUserShares(receiverID: UInt64): UFix64 {
832            return self.userShares[receiverID] ?? 0.0
833        }
834        
835        access(all) view fun getUserAccumulatedRaw(receiverID: UInt64): UFix64 {
836            return self.userCumulativeBalanceSeconds[receiverID] ?? 0.0
837        }
838        
839        access(all) view fun getUserLastUpdateTime(receiverID: UInt64): UFix64 {
840            return self.userLastUpdateTime[receiverID] ?? self.epochStartTime
841        }
842        
843        access(all) view fun getUserEpochID(receiverID: UInt64): UInt64 {
844            return self.userEpochID[receiverID] ?? 0
845        }
846        
847        /// Calculate projected balance-seconds at a specific time (no state change)
848        access(all) view fun calculateBalanceSecondsAtTime(receiverID: UInt64, targetTime: UFix64): UFix64 {
849            let userEpoch = self.userEpochID[receiverID] ?? 0
850            let balance = self.getUserAssetValue(receiverID: receiverID)
851            
852            if userEpoch < self.currentEpochID {
853                if targetTime <= self.epochStartTime { return 0.0 }
854                return balance * (targetTime - self.epochStartTime)
855            }
856            
857            let lastUpdate = self.userLastUpdateTime[receiverID] ?? self.epochStartTime
858            let accumulated = self.userCumulativeBalanceSeconds[receiverID] ?? 0.0
859            
860            if targetTime <= lastUpdate {
861                let overdraft = lastUpdate - targetTime
862                let overdraftAmount = balance * overdraft
863                return accumulated >= overdraftAmount ? accumulated - overdraftAmount : 0.0
864            }
865            
866            return accumulated + (balance * (targetTime - lastUpdate))
867        }
868    }
869    
870    access(all) resource LotteryDistributor {
871        access(self) var prizeVault: @{FungibleToken.Vault}
872        access(self) var nftPrizeSavings: @{UInt64: {NonFungibleToken.NFT}}
873        access(self) var pendingNFTClaims: @{UInt64: [{NonFungibleToken.NFT}]}
874        access(self) var _prizeRound: UInt64
875        access(all) var totalPrizesDistributed: UFix64
876        
877        access(all) view fun getPrizeRound(): UInt64 {
878            return self._prizeRound
879        }
880        
881        access(contract) fun setPrizeRound(round: UInt64) {
882            self._prizeRound = round
883        }
884        
885        init(vaultType: Type) {
886            self.prizeVault <- DeFiActionsUtils.getEmptyVault(vaultType)
887            self.nftPrizeSavings <- {}
888            self.pendingNFTClaims <- {}
889            self._prizeRound = 0
890            self.totalPrizesDistributed = 0.0
891        }
892        
893        access(contract) fun fundPrizePool(vault: @{FungibleToken.Vault}) {
894            self.prizeVault.deposit(from: <- vault)
895        }
896        
897        access(all) view fun getPrizePoolBalance(): UFix64 {
898            return self.prizeVault.balance
899        }
900        
901        access(contract) fun awardPrize(receiverID: UInt64, amount: UFix64, yieldSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}?): @{FungibleToken.Vault} {
902            self.totalPrizesDistributed = self.totalPrizesDistributed + amount
903            
904            var result <- DeFiActionsUtils.getEmptyVault(self.prizeVault.getType())
905            
906            if let source = yieldSource {
907                let available = source.minimumAvailable()
908                if available >= amount {
909                    result.deposit(from: <- source.withdrawAvailable(maxAmount: amount))
910                    return <- result
911                } else if available > 0.0 {
912                    result.deposit(from: <- source.withdrawAvailable(maxAmount: available))
913                }
914            }
915            
916            if result.balance < amount {
917                let remaining = amount - result.balance
918                assert(self.prizeVault.balance >= remaining, message: "Insufficient prize pool")
919                result.deposit(from: <- self.prizeVault.withdraw(amount: remaining))
920            }
921            
922            return <- result
923        }
924        
925        access(contract) fun depositNFTPrize(nft: @{NonFungibleToken.NFT}) {
926            let nftID = nft.uuid
927            self.nftPrizeSavings[nftID] <-! nft
928        }
929        
930        access(contract) fun withdrawNFTPrize(nftID: UInt64): @{NonFungibleToken.NFT} {
931            if let nft <- self.nftPrizeSavings.remove(key: nftID) {
932                return <- nft
933            }
934            panic("NFT not found in prize vault")
935        }
936        
937        access(contract) fun storePendingNFT(receiverID: UInt64, nft: @{NonFungibleToken.NFT}) {
938            if self.pendingNFTClaims[receiverID] == nil {
939                self.pendingNFTClaims[receiverID] <-! []
940            }
941            if let arrayRef = &self.pendingNFTClaims[receiverID] as auth(Mutate) &[{NonFungibleToken.NFT}]? {
942                arrayRef.append(<- nft)
943            } else {
944                destroy nft
945                panic("Failed to store NFT in pending claims")
946            }
947        }
948        
949        access(all) view fun getPendingNFTCount(receiverID: UInt64): Int {
950            return self.pendingNFTClaims[receiverID]?.length ?? 0
951        }
952        
953        access(all) fun getPendingNFTIDs(receiverID: UInt64): [UInt64] {
954            if let nfts = &self.pendingNFTClaims[receiverID] as &[{NonFungibleToken.NFT}]? {
955                var ids: [UInt64] = []
956                for nft in nfts {
957                    ids.append(nft.uuid)
958                }
959                return ids
960            }
961            return []
962        }
963        
964        access(all) view fun getAvailableNFTPrizeIDs(): [UInt64] {
965            return self.nftPrizeSavings.keys
966        }
967        
968        access(all) view fun borrowNFTPrize(nftID: UInt64): &{NonFungibleToken.NFT}? {
969            return &self.nftPrizeSavings[nftID]
970        }
971
972        access(contract) fun claimPendingNFT(receiverID: UInt64, nftIndex: Int): @{NonFungibleToken.NFT} {
973            pre {
974                self.pendingNFTClaims[receiverID] != nil: "No pending NFTs for this receiver"
975                nftIndex < (self.pendingNFTClaims[receiverID]?.length ?? 0): "Invalid NFT index"
976            }
977            if let nftsRef = &self.pendingNFTClaims[receiverID] as auth(Remove) &[{NonFungibleToken.NFT}]? {
978                return <- nftsRef.remove(at: nftIndex)
979            }
980            panic("Failed to access pending NFT claims")
981        }
982    }
983    
984    
985    access(all) resource PrizeDrawReceipt {
986        access(all) let prizeAmount: UFix64
987        access(self) var request: @RandomConsumer.Request?
988        access(all) let timeWeightedStakes: {UInt64: UFix64}
989        
990        init(prizeAmount: UFix64, request: @RandomConsumer.Request, timeWeightedStakes: {UInt64: UFix64}) {
991            self.prizeAmount = prizeAmount
992            self.request <- request
993            self.timeWeightedStakes = timeWeightedStakes
994        }
995        
996        access(all) view fun getRequestBlock(): UInt64? {
997            return self.request?.block
998        }
999        
1000        access(contract) fun popRequest(): @RandomConsumer.Request {
1001            let request <- self.request <- nil
1002            if let r <- request {
1003                return <- r
1004            }
1005            panic("No request to pop")
1006        }
1007        
1008        access(all) view fun getTimeWeightedStakes(): {UInt64: UFix64} {
1009            return self.timeWeightedStakes
1010        }
1011    }
1012    
1013    access(all) struct WinnerSelectionResult {
1014        access(all) let winners: [UInt64]
1015        access(all) let amounts: [UFix64]
1016        access(all) let nftIDs: [[UInt64]]
1017        
1018        init(winners: [UInt64], amounts: [UFix64], nftIDs: [[UInt64]]) {
1019            pre {
1020                winners.length == amounts.length: "Winners and amounts must have same length"
1021                winners.length == nftIDs.length: "Winners and nftIDs must have same length"
1022            }
1023            self.winners = winners
1024            self.amounts = amounts
1025            self.nftIDs = nftIDs
1026        }
1027    }
1028    
1029    /// Interface for winner selection strategies in lottery draws.
1030    /// Implementations receive weighted values (not raw deposits) to determine winner probability.
1031    access(all) struct interface WinnerSelectionStrategy {
1032        /// Select winners based on weighted random selection.
1033        /// - Parameter randomNumber: Source of randomness for selection
1034        /// - Parameter receiverWeights: Map of receiverID to their selection weight (balance-seconds + bonuses).
1035        ///   NOTE: These are NOT raw deposit balances - they represent lottery ticket weights.
1036        /// - Parameter totalPrizeAmount: Total prize pool to distribute among winners
1037        access(all) fun selectWinners(
1038            randomNumber: UInt64,
1039            receiverWeights: {UInt64: UFix64},
1040            totalPrizeAmount: UFix64
1041        ): WinnerSelectionResult
1042        access(all) view fun getStrategyName(): String
1043    }
1044    
1045    access(all) struct WeightedSingleWinner: WinnerSelectionStrategy {
1046        access(all) let RANDOM_SCALING_FACTOR: UInt64
1047        access(all) let RANDOM_SCALING_DIVISOR: UFix64
1048        
1049        access(all) let nftIDs: [UInt64]
1050        
1051        init(nftIDs: [UInt64]) {
1052            self.RANDOM_SCALING_FACTOR = 1_000_000_000
1053            self.RANDOM_SCALING_DIVISOR = 1_000_000_000.0
1054            self.nftIDs = nftIDs
1055        }
1056        
1057        access(all) fun selectWinners(
1058            randomNumber: UInt64,
1059            receiverWeights: {UInt64: UFix64},
1060            totalPrizeAmount: UFix64
1061        ): WinnerSelectionResult {
1062            let receiverIDs = receiverWeights.keys
1063            
1064            if receiverIDs.length == 0 {
1065                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1066            }
1067            
1068            if receiverIDs.length == 1 {
1069                return WinnerSelectionResult(
1070                    winners: [receiverIDs[0]],
1071                    amounts: [totalPrizeAmount],
1072                    nftIDs: [self.nftIDs]
1073                )
1074            }
1075            
1076            var cumulativeSum: [UFix64] = []
1077            var runningTotal: UFix64 = 0.0
1078            
1079            for receiverID in receiverIDs {
1080                let weight = receiverWeights[receiverID] ?? 0.0
1081                runningTotal = runningTotal + weight
1082                cumulativeSum.append(runningTotal)
1083            }
1084            
1085            if runningTotal == 0.0 {
1086                return WinnerSelectionResult(
1087                    winners: [receiverIDs[0]],
1088                    amounts: [totalPrizeAmount],
1089                    nftIDs: [self.nftIDs]
1090                )
1091            }
1092            
1093            let scaledRandom = UFix64(randomNumber % self.RANDOM_SCALING_FACTOR) / self.RANDOM_SCALING_DIVISOR
1094            let randomValue = scaledRandom * runningTotal
1095            
1096            var winnerIndex = 0
1097            for i, cumSum in cumulativeSum {
1098                if randomValue < cumSum {
1099                    winnerIndex = i
1100                    break
1101                }
1102            }
1103            
1104            return WinnerSelectionResult(
1105                winners: [receiverIDs[winnerIndex]],
1106                amounts: [totalPrizeAmount],
1107                nftIDs: [self.nftIDs]
1108            )
1109        }
1110        
1111        access(all) view fun getStrategyName(): String {
1112            return "Weighted Single Winner"
1113        }
1114    }
1115    
1116    access(all) struct MultiWinnerSplit: WinnerSelectionStrategy {
1117        access(all) let RANDOM_SCALING_FACTOR: UInt64
1118        access(all) let RANDOM_SCALING_DIVISOR: UFix64
1119        access(all) let winnerCount: Int
1120        access(all) let prizeSplits: [UFix64]
1121        access(all) let nftIDsPerWinner: [[UInt64]]
1122        
1123        init(winnerCount: Int, prizeSplits: [UFix64], nftIDs: [UInt64]) {
1124            pre {
1125                winnerCount > 0: "Must have at least one winner"
1126                prizeSplits.length == winnerCount: "Prize splits must match winner count"
1127            }
1128            
1129            var total: UFix64 = 0.0
1130            for split in prizeSplits {
1131                assert(split >= 0.0 && split <= 1.0, message: "Each split must be between 0 and 1")
1132                total = total + split
1133            }
1134            
1135            assert(total == 1.0, message: "Prize splits must sum to 1.0")
1136            
1137            self.RANDOM_SCALING_FACTOR = 1_000_000_000
1138            self.RANDOM_SCALING_DIVISOR = 1_000_000_000.0
1139            self.winnerCount = winnerCount
1140            self.prizeSplits = prizeSplits
1141            
1142            var nftArray: [[UInt64]] = []
1143            var nftIndex = 0
1144            for winnerIdx in InclusiveRange(0, winnerCount - 1) {
1145                if nftIndex < nftIDs.length {
1146                    nftArray.append([nftIDs[nftIndex]])
1147                    nftIndex = nftIndex + 1
1148                } else {
1149                    nftArray.append([])
1150                }
1151            }
1152            self.nftIDsPerWinner = nftArray
1153        }
1154        
1155        access(all) fun selectWinners(
1156            randomNumber: UInt64,
1157            receiverWeights: {UInt64: UFix64},
1158            totalPrizeAmount: UFix64
1159        ): WinnerSelectionResult {
1160            let receiverIDs = receiverWeights.keys
1161            let receiverCount = receiverIDs.length
1162            
1163            if receiverCount == 0 {
1164                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1165            }
1166            
1167            let actualWinnerCount = self.winnerCount < receiverCount ? self.winnerCount : receiverCount
1168            
1169            if receiverCount == 1 {
1170                let nftIDsForFirst: [UInt64] = self.nftIDsPerWinner.length > 0 ? self.nftIDsPerWinner[0] : []
1171                return WinnerSelectionResult(
1172                    winners: [receiverIDs[0]],
1173                    amounts: [totalPrizeAmount],
1174                    nftIDs: [nftIDsForFirst]
1175                )
1176            }
1177            
1178            var cumulativeSum: [UFix64] = []
1179            var runningTotal: UFix64 = 0.0
1180            var weightsList: [UFix64] = []
1181            
1182            for receiverID in receiverIDs {
1183                let weight = receiverWeights[receiverID] ?? 0.0
1184                weightsList.append(weight)
1185                runningTotal = runningTotal + weight
1186                cumulativeSum.append(runningTotal)
1187            }
1188            
1189            if runningTotal == 0.0 {
1190                var uniformWinners: [UInt64] = []
1191                var uniformAmounts: [UFix64] = []
1192                var uniformNFTs: [[UInt64]] = []
1193                var calculatedSum: UFix64 = 0.0
1194                
1195                for idx in InclusiveRange(0, actualWinnerCount - 1) {
1196                    uniformWinners.append(receiverIDs[idx])
1197                    if idx < actualWinnerCount - 1 {
1198                        let amount = totalPrizeAmount * self.prizeSplits[idx]
1199                        uniformAmounts.append(amount)
1200                        calculatedSum = calculatedSum + amount
1201                    }
1202                    if idx < self.nftIDsPerWinner.length {
1203                        uniformNFTs.append(self.nftIDsPerWinner[idx])
1204                    } else {
1205                        uniformNFTs.append([])
1206                    }
1207                }
1208                uniformAmounts.append(totalPrizeAmount - calculatedSum)
1209                
1210                return WinnerSelectionResult(
1211                    winners: uniformWinners,
1212                    amounts: uniformAmounts,
1213                    nftIDs: uniformNFTs
1214                )
1215            }
1216            
1217            var selectedWinners: [UInt64] = []
1218            var remainingWeights = weightsList
1219            var remainingIDs = receiverIDs
1220            var remainingCumSum = cumulativeSum
1221            var remainingTotal = runningTotal
1222            
1223            var randomBytes = randomNumber.toBigEndianBytes()
1224            while randomBytes.length < 16 {
1225                randomBytes.appendAll(randomNumber.toBigEndianBytes())
1226            }
1227            var paddedBytes: [UInt8] = []
1228            for padIdx in InclusiveRange(0, 15) {
1229                paddedBytes.append(randomBytes[padIdx % randomBytes.length])
1230            }
1231            
1232            let prg = Xorshift128plus.PRG(
1233                sourceOfRandomness: paddedBytes,
1234                salt: []
1235            )
1236            
1237            var winnerIndex = 0
1238            while winnerIndex < actualWinnerCount && remainingIDs.length > 0 && remainingTotal > 0.0 {
1239                let rng = prg.nextUInt64()
1240                let scaledRandom = UFix64(rng % self.RANDOM_SCALING_FACTOR) / self.RANDOM_SCALING_DIVISOR
1241                let randomValue = scaledRandom * remainingTotal
1242                
1243                var selectedIdx = 0
1244                for i, cumSum in remainingCumSum {
1245                    if randomValue < cumSum {
1246                        selectedIdx = i
1247                        break
1248                    }
1249                }
1250                
1251                selectedWinners.append(remainingIDs[selectedIdx])
1252                var newRemainingIDs: [UInt64] = []
1253                var newRemainingWeights: [UFix64] = []
1254                var newCumSum: [UFix64] = []
1255                var newRunningTotal: UFix64 = 0.0
1256                
1257                for idx in InclusiveRange(0, remainingIDs.length - 1) {
1258                    if idx != selectedIdx {
1259                        newRemainingIDs.append(remainingIDs[idx])
1260                        newRemainingWeights.append(remainingWeights[idx])
1261                        newRunningTotal = newRunningTotal + remainingWeights[idx]
1262                        newCumSum.append(newRunningTotal)
1263                    }
1264                }
1265                
1266                remainingIDs = newRemainingIDs
1267                remainingWeights = newRemainingWeights
1268                remainingCumSum = newCumSum
1269                remainingTotal = newRunningTotal
1270                winnerIndex = winnerIndex + 1
1271            }
1272            
1273            var prizeAmounts: [UFix64] = []
1274            var calculatedSum: UFix64 = 0.0
1275            
1276            if selectedWinners.length > 1 {
1277                for idx in InclusiveRange(0, selectedWinners.length - 2) {
1278                    let split = self.prizeSplits[idx]
1279                    let amount = totalPrizeAmount * split
1280                    prizeAmounts.append(amount)
1281                    calculatedSum = calculatedSum + amount
1282                }
1283            }
1284            
1285            let lastPrize = totalPrizeAmount - calculatedSum
1286            prizeAmounts.append(lastPrize)
1287            
1288            if selectedWinners.length == self.winnerCount {
1289                let expectedLast = totalPrizeAmount * self.prizeSplits[selectedWinners.length - 1]
1290                let deviation = lastPrize > expectedLast ? lastPrize - expectedLast : expectedLast - lastPrize
1291                let maxDeviation = totalPrizeAmount * 0.01
1292                assert(deviation <= maxDeviation, message: "Last prize deviation too large - check splits")
1293            }
1294            
1295            var nftIDsArray: [[UInt64]] = []
1296            for idx2 in InclusiveRange(0, selectedWinners.length - 1) {
1297                if idx2 < self.nftIDsPerWinner.length {
1298                    nftIDsArray.append(self.nftIDsPerWinner[idx2])
1299                } else {
1300                    nftIDsArray.append([])
1301                }
1302            }
1303            
1304            return WinnerSelectionResult(
1305                winners: selectedWinners,
1306                amounts: prizeAmounts,
1307                nftIDs: nftIDsArray
1308            )
1309        }
1310        
1311        access(all) view fun getStrategyName(): String {
1312            var name = "Multi-Winner (\(self.winnerCount) winners): "
1313            for idx in InclusiveRange(0, self.prizeSplits.length - 1) {
1314                if idx > 0 {
1315                    name = name.concat(", ")
1316                }
1317                name = name.concat("\(self.prizeSplits[idx] * 100.0)%")
1318            }
1319            return name
1320        }
1321    }
1322    
1323    access(all) struct PrizeTier {
1324        access(all) let prizeAmount: UFix64
1325        access(all) let winnerCount: Int
1326        access(all) let name: String
1327        access(all) let nftIDs: [UInt64]
1328        
1329        init(amount: UFix64, count: Int, name: String, nftIDs: [UInt64]) {
1330            pre {
1331                amount > 0.0: "Prize amount must be positive"
1332                count > 0: "Winner count must be positive"
1333                nftIDs.length <= count: "Cannot have more NFTs than winners in tier"
1334            }
1335            self.prizeAmount = amount
1336            self.winnerCount = count
1337            self.name = name
1338            self.nftIDs = nftIDs
1339        }
1340    }
1341    
1342    access(all) struct FixedPrizeTiers: WinnerSelectionStrategy {
1343        access(all) let RANDOM_SCALING_FACTOR: UInt64
1344        access(all) let RANDOM_SCALING_DIVISOR: UFix64
1345        
1346        access(all) let tiers: [PrizeTier]
1347        
1348        init(tiers: [PrizeTier]) {
1349            pre {
1350                tiers.length > 0: "Must have at least one prize tier"
1351            }
1352            self.RANDOM_SCALING_FACTOR = 1_000_000_000
1353            self.RANDOM_SCALING_DIVISOR = 1_000_000_000.0
1354            self.tiers = tiers
1355        }
1356        
1357        access(all) fun selectWinners(
1358            randomNumber: UInt64,
1359            receiverWeights: {UInt64: UFix64},
1360            totalPrizeAmount: UFix64
1361        ): WinnerSelectionResult {
1362            let receiverIDs = receiverWeights.keys
1363            let receiverCount = receiverIDs.length
1364            
1365            if receiverCount == 0 {
1366                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1367            }
1368            
1369            var totalNeeded: UFix64 = 0.0
1370            var totalWinnersNeeded = 0
1371            for tier in self.tiers {
1372                totalNeeded = totalNeeded + (tier.prizeAmount * UFix64(tier.winnerCount))
1373                totalWinnersNeeded = totalWinnersNeeded + tier.winnerCount
1374            }
1375            
1376            if totalPrizeAmount < totalNeeded {
1377                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1378            }
1379            
1380            if totalWinnersNeeded > receiverCount {
1381                return WinnerSelectionResult(winners: [], amounts: [], nftIDs: [])
1382            }
1383            
1384            var cumulativeSum: [UFix64] = []
1385            var runningTotal: UFix64 = 0.0
1386            
1387            for receiverID in receiverIDs {
1388                let weight = receiverWeights[receiverID] ?? 0.0
1389                runningTotal = runningTotal + weight
1390                cumulativeSum.append(runningTotal)
1391            }
1392            
1393            var randomBytes = randomNumber.toBigEndianBytes()
1394            while randomBytes.length < 16 {
1395                randomBytes.appendAll(randomNumber.toBigEndianBytes())
1396            }
1397            var paddedBytes: [UInt8] = []
1398            for padIdx in InclusiveRange(0, 15) {
1399                paddedBytes.append(randomBytes[padIdx % randomBytes.length])
1400            }
1401            
1402            let prg = Xorshift128plus.PRG(
1403                sourceOfRandomness: paddedBytes,
1404                salt: []
1405            )
1406            
1407            var allWinners: [UInt64] = []
1408            var allPrizes: [UFix64] = []
1409            var allNFTIDs: [[UInt64]] = []
1410            var remainingIDs = receiverIDs
1411            var remainingCumSum = cumulativeSum
1412            var remainingTotal = runningTotal
1413            
1414            for tier in self.tiers {
1415                var tierWinnerCount = 0
1416                
1417                while tierWinnerCount < tier.winnerCount && remainingIDs.length > 0 && remainingTotal > 0.0 {
1418                    let rng = prg.nextUInt64()
1419                    let scaledRandom = UFix64(rng % self.RANDOM_SCALING_FACTOR) / self.RANDOM_SCALING_DIVISOR
1420                    let randomValue = scaledRandom * remainingTotal
1421                    
1422                    var selectedIdx = 0
1423                    for i, cumSum in remainingCumSum {
1424                        if randomValue < cumSum {
1425                            selectedIdx = i
1426                            break
1427                        }
1428                    }
1429                    
1430                    let winnerID = remainingIDs[selectedIdx]
1431                    allWinners.append(winnerID)
1432                    allPrizes.append(tier.prizeAmount)
1433                    
1434                    if tierWinnerCount < tier.nftIDs.length {
1435                        allNFTIDs.append([tier.nftIDs[tierWinnerCount]])
1436                    } else {
1437                        allNFTIDs.append([])
1438                    }
1439                    
1440                    var newRemainingIDs: [UInt64] = []
1441                    var newRemainingCumSum: [UFix64] = []
1442                    var newRunningTotal: UFix64 = 0.0
1443                    
1444                    for oldIdx in InclusiveRange(0, remainingIDs.length - 1) {
1445                        if oldIdx != selectedIdx {
1446                            newRemainingIDs.append(remainingIDs[oldIdx])
1447                            let weight = receiverWeights[remainingIDs[oldIdx]] ?? 0.0
1448                            newRunningTotal = newRunningTotal + weight
1449                            newRemainingCumSum.append(newRunningTotal)
1450                        }
1451                    }
1452                    
1453                    remainingIDs = newRemainingIDs
1454                    remainingCumSum = newRemainingCumSum
1455                    remainingTotal = newRunningTotal
1456                    tierWinnerCount = tierWinnerCount + 1
1457                }
1458            }
1459            
1460            return WinnerSelectionResult(
1461                winners: allWinners,
1462                amounts: allPrizes,
1463                nftIDs: allNFTIDs
1464            )
1465        }
1466        
1467        access(all) view fun getStrategyName(): String {
1468            var name = "Fixed Prizes ("
1469            for idx in InclusiveRange(0, self.tiers.length - 1) {
1470                if idx > 0 {
1471                    name = name.concat(", ")
1472                }
1473                let tier = self.tiers[idx]
1474                name = name.concat("\(tier.winnerCount)x \(tier.prizeAmount)")
1475            }
1476            return name.concat(")")
1477        }
1478    }
1479    
1480    access(all) struct PoolConfig {
1481        access(all) let assetType: Type
1482        access(all) let yieldConnector: {DeFiActions.Sink, DeFiActions.Source}
1483        access(all) var minimumDeposit: UFix64
1484        access(all) var drawIntervalSeconds: UFix64
1485        access(all) var distributionStrategy: {DistributionStrategy}
1486        access(all) var winnerSelectionStrategy: {WinnerSelectionStrategy}
1487        access(all) var winnerTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?
1488        
1489        init(
1490            assetType: Type,
1491            yieldConnector: {DeFiActions.Sink, DeFiActions.Source},
1492            minimumDeposit: UFix64,
1493            drawIntervalSeconds: UFix64,
1494            distributionStrategy: {DistributionStrategy},
1495            winnerSelectionStrategy: {WinnerSelectionStrategy},
1496            winnerTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?
1497        ) {
1498            self.assetType = assetType
1499            self.yieldConnector = yieldConnector
1500            self.minimumDeposit = minimumDeposit
1501            self.drawIntervalSeconds = drawIntervalSeconds
1502            self.distributionStrategy = distributionStrategy
1503            self.winnerSelectionStrategy = winnerSelectionStrategy
1504            self.winnerTrackerCap = winnerTrackerCap
1505        }
1506        
1507        access(contract) fun setDistributionStrategy(strategy: {DistributionStrategy}) {
1508            self.distributionStrategy = strategy
1509        }
1510        
1511        access(contract) fun setWinnerSelectionStrategy(strategy: {WinnerSelectionStrategy}) {
1512            self.winnerSelectionStrategy = strategy
1513        }
1514        
1515        access(contract) fun setWinnerTrackerCap(cap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?) {
1516            self.winnerTrackerCap = cap
1517        }
1518        
1519        access(contract) fun setDrawIntervalSeconds(interval: UFix64) {
1520            pre {
1521                interval >= 1.0: "Draw interval must be at least 1 seconds"
1522            }
1523            self.drawIntervalSeconds = interval
1524        }
1525        
1526        access(contract) fun setMinimumDeposit(minimum: UFix64) {
1527            pre {
1528                minimum >= 0.0: "Minimum deposit cannot be negative"
1529            }
1530            self.minimumDeposit = minimum
1531        }
1532    }
1533    
1534    /// Batch draw state for breaking startDraw() into multiple transactions.
1535    /// Flow: startDrawSnapshot() → captureStakesBatch() (repeat) → finalizeDrawStart()
1536    access(all) struct BatchDrawState {
1537        access(all) let drawCutoffTime: UFix64
1538        access(all) let previousEpochStartTime: UFix64  // Store epoch start time before startNewPeriod()
1539        access(all) var capturedWeights: {UInt64: UFix64}
1540        access(all) var totalWeight: UFix64
1541        access(all) var processedCount: Int
1542        access(all) let snapshotEpochID: UInt64
1543        
1544        init(cutoffTime: UFix64, epochStartTime: UFix64, epochID: UInt64) {
1545            self.drawCutoffTime = cutoffTime
1546            self.previousEpochStartTime = epochStartTime
1547            self.capturedWeights = {}
1548            self.totalWeight = 0.0
1549            self.processedCount = 0
1550            self.snapshotEpochID = epochID
1551        }
1552    }
1553    
1554    /// Pool contains nested resources (SavingsDistributor, LotteryDistributor)
1555    /// that hold FungibleToken vaults and NFTs. In Cadence 1.0+, these are automatically
1556    /// destroyed when the Pool is destroyed. The destroy order is:
1557    /// 1. pendingDrawReceipt (RandomConsumer.Request)
1558    /// 2. randomConsumer
1559    /// 3. savingsDistributor (contains user share data)
1560    /// 4. lotteryDistributor (contains prizeVault, nftPrizeSavings, pendingNFTClaims)
1561    /// Treasury is auto-forwarded to configured recipient during processRewards().
1562    access(all) resource Pool {
1563        access(self) var config: PoolConfig
1564        access(self) var poolID: UInt64
1565        
1566        access(self) var emergencyState: PoolEmergencyState
1567        access(self) var emergencyReason: String?
1568        access(self) var emergencyActivatedAt: UFix64?
1569        access(self) var emergencyConfig: EmergencyConfig
1570        access(self) var consecutiveWithdrawFailures: Int
1571        
1572        access(self) var fundingPolicy: FundingPolicy
1573        
1574        access(contract) fun setPoolID(id: UInt64) {
1575            self.poolID = id
1576        }
1577        
1578        access(self) let receiverDeposits: {UInt64: UFix64}
1579        access(self) let receiverTotalEarnedPrizes: {UInt64: UFix64}
1580        access(self) let registeredReceivers: {UInt64: Bool}
1581        access(self) let receiverBonusWeights: {UInt64: BonusWeightRecord}
1582        access(self) let receiverAddresses: {UInt64: Address}
1583        
1584        /// ACCOUNTING VARIABLES - Key relationships:
1585        /// 
1586        /// totalDeposited: Sum of user deposits + auto-compounded lottery prizes (excludes savings interest)
1587        ///   Updated on: deposit (+), prize awarded (+), withdraw (-)
1588        ///   Note: This is the "no-loss guarantee" amount - users can always withdraw at least this much
1589        ///   
1590        /// totalStaked: Amount tracked as being in the yield source
1591        ///   Updated on: deposit (+), prize awarded (+), savings yield accrual (+), withdraw from yield source (-)
1592        ///   Should equal: yieldSourceBalance (approximately)
1593        ///   
1594        /// Invariant: totalStaked >= totalDeposited (difference is reinvested savings interest)
1595        /// 
1596        /// Note: SavingsDistributor.totalAssets tracks what users own and should equal totalStaked
1597        access(all) var totalDeposited: UFix64
1598        access(all) var totalStaked: UFix64
1599        access(all) var lastDrawTimestamp: UFix64
1600        access(all) var pendingLotteryYield: UFix64  // Lottery funds still earning in yield source
1601        access(all) var totalTreasuryForwarded: UFix64  // Total treasury auto-forwarded to recipient
1602        access(self) var treasuryRecipientCap: Capability<&{FungibleToken.Receiver}>?  // Auto-forward treasury to this address
1603        access(self) let savingsDistributor: @SavingsDistributor
1604        access(self) let lotteryDistributor: @LotteryDistributor
1605        
1606        access(self) var pendingDrawReceipt: @PrizeDrawReceipt?
1607        access(self) let randomConsumer: @RandomConsumer.Consumer
1608        
1609        /// Batch draw state. When set, deposits/withdrawals are locked.
1610        access(self) var batchDrawState: BatchDrawState?
1611        
1612        init(
1613            config: PoolConfig, 
1614            emergencyConfig: EmergencyConfig?,
1615            fundingPolicy: FundingPolicy?
1616        ) {
1617            self.config = config
1618            self.poolID = 0
1619            
1620            self.emergencyState = PoolEmergencyState.Normal
1621            self.emergencyReason = nil
1622            self.emergencyActivatedAt = nil
1623            self.emergencyConfig = emergencyConfig ?? PrizeSavings.createDefaultEmergencyConfig()
1624            self.consecutiveWithdrawFailures = 0
1625            
1626            self.fundingPolicy = fundingPolicy ?? PrizeSavings.createDefaultFundingPolicy()
1627            
1628            self.receiverDeposits = {}
1629            self.receiverTotalEarnedPrizes = {}
1630            self.registeredReceivers = {}
1631            self.receiverBonusWeights = {}
1632            self.receiverAddresses = {}
1633            self.totalDeposited = 0.0
1634            self.totalStaked = 0.0
1635            self.lastDrawTimestamp = 0.0
1636            self.pendingLotteryYield = 0.0
1637            self.totalTreasuryForwarded = 0.0
1638            self.treasuryRecipientCap = nil
1639            
1640            self.savingsDistributor <- create SavingsDistributor(vaultType: config.assetType)
1641            self.lotteryDistributor <- create LotteryDistributor(vaultType: config.assetType)
1642            
1643            self.pendingDrawReceipt <- nil
1644            self.randomConsumer <- RandomConsumer.createConsumer()
1645            self.batchDrawState = nil
1646        }
1647        
1648        /// Register a receiver ID with owner address. Only callable within contract (by PoolPositionCollection).
1649        access(contract) fun registerReceiver(receiverID: UInt64, ownerAddress: Address) {
1650            pre {
1651                self.registeredReceivers[receiverID] == nil: "Receiver already registered"
1652            }
1653            self.registeredReceivers[receiverID] = true
1654            self.receiverAddresses[receiverID] = ownerAddress
1655        }
1656        
1657        /// Get the owner address for a receiver ID.
1658        access(all) view fun getReceiverAddress(receiverID: UInt64): Address? {
1659            return self.receiverAddresses[receiverID]
1660        }
1661        
1662        
1663        access(all) view fun getEmergencyState(): PoolEmergencyState {
1664            return self.emergencyState
1665        }
1666        
1667        access(all) view fun getEmergencyConfig(): EmergencyConfig {
1668            return self.emergencyConfig
1669        }
1670        
1671        access(contract) fun setState(state: PoolEmergencyState, reason: String?) {
1672            self.emergencyState = state
1673            if state == PoolEmergencyState.Normal {
1674                self.emergencyReason = nil
1675                self.emergencyActivatedAt = nil
1676                self.consecutiveWithdrawFailures = 0
1677            } else {
1678                self.emergencyReason = reason
1679                self.emergencyActivatedAt = getCurrentBlock().timestamp
1680            }
1681        }
1682        
1683        access(contract) fun setEmergencyMode(reason: String) {
1684            self.emergencyState = PoolEmergencyState.EmergencyMode
1685            self.emergencyReason = reason
1686            self.emergencyActivatedAt = getCurrentBlock().timestamp
1687        }
1688        
1689        access(contract) fun setPartialMode(reason: String) {
1690            self.emergencyState = PoolEmergencyState.PartialMode
1691            self.emergencyReason = reason
1692            self.emergencyActivatedAt = getCurrentBlock().timestamp
1693        }
1694        
1695        access(contract) fun clearEmergencyMode() {
1696            self.emergencyState = PoolEmergencyState.Normal
1697            self.emergencyReason = nil
1698            self.emergencyActivatedAt = nil
1699            self.consecutiveWithdrawFailures = 0
1700        }
1701        
1702        access(contract) fun setEmergencyConfig(config: EmergencyConfig) {
1703            self.emergencyConfig = config
1704        }
1705        
1706        access(all) view fun isEmergencyMode(): Bool {
1707            return self.emergencyState == PoolEmergencyState.EmergencyMode
1708        }
1709        
1710        access(all) view fun isPartialMode(): Bool {
1711            return self.emergencyState == PoolEmergencyState.PartialMode
1712        }
1713        
1714        access(all) fun getEmergencyInfo(): {String: AnyStruct}? {
1715            if self.emergencyState != PoolEmergencyState.Normal {
1716                let duration = getCurrentBlock().timestamp - (self.emergencyActivatedAt ?? 0.0)
1717                let health = self.checkYieldSourceHealth()
1718                return {
1719                    "state": self.emergencyState.rawValue,
1720                    "reason": self.emergencyReason ?? "Unknown",
1721                    "activatedAt": self.emergencyActivatedAt ?? 0.0,
1722                    "durationSeconds": duration,
1723                    "yieldSourceHealth": health,
1724                    "canAutoRecover": self.emergencyConfig.autoRecoveryEnabled,
1725                    "maxDuration": self.emergencyConfig.maxEmergencyDuration ?? 0.0  // 0.0 = no max
1726                }
1727            }
1728            return nil
1729        }
1730        
1731        access(contract) fun checkYieldSourceHealth(): UFix64 {
1732            let yieldSource = &self.config.yieldConnector as &{DeFiActions.Source}
1733            let balance = yieldSource.minimumAvailable()
1734            let threshold = self.getEmergencyConfig().minBalanceThreshold
1735            let balanceHealthy = balance >= self.totalStaked * threshold
1736            let withdrawSuccessRate = self.consecutiveWithdrawFailures == 0 ? 1.0 : 
1737                (1.0 / UFix64(self.consecutiveWithdrawFailures + 1))
1738            
1739            var health: UFix64 = 0.0
1740            if balanceHealthy { health = health + 0.5 }
1741            health = health + (withdrawSuccessRate * 0.5)
1742            return health
1743        }
1744        
1745        access(contract) fun checkAndAutoTriggerEmergency(): Bool {
1746            if self.emergencyState != PoolEmergencyState.Normal {
1747                return false
1748            }
1749            
1750            let health = self.checkYieldSourceHealth()
1751            if health < self.emergencyConfig.minYieldSourceHealth {
1752                self.setEmergencyMode(reason: "Auto-triggered: Yield source health below threshold (\(health))")
1753                emit EmergencyModeAutoTriggered(
1754                    poolID: self.poolID,
1755                    reason: "Low yield source health",
1756                    healthScore: health,
1757                    timestamp: getCurrentBlock().timestamp
1758                )
1759                return true
1760            }
1761            
1762            if self.consecutiveWithdrawFailures >= self.emergencyConfig.maxWithdrawFailures {
1763                self.setEmergencyMode(reason: "Auto-triggered: Multiple consecutive withdrawal failures")
1764                emit EmergencyModeAutoTriggered(
1765                    poolID: self.poolID,
1766                    reason: "Withdrawal failures",
1767                    healthScore: health,
1768                    timestamp: getCurrentBlock().timestamp)
1769                return true
1770            }
1771            
1772            return false
1773        }
1774        
1775        access(contract) fun checkAndAutoRecover(): Bool {
1776            if self.emergencyState != PoolEmergencyState.EmergencyMode {
1777                return false
1778            }
1779            
1780            if !self.emergencyConfig.autoRecoveryEnabled {
1781                return false
1782            }
1783            
1784            let health = self.checkYieldSourceHealth()
1785            
1786            // Health-based recovery: yield source is healthy
1787            if health >= 0.9 {
1788                self.clearEmergencyMode()
1789                emit EmergencyModeAutoRecovered(poolID: self.poolID, reason: "Yield source recovered", healthScore: health, duration: nil, timestamp: getCurrentBlock().timestamp)
1790                return true
1791            }
1792            
1793            // Time-based recovery: only if health is not critically low
1794            let minRecoveryHealth = self.emergencyConfig.minRecoveryHealth
1795            if let maxDuration = self.emergencyConfig.maxEmergencyDuration {
1796                let duration = getCurrentBlock().timestamp - (self.emergencyActivatedAt ?? 0.0)
1797                if duration > maxDuration && health >= minRecoveryHealth {
1798                    self.clearEmergencyMode()
1799                    emit EmergencyModeAutoRecovered(poolID: self.poolID, reason: "Max duration exceeded", healthScore: health, duration: duration, timestamp: getCurrentBlock().timestamp)
1800                    return true
1801                }
1802            }
1803            
1804            return false
1805        }
1806        
1807        access(contract) fun fundDirectInternal(
1808            destination: PoolFundingDestination,
1809            from: @{FungibleToken.Vault},
1810            sponsor: Address,
1811            purpose: String,
1812            metadata: {String: String}
1813        ) {
1814            pre {
1815                self.emergencyState == PoolEmergencyState.Normal: "Direct funding only in normal state"
1816                from.getType() == self.config.assetType: "Invalid vault type"
1817            }
1818            
1819            let amount = from.balance
1820            var policy = self.fundingPolicy
1821            policy.recordDirectFunding(destination: destination, amount: amount)
1822            self.fundingPolicy = policy
1823            
1824            switch destination {
1825                case PoolFundingDestination.Lottery:
1826                    self.lotteryDistributor.fundPrizePool(vault: <- from)
1827                case PoolFundingDestination.Savings:
1828                    // Prevent orphaned funds: savings distribution requires depositors
1829                    assert(
1830                        self.savingsDistributor.getTotalShares() > 0.0,
1831                        message: "Cannot fund savings with no depositors - funds would be orphaned"
1832                    )
1833                    self.config.yieldConnector.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
1834                    destroy from
1835                    let actualSavings = self.savingsDistributor.accrueYield(amount: amount)
1836                    let dustAmount = amount - actualSavings
1837                    self.totalStaked = self.totalStaked + actualSavings
1838                    emit SavingsYieldAccrued(poolID: self.poolID, amount: actualSavings)
1839                    
1840                    // Route dust to treasury if recipient configured
1841                    if dustAmount > 0.0 {
1842                        emit SavingsRoundingDustToTreasury(poolID: self.poolID, amount: dustAmount)
1843                        if let cap = self.treasuryRecipientCap {
1844                            if let recipientRef = cap.borrow() {
1845                                let dustVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: dustAmount)
1846                                if dustVault.balance > 0.0 {
1847                                    recipientRef.deposit(from: <- dustVault)
1848                                    self.totalTreasuryForwarded = self.totalTreasuryForwarded + dustAmount
1849                                    emit TreasuryForwarded(poolID: self.poolID, amount: dustAmount, recipient: cap.address)
1850                                } else {
1851                                    destroy dustVault
1852                                }
1853                            }
1854                        }
1855                    }
1856                default:
1857                    panic("Unsupported funding destination")
1858            }
1859        }
1860        
1861        access(all) view fun getFundingStats(): {String: UFix64} {
1862            return {
1863                "totalDirectLottery": self.fundingPolicy.totalDirectLottery,
1864                "totalDirectSavings": self.fundingPolicy.totalDirectSavings,
1865                "maxDirectLottery": self.fundingPolicy.maxDirectLottery ?? 0.0,
1866                "maxDirectSavings": self.fundingPolicy.maxDirectSavings ?? 0.0
1867            }
1868        }
1869        
1870        /// Deposit funds for a receiver. Only callable within contract (by PoolPositionCollection).
1871        access(contract) fun deposit(from: @{FungibleToken.Vault}, receiverID: UInt64) {
1872            pre {
1873                from.balance > 0.0: "Deposit amount must be positive"
1874                from.getType() == self.config.assetType: "Invalid vault type"
1875                self.registeredReceivers[receiverID] != nil: "Receiver not registered"
1876            }
1877            
1878            // TODO: Future batch draw support - add check here:
1879            // assert(self.batchDrawState == nil, message: "Deposits locked during batch draw processing")
1880            
1881            switch self.emergencyState {
1882                case PoolEmergencyState.Normal:
1883                    assert(from.balance >= self.config.minimumDeposit, message: "Below minimum deposit of \(self.config.minimumDeposit)")
1884                case PoolEmergencyState.PartialMode:
1885                    let depositLimit = self.emergencyConfig.partialModeDepositLimit ?? 0.0
1886                    assert(depositLimit > 0.0, message: "Partial mode deposit limit not configured")
1887                    assert(from.balance <= depositLimit, message: "Deposit exceeds partial mode limit of \(depositLimit)")
1888                case PoolEmergencyState.EmergencyMode:
1889                    panic("Deposits disabled in emergency mode. Withdrawals only.")
1890                case PoolEmergencyState.Paused:
1891                    panic("Pool is paused. No operations allowed.")
1892            }
1893            
1894            if self.getAvailableYieldRewards() > 0.0 {
1895                self.processRewards()
1896            }
1897            
1898            let amount = from.balance
1899            self.savingsDistributor.deposit(receiverID: receiverID, amount: amount)
1900            let currentPrincipal = self.receiverDeposits[receiverID] ?? 0.0
1901            self.receiverDeposits[receiverID] = currentPrincipal + amount
1902            self.totalDeposited = self.totalDeposited + amount
1903            self.totalStaked = self.totalStaked + amount
1904            self.config.yieldConnector.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
1905            destroy from
1906            let userAddress = self.receiverAddresses[receiverID] ?? Address(0x0)
1907            emit Deposited(poolID: self.poolID, receiverID: receiverID, userAddress: userAddress, amount: amount)
1908        }
1909        
1910        /// Withdraw funds for a receiver. Only callable within contract (by PoolPositionCollection).
1911        access(contract) fun withdraw(amount: UFix64, receiverID: UInt64): @{FungibleToken.Vault} {
1912            pre {
1913                amount > 0.0: "Withdrawal amount must be positive"
1914                self.registeredReceivers[receiverID] != nil: "Receiver not registered"
1915            }
1916            
1917            // TODO: Future batch draw support - add check here:
1918            // assert(self.batchDrawState == nil, message: "Withdrawals locked during batch draw processing")
1919            
1920            assert(self.emergencyState != PoolEmergencyState.Paused, message: "Pool is paused - no operations allowed")
1921            
1922            if self.emergencyState == PoolEmergencyState.EmergencyMode {
1923                let _ = self.checkAndAutoRecover()
1924            }
1925            
1926            if self.emergencyState == PoolEmergencyState.Normal && self.getAvailableYieldRewards() > 0.0 {
1927                self.processRewards()
1928            }
1929            
1930            let totalBalance = self.savingsDistributor.getUserAssetValue(receiverID: receiverID)
1931            assert(totalBalance >= amount, message: "Insufficient balance. You have \(totalBalance) but trying to withdraw \(amount)")
1932            
1933            let yieldAvailable = self.config.yieldConnector.minimumAvailable()
1934            
1935            let userAddress = self.receiverAddresses[receiverID] ?? Address(0x0)
1936            
1937            if yieldAvailable < amount {
1938                let newFailureCount = self.consecutiveWithdrawFailures
1939                    + (self.emergencyState == PoolEmergencyState.Normal ? 1 : 0)
1940                
1941                emit WithdrawalFailure(
1942                    poolID: self.poolID, 
1943                    receiverID: receiverID,
1944                    userAddress: userAddress,
1945                    amount: amount,
1946                    consecutiveFailures: newFailureCount, 
1947                    yieldAvailable: yieldAvailable
1948                )
1949                
1950                if self.emergencyState == PoolEmergencyState.Normal {
1951                    self.consecutiveWithdrawFailures = newFailureCount
1952                    let _ = self.checkAndAutoTriggerEmergency()
1953                }
1954                
1955                emit Withdrawn(poolID: self.poolID, receiverID: receiverID, userAddress: userAddress, requestedAmount: amount, actualAmount: 0.0)
1956                return <- DeFiActionsUtils.getEmptyVault(self.config.assetType)
1957            }
1958            
1959            let withdrawn <- self.config.yieldConnector.withdrawAvailable(maxAmount: amount)
1960            let actualWithdrawn = withdrawn.balance
1961            
1962            if actualWithdrawn == 0.0 {
1963                let newFailureCount = self.consecutiveWithdrawFailures
1964                    + (self.emergencyState == PoolEmergencyState.Normal ? 1 : 0)
1965                
1966                emit WithdrawalFailure(
1967                    poolID: self.poolID, 
1968                    receiverID: receiverID,
1969                    userAddress: userAddress,
1970                    amount: amount,
1971                    consecutiveFailures: newFailureCount, 
1972                    yieldAvailable: yieldAvailable
1973                )
1974                
1975                if self.emergencyState == PoolEmergencyState.Normal {
1976                    self.consecutiveWithdrawFailures = newFailureCount
1977                    let _ = self.checkAndAutoTriggerEmergency()
1978                }
1979                
1980                emit Withdrawn(poolID: self.poolID, receiverID: receiverID, userAddress: userAddress, requestedAmount: amount, actualAmount: 0.0)
1981                return <- withdrawn
1982            }
1983            
1984            if self.emergencyState == PoolEmergencyState.Normal {
1985                self.consecutiveWithdrawFailures = 0
1986            }
1987            
1988            let _ = self.savingsDistributor.withdraw(receiverID: receiverID, amount: actualWithdrawn)
1989            
1990            let currentPrincipal = self.receiverDeposits[receiverID] ?? 0.0
1991            let interestEarned: UFix64 = totalBalance > currentPrincipal ? totalBalance - currentPrincipal : 0.0
1992            let principalWithdrawn: UFix64 = actualWithdrawn > interestEarned ? actualWithdrawn - interestEarned : 0.0
1993            
1994            if principalWithdrawn > 0.0 {
1995                self.receiverDeposits[receiverID] = currentPrincipal - principalWithdrawn
1996                self.totalDeposited = self.totalDeposited - principalWithdrawn
1997            }
1998            
1999            self.totalStaked = self.totalStaked - actualWithdrawn
2000            
2001            emit Withdrawn(poolID: self.poolID, receiverID: receiverID, userAddress: userAddress, requestedAmount: amount, actualAmount: actualWithdrawn)
2002            return <- withdrawn
2003        }
2004        
2005        access(contract) fun processRewards() {
2006            let yieldBalance = self.config.yieldConnector.minimumAvailable()
2007            let allocatedFunds = self.totalStaked + self.pendingLotteryYield
2008            let availableYield: UFix64 = yieldBalance > allocatedFunds ? yieldBalance - allocatedFunds : 0.0
2009            
2010            if availableYield == 0.0 {
2011                return
2012            }
2013            
2014            let plan = self.config.distributionStrategy.calculateDistribution(totalAmount: availableYield)
2015            
2016            var savingsDust: UFix64 = 0.0
2017            
2018            if plan.savingsAmount > 0.0 {
2019                let actualSavings = self.savingsDistributor.accrueYield(amount: plan.savingsAmount)
2020                savingsDust = plan.savingsAmount - actualSavings
2021                self.totalStaked = self.totalStaked + actualSavings
2022                emit SavingsYieldAccrued(poolID: self.poolID, amount: actualSavings)
2023                
2024                if savingsDust > 0.0 {
2025                    emit SavingsRoundingDustToTreasury(poolID: self.poolID, amount: savingsDust)
2026                }
2027            }
2028            
2029            if plan.lotteryAmount > 0.0 {
2030                self.pendingLotteryYield = self.pendingLotteryYield + plan.lotteryAmount
2031                emit LotteryPrizePoolFunded(
2032                    poolID: self.poolID,
2033                    amount: plan.lotteryAmount,
2034                    source: "yield_pending"
2035                )
2036            }
2037            
2038            // Combine planned treasury amount with savings dust
2039            let totalTreasuryAmount = plan.treasuryAmount + savingsDust
2040            
2041            if totalTreasuryAmount > 0.00001 {
2042                if let cap = self.treasuryRecipientCap {
2043                    if let recipientRef = cap.borrow() {
2044                        let treasuryVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: totalTreasuryAmount)
2045                        let actualAmount = treasuryVault.balance
2046                        if actualAmount > 0.0 {
2047                            recipientRef.deposit(from: <- treasuryVault)
2048                            self.totalTreasuryForwarded = self.totalTreasuryForwarded + actualAmount
2049                            emit TreasuryForwarded(
2050                                poolID: self.poolID,
2051                                amount: actualAmount,
2052                                recipient: cap.address
2053                            )
2054                        } else {
2055                            destroy treasuryVault
2056                        }
2057                    }
2058                }
2059            }
2060            
2061            emit RewardsProcessed(
2062                poolID: self.poolID,
2063                totalAmount: availableYield,
2064                savingsAmount: plan.savingsAmount - savingsDust,
2065                lotteryAmount: plan.lotteryAmount
2066            )
2067        }
2068        
2069        /// Start a lottery draw using balance-seconds (TWAB) for fair weighting.
2070        /// Each user's lottery weight = accumulated (balance × time) during the epoch.
2071        access(all) fun startDraw() {
2072            pre {
2073                self.emergencyState == PoolEmergencyState.Normal: "Draws disabled - pool state: \(self.emergencyState.rawValue)"
2074                self.pendingDrawReceipt == nil: "Draw already in progress"
2075            }
2076            
2077            assert(self.canDrawNow(), message: "Not enough blocks since last draw")
2078            
2079            if self.checkAndAutoTriggerEmergency() {
2080                panic("Emergency mode auto-triggered - cannot start draw")
2081            }
2082            
2083            let timeWeightedStakes: {UInt64: UFix64} = {}
2084            for receiverID in self.registeredReceivers.keys {
2085                let twabStake = self.savingsDistributor.updateAndGetTimeWeightedBalance(receiverID: receiverID)
2086                
2087                // Scale bonus weights by epoch duration
2088                let bonusWeight = self.getBonusWeight(receiverID: receiverID)
2089                let epochDuration = getCurrentBlock().timestamp - self.savingsDistributor.getEpochStartTime()
2090                let scaledBonus = bonusWeight * epochDuration
2091                
2092                let totalStake = twabStake + scaledBonus
2093                if totalStake > 0.0 {
2094                    timeWeightedStakes[receiverID] = totalStake
2095                }
2096            }
2097            
2098            self.savingsDistributor.startNewPeriod()
2099            emit NewEpochStarted(
2100                poolID: self.poolID,
2101                epochID: self.savingsDistributor.getCurrentEpochID(),
2102                startTime: self.savingsDistributor.getEpochStartTime()
2103            )
2104            
2105            // Materialize pending lottery funds from yield source
2106            if self.pendingLotteryYield > 0.0 {
2107                let lotteryVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: self.pendingLotteryYield)
2108                let actualWithdrawn = lotteryVault.balance
2109                self.lotteryDistributor.fundPrizePool(vault: <- lotteryVault)
2110                self.pendingLotteryYield = self.pendingLotteryYield - actualWithdrawn
2111            }
2112            
2113            let prizeAmount = self.lotteryDistributor.getPrizePoolBalance()
2114            assert(prizeAmount > 0.0, message: "No prize pool funds")
2115            
2116            let randomRequest <- self.randomConsumer.requestRandomness()
2117            let receipt <- create PrizeDrawReceipt(
2118                prizeAmount: prizeAmount,
2119                request: <- randomRequest,
2120                timeWeightedStakes: timeWeightedStakes
2121            )
2122            emit PrizeDrawCommitted(
2123                poolID: self.poolID,
2124                prizeAmount: prizeAmount,
2125                commitBlock: receipt.getRequestBlock() ?? 0
2126            )
2127            
2128            self.pendingDrawReceipt <-! receipt
2129            self.lastDrawTimestamp = getCurrentBlock().timestamp
2130        }
2131        
2132        // Batch draw: breaks startDraw() into multiple transactions for scalability
2133        // Flow: startDrawSnapshot() → captureStakesBatch() (repeat) → finalizeDrawStart()
2134        
2135        /// Step 1: Lock the pool and take time snapshot (not yet implemented)
2136        access(CriticalOps) fun startDrawSnapshot() {
2137            pre {
2138                self.emergencyState == PoolEmergencyState.Normal: "Draws disabled"
2139                self.batchDrawState == nil: "Draw already in progress"
2140                self.pendingDrawReceipt == nil: "Receipt exists"
2141            }
2142            panic("Batch draw not yet implemented")
2143        }
2144        
2145        /// Step 2: Calculate stakes for a batch of users (not yet implemented)
2146        access(CriticalOps) fun captureStakesBatch(limit: Int) {
2147            pre {
2148                self.batchDrawState != nil: "No batch draw active"
2149            }
2150            panic("Batch draw not yet implemented")
2151        }
2152        
2153        /// Step 3: Request randomness after all batches processed (not yet implemented)
2154        access(CriticalOps) fun finalizeDrawStart() {
2155            pre {
2156                self.batchDrawState != nil: "No batch draw active"
2157                (self.batchDrawState?.processedCount ?? 0) >= self.registeredReceivers.keys.length: "Batch processing not complete"
2158            }
2159            panic("Batch draw not yet implemented")
2160        }
2161        
2162        access(all) fun completeDraw() {
2163            pre {
2164                self.pendingDrawReceipt != nil: "No draw in progress"
2165            }
2166            
2167            let receipt <- self.pendingDrawReceipt <- nil
2168            let unwrappedReceipt <- receipt!
2169            let totalPrizeAmount = unwrappedReceipt.prizeAmount
2170            
2171            let timeWeightedStakes = unwrappedReceipt.getTimeWeightedStakes()
2172            
2173            let request <- unwrappedReceipt.popRequest()
2174            let randomNumber = self.randomConsumer.fulfillRandomRequest(<- request)
2175            destroy unwrappedReceipt
2176            
2177            let selectionResult = self.config.winnerSelectionStrategy.selectWinners(
2178                randomNumber: randomNumber,
2179                receiverWeights: timeWeightedStakes,
2180                totalPrizeAmount: totalPrizeAmount
2181            )
2182            
2183            let winners = selectionResult.winners
2184            let prizeAmounts = selectionResult.amounts
2185            let nftIDsPerWinner = selectionResult.nftIDs
2186            
2187            if winners.length == 0 {
2188                emit PrizesAwarded(
2189                    poolID: self.poolID,
2190                    winners: [],
2191                    winnerAddresses: [],
2192                    amounts: [],
2193                    round: self.lotteryDistributor.getPrizeRound()
2194                )
2195                return
2196            }
2197            
2198            assert(winners.length == prizeAmounts.length, message: "Winners and prize amounts must match")
2199            assert(winners.length == nftIDsPerWinner.length, message: "Winners and NFT IDs must match")
2200            
2201            let currentRound = self.lotteryDistributor.getPrizeRound() + 1
2202            self.lotteryDistributor.setPrizeRound(round: currentRound)
2203            var totalAwarded: UFix64 = 0.0
2204            
2205            for i in InclusiveRange(0, winners.length - 1) {
2206                let winnerID = winners[i]
2207                let prizeAmount = prizeAmounts[i]
2208                let nftIDsForWinner = nftIDsPerWinner[i]
2209                
2210                let prizeVault <- self.lotteryDistributor.awardPrize(
2211                    receiverID: winnerID,
2212                    amount: prizeAmount,
2213                    yieldSource: nil
2214                )
2215                
2216                self.savingsDistributor.deposit(receiverID: winnerID, amount: prizeAmount)
2217                let currentPrincipal = self.receiverDeposits[winnerID] ?? 0.0
2218                self.receiverDeposits[winnerID] = currentPrincipal + prizeAmount
2219                self.totalDeposited = self.totalDeposited + prizeAmount
2220                self.totalStaked = self.totalStaked + prizeAmount
2221                self.config.yieldConnector.depositCapacity(from: &prizeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
2222                destroy prizeVault
2223                let totalPrizes = self.receiverTotalEarnedPrizes[winnerID] ?? 0.0
2224                self.receiverTotalEarnedPrizes[winnerID] = totalPrizes + prizeAmount
2225                
2226                for nftID in nftIDsForWinner {
2227                    let availableNFTs = self.lotteryDistributor.getAvailableNFTPrizeIDs()
2228                    var nftFound = false
2229                    for availableID in availableNFTs {
2230                        if availableID == nftID {
2231                            nftFound = true
2232                            break
2233                        }
2234                    }
2235                    
2236                    if !nftFound {
2237                        continue
2238                    }
2239                    
2240                    let nft <- self.lotteryDistributor.withdrawNFTPrize(nftID: nftID)
2241                    let nftType = nft.getType().identifier
2242                    self.lotteryDistributor.storePendingNFT(receiverID: winnerID, nft: <- nft)
2243                    
2244                    let winnerAddress = self.receiverAddresses[winnerID] ?? Address(0x0)
2245                    
2246                    emit NFTPrizeStored(
2247                        poolID: self.poolID,
2248                        receiverID: winnerID,
2249                        userAddress: winnerAddress,
2250                        nftID: nftID,
2251                        nftType: nftType,
2252                        reason: "Lottery win - round \(currentRound)"
2253                    )
2254                    
2255                    emit NFTPrizeAwarded(
2256                        poolID: self.poolID,
2257                        receiverID: winnerID,
2258                        userAddress: winnerAddress,
2259                        nftID: nftID,
2260                        nftType: nftType,
2261                        round: currentRound
2262                    )
2263                }
2264                
2265                totalAwarded = totalAwarded + prizeAmount
2266            }
2267            
2268            // Build array of winner addresses
2269            var winnerAddresses: [Address] = []
2270            for winnerID in winners {
2271                // Default to 0x0 if address not found (shouldn't happen for valid receivers)
2272                let address = self.receiverAddresses[winnerID] ?? Address(0x0)
2273                winnerAddresses.append(address)
2274            }
2275            
2276            if let trackerCap = self.config.winnerTrackerCap {
2277                if let trackerRef = trackerCap.borrow() {
2278                    for idx in InclusiveRange(0, winners.length - 1) {
2279                        trackerRef.recordWinner(
2280                            poolID: self.poolID,
2281                            round: currentRound,
2282                            winnerReceiverID: winners[idx],
2283                            winnerAddress: winnerAddresses[idx],
2284                            amount: prizeAmounts[idx],
2285                            nftIDs: nftIDsPerWinner[idx]
2286                        )
2287                    }
2288                }
2289            }
2290            
2291            emit PrizesAwarded(
2292                poolID: self.poolID,
2293                winners: winners,
2294                winnerAddresses: winnerAddresses,
2295                amounts: prizeAmounts,
2296                round: currentRound
2297            )
2298        }
2299        
2300        access(contract) fun getDistributionStrategyName(): String {
2301            return self.config.distributionStrategy.getStrategyName()
2302        }
2303        
2304        access(contract) fun setDistributionStrategy(strategy: {DistributionStrategy}) {
2305            self.config.setDistributionStrategy(strategy: strategy)
2306        }
2307        
2308        access(contract) fun getWinnerSelectionStrategyName(): String {
2309            return self.config.winnerSelectionStrategy.getStrategyName()
2310        }
2311        
2312        access(contract) fun setWinnerSelectionStrategy(strategy: {WinnerSelectionStrategy}) {
2313            self.config.setWinnerSelectionStrategy(strategy: strategy)
2314        }
2315        
2316        access(all) fun hasWinnerTracker(): Bool {
2317            return self.config.winnerTrackerCap != nil
2318        }
2319        
2320        access(contract) fun setWinnerTrackerCap(cap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?) {
2321            self.config.setWinnerTrackerCap(cap: cap)
2322        }
2323        
2324        access(contract) fun setDrawIntervalSeconds(interval: UFix64) {
2325            assert(!self.isDrawInProgress(), message: "Cannot change draw interval during an active draw")
2326            self.config.setDrawIntervalSeconds(interval: interval)
2327        }
2328        
2329        access(contract) fun setMinimumDeposit(minimum: UFix64) {
2330            self.config.setMinimumDeposit(minimum: minimum)
2331        }
2332        
2333        access(contract) fun setBonusWeight(receiverID: UInt64, bonusWeight: UFix64, reason: String, setBy: Address) {
2334            let timestamp = getCurrentBlock().timestamp
2335            let record = BonusWeightRecord(bonusWeight: bonusWeight, reason: reason, addedBy: setBy)
2336            self.receiverBonusWeights[receiverID] = record
2337            
2338            let userAddress = self.receiverAddresses[receiverID] ?? Address(0x0)
2339            emit BonusLotteryWeightSet(
2340                poolID: self.poolID,
2341                receiverID: receiverID,
2342                userAddress: userAddress,
2343                bonusWeight: bonusWeight,
2344                reason: reason,
2345                setBy: setBy,
2346                timestamp: timestamp
2347            )
2348        }
2349        
2350        access(contract) fun addBonusWeight(receiverID: UInt64, additionalWeight: UFix64, reason: String, addedBy: Address) {
2351            let timestamp = getCurrentBlock().timestamp
2352            let currentBonus = self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0
2353            let newTotalBonus = currentBonus + additionalWeight
2354            
2355            let record = BonusWeightRecord(bonusWeight: newTotalBonus, reason: reason, addedBy: addedBy)
2356            self.receiverBonusWeights[receiverID] = record
2357            
2358            let userAddress = self.receiverAddresses[receiverID] ?? Address(0x0)
2359            emit BonusLotteryWeightAdded(
2360                poolID: self.poolID,
2361                receiverID: receiverID,
2362                userAddress: userAddress,
2363                additionalWeight: additionalWeight,
2364                newTotalBonus: newTotalBonus,
2365                reason: reason,
2366                addedBy: addedBy,
2367                timestamp: timestamp
2368            )
2369        }
2370        
2371        access(contract) fun removeBonusWeight(receiverID: UInt64, removedBy: Address) {
2372            let timestamp = getCurrentBlock().timestamp
2373            let previousBonus = self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0
2374            
2375            let _ = self.receiverBonusWeights.remove(key: receiverID)
2376            
2377            let userAddress = self.receiverAddresses[receiverID] ?? Address(0x0)
2378            emit BonusLotteryWeightRemoved(
2379                poolID: self.poolID,
2380                receiverID: receiverID,
2381                userAddress: userAddress,
2382                previousBonus: previousBonus,
2383                removedBy: removedBy,
2384                timestamp: timestamp
2385            )
2386        }
2387        
2388        access(all) view fun getBonusWeight(receiverID: UInt64): UFix64 {
2389            return self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0
2390        }
2391        
2392        access(all) view fun getBonusWeightRecord(receiverID: UInt64): BonusWeightRecord? {
2393            return self.receiverBonusWeights[receiverID]
2394        }
2395        
2396        access(all) view fun getAllBonusWeightReceivers(): [UInt64] {
2397            return self.receiverBonusWeights.keys
2398        }
2399        
2400        access(contract) fun depositNFTPrize(nft: @{NonFungibleToken.NFT}) {
2401            self.lotteryDistributor.depositNFTPrize(nft: <- nft)
2402        }
2403        
2404        access(contract) fun withdrawNFTPrize(nftID: UInt64): @{NonFungibleToken.NFT} {
2405            return <- self.lotteryDistributor.withdrawNFTPrize(nftID: nftID)
2406        }
2407        
2408        access(all) view fun getAvailableNFTPrizeIDs(): [UInt64] {
2409            return self.lotteryDistributor.getAvailableNFTPrizeIDs()
2410        }
2411        
2412        access(all) view fun borrowAvailableNFTPrize(nftID: UInt64): &{NonFungibleToken.NFT}? {
2413            return self.lotteryDistributor.borrowNFTPrize(nftID: nftID)
2414        }
2415        
2416        access(all) view fun getPendingNFTCount(receiverID: UInt64): Int {
2417            return self.lotteryDistributor.getPendingNFTCount(receiverID: receiverID)
2418        }
2419        
2420        access(all) fun getPendingNFTIDs(receiverID: UInt64): [UInt64] {
2421            return self.lotteryDistributor.getPendingNFTIDs(receiverID: receiverID)
2422        }
2423        
2424        /// Claim pending NFT prize. Only callable within contract (by PoolPositionCollection).
2425        access(contract) fun claimPendingNFT(receiverID: UInt64, nftIndex: Int): @{NonFungibleToken.NFT} {
2426            let nft <- self.lotteryDistributor.claimPendingNFT(receiverID: receiverID, nftIndex: nftIndex)
2427            let nftType = nft.getType().identifier
2428            
2429            let userAddress = self.receiverAddresses[receiverID] ?? Address(0x0)
2430            emit NFTPrizeClaimed(
2431                poolID: self.poolID,
2432                receiverID: receiverID,
2433                userAddress: userAddress,
2434                nftID: nft.uuid,
2435                nftType: nftType
2436            )
2437            
2438            return <- nft
2439        }
2440        
2441        access(all) view fun canDrawNow(): Bool {
2442            return (getCurrentBlock().timestamp - self.lastDrawTimestamp) >= self.config.drawIntervalSeconds
2443        }
2444        
2445        /// Returns the "no-loss guarantee" amount: user deposits + auto-compounded lottery prizes
2446        /// This is the minimum amount users can always withdraw (excludes savings interest)
2447        access(all) view fun getReceiverDeposit(receiverID: UInt64): UFix64 {
2448            return self.receiverDeposits[receiverID] ?? 0.0
2449        }
2450        
2451        /// Returns total withdrawable balance (principal + interest)
2452        access(all) view fun getReceiverTotalBalance(receiverID: UInt64): UFix64 {
2453            return self.savingsDistributor.getUserAssetValue(receiverID: receiverID)
2454        }
2455        
2456        /// Returns lifetime total lottery prizes earned by this receiver.
2457        /// This is a cumulative counter that increases when prizes are won.
2458        access(all) view fun getReceiverTotalEarnedPrizes(receiverID: UInt64): UFix64 {
2459            return self.receiverTotalEarnedPrizes[receiverID] ?? 0.0
2460        }
2461        
2462        /// Returns current pending savings interest (not yet withdrawn).
2463        /// This is NOT lifetime total - it's the current accrued interest that
2464        /// will be included in the next withdrawal.
2465        /// Formula: totalBalance - (deposits + prizes)
2466        /// Note: "principal" here includes both user deposits AND auto-compounded lottery prizes
2467        access(all) view fun getPendingSavingsInterest(receiverID: UInt64): UFix64 {
2468            let principal = self.receiverDeposits[receiverID] ?? 0.0  // deposits + prizes
2469            let totalBalance = self.savingsDistributor.getUserAssetValue(receiverID: receiverID)
2470            return totalBalance > principal ? totalBalance - principal : 0.0
2471        }
2472        
2473        access(all) view fun getUserSavingsShares(receiverID: UInt64): UFix64 {
2474            return self.savingsDistributor.getUserShares(receiverID: receiverID)
2475        }
2476        
2477        access(all) view fun getTotalSavingsShares(): UFix64 {
2478            return self.savingsDistributor.getTotalShares()
2479        }
2480        
2481        access(all) view fun getTotalSavingsAssets(): UFix64 {
2482            return self.savingsDistributor.getTotalAssets()
2483        }
2484        
2485        access(all) view fun getSavingsSharePrice(): UFix64 {
2486            let totalShares = self.savingsDistributor.getTotalShares()
2487            let totalAssets = self.savingsDistributor.getTotalAssets()
2488            let effectiveShares = totalShares + PrizeSavings.VIRTUAL_SHARES
2489            let effectiveAssets = totalAssets + PrizeSavings.VIRTUAL_ASSETS
2490            return effectiveAssets / effectiveShares
2491        }
2492        
2493        access(all) view fun getUserTimeWeightedBalance(receiverID: UInt64): UFix64 {
2494            return self.savingsDistributor.getTimeWeightedBalance(receiverID: receiverID)
2495        }
2496        
2497        access(all) view fun getCurrentEpochID(): UInt64 {
2498            return self.savingsDistributor.getCurrentEpochID()
2499        }
2500        
2501        access(all) view fun getEpochStartTime(): UFix64 {
2502            return self.savingsDistributor.getEpochStartTime()
2503        }
2504        
2505        access(all) view fun getEpochElapsedTime(): UFix64 {
2506            return getCurrentBlock().timestamp - self.savingsDistributor.getEpochStartTime()
2507        }
2508        
2509        /// Batch draw support
2510        access(all) view fun isBatchDrawInProgress(): Bool {
2511            return self.batchDrawState != nil
2512        }
2513        
2514        access(all) view fun getBatchDrawProgress(): {String: AnyStruct}? {
2515            if let state = self.batchDrawState {
2516                return {
2517                    "cutoffTime": state.drawCutoffTime,
2518                    "totalWeight": state.totalWeight,
2519                    "processedCount": state.processedCount,
2520                    "snapshotEpochID": state.snapshotEpochID
2521                }
2522            }
2523            return nil
2524        }
2525        
2526        access(all) view fun getUserProjectedBalanceSeconds(receiverID: UInt64, atTime: UFix64): UFix64 {
2527            return self.savingsDistributor.calculateBalanceSecondsAtTime(receiverID: receiverID, targetTime: atTime)
2528        }
2529        
2530        /// Preview how many shares would be minted for a deposit amount (ERC-4626 style)
2531        access(all) view fun previewDeposit(amount: UFix64): UFix64 {
2532            return self.savingsDistributor.convertToShares(amount)
2533        }
2534        
2535        /// Preview how many assets a number of shares is worth (ERC-4626 style)
2536        access(all) view fun previewRedeem(shares: UFix64): UFix64 {
2537            return self.savingsDistributor.convertToAssets(shares)
2538        }
2539        
2540        access(all) view fun getUserSavingsValue(receiverID: UInt64): UFix64 {
2541            return self.savingsDistributor.getUserAssetValue(receiverID: receiverID)
2542        }
2543        
2544        access(all) view fun isReceiverRegistered(receiverID: UInt64): Bool {
2545            return self.registeredReceivers[receiverID] != nil
2546        }
2547        
2548        access(all) view fun getRegisteredReceiverIDs(): [UInt64] {
2549            return self.registeredReceivers.keys
2550        }
2551        
2552        access(all) view fun isDrawInProgress(): Bool {
2553            return self.pendingDrawReceipt != nil
2554        }
2555        
2556        access(all) view fun getConfig(): PoolConfig {
2557            return self.config
2558        }
2559        
2560        access(all) view fun getTotalSavingsDistributed(): UFix64 {
2561            return self.savingsDistributor.getTotalDistributed()
2562        }
2563        
2564        access(all) view fun getCurrentReinvestedSavings(): UFix64 {
2565            if self.totalStaked > self.totalDeposited {
2566                return self.totalStaked - self.totalDeposited
2567            }
2568            return 0.0
2569        }
2570        
2571        access(all) fun getAvailableYieldRewards(): UFix64 {
2572            let yieldSource = &self.config.yieldConnector as &{DeFiActions.Source}
2573            let available = yieldSource.minimumAvailable()
2574            // Exclude already-allocated funds (same logic as processRewards)
2575            let allocatedFunds = self.totalStaked + self.pendingLotteryYield
2576            if available > allocatedFunds {
2577                return available - allocatedFunds
2578            }
2579            return 0.0
2580        }
2581        
2582        access(all) view fun getLotteryPoolBalance(): UFix64 {
2583            return self.lotteryDistributor.getPrizePoolBalance() + self.pendingLotteryYield
2584        }
2585        
2586        access(all) view fun getPendingLotteryYield(): UFix64 {
2587            return self.pendingLotteryYield
2588        }
2589        
2590        access(all) view fun getTreasuryRecipient(): Address? {
2591            return self.treasuryRecipientCap?.address
2592        }
2593        
2594        access(all) view fun hasTreasuryRecipient(): Bool {
2595            if let cap = self.treasuryRecipientCap {
2596                return cap.check()
2597            }
2598            return false
2599        }
2600        
2601        access(all) view fun getTotalTreasuryForwarded(): UFix64 {
2602            return self.totalTreasuryForwarded
2603        }
2604        
2605        /// Set treasury recipient for auto-forwarding. Only callable by account owner.
2606        access(contract) fun setTreasuryRecipient(cap: Capability<&{FungibleToken.Receiver}>?) {
2607            self.treasuryRecipientCap = cap
2608        }
2609        
2610        // ============================================================
2611        // ENTRY VIEW FUNCTIONS - Human-readable UI helpers
2612        // ============================================================
2613        // "Entries" represent the user's lottery weight for the current draw.
2614        // Formula: entries = projectedTWAB / drawInterval
2615        // 
2616        // This normalizes balance-seconds to human-readable whole numbers:
2617        // - $10 deposited at start of 7-day draw → 10 entries
2618        // - $10 deposited halfway through → 5 entries (prorated for this draw)
2619        // - At next round: same $10 deposit → 10 entries (full period credit)
2620        //
2621        // The TWAB naturally handles:
2622        // - Multiple deposits at different times (weighted correctly)
2623        // - Partial withdrawals (reduces entry count)
2624        // - Yield compounding (higher balance = more entries over time)
2625        // ============================================================
2626        
2627        /// Internal: Returns the user's current accumulated entries (TWAB / drawInterval).
2628        /// This represents their lottery weight accumulated so far, NOT their final weight.
2629        access(self) view fun getCurrentEntries(receiverID: UInt64): UFix64 {
2630            let twab = self.savingsDistributor.getTimeWeightedBalance(receiverID: receiverID)
2631            let drawInterval = self.config.drawIntervalSeconds
2632            if drawInterval == 0.0 {
2633                return 0.0
2634            }
2635            return twab / drawInterval
2636        }
2637        
2638        /// Returns the user's entry count for this draw.
2639        /// Projects TWAB forward to epoch end assuming no balance changes.
2640        /// Formula: (currentTWAB + balance × remainingEpochTime) / drawInterval
2641        /// 
2642        /// Examples:
2643        /// - $10 deposited at start of draw → 10 entries
2644        /// - $10 deposited halfway through draw → 5 entries
2645        /// - At next round, same $10 → 10 entries (full credit)
2646        access(all) view fun getUserEntries(receiverID: UInt64): UFix64 {
2647            let drawInterval = self.config.drawIntervalSeconds
2648            if drawInterval == 0.0 {
2649                return 0.0
2650            }
2651            
2652            let currentTwab = self.savingsDistributor.getTimeWeightedBalance(receiverID: receiverID)
2653            let currentBalance = self.savingsDistributor.getUserAssetValue(receiverID: receiverID)
2654            let remainingEpochTime = self.getTimeRemainingInEpoch()
2655            
2656            // Project TWAB forward to epoch end: current + (balance × remaining epoch time)
2657            let projectedTwab = currentTwab + (currentBalance * remainingEpochTime)
2658            return projectedTwab / drawInterval
2659        }
2660        
2661        /// Returns time remaining in the current epoch (for TWAB projection).
2662        /// Different from getTimeUntilNextDraw() which is based on draw cooldown.
2663        access(all) view fun getTimeRemainingInEpoch(): UFix64 {
2664            let epochStart = self.savingsDistributor.getEpochStartTime()
2665            let elapsed = getCurrentBlock().timestamp - epochStart
2666            let drawInterval = self.config.drawIntervalSeconds
2667            if elapsed >= drawInterval {
2668                return 0.0
2669            }
2670            return drawInterval - elapsed
2671        }
2672        
2673        /// Returns how far through the current draw period we are (0.0 to 1.0+).
2674        /// - 0.0 = draw just started
2675        /// - 0.5 = halfway through draw period
2676        /// - 1.0 = draw period complete, ready for next draw
2677        access(all) view fun getDrawProgressPercent(): UFix64 {
2678            let epochStart = self.savingsDistributor.getEpochStartTime()
2679            let elapsed = getCurrentBlock().timestamp - epochStart
2680            let drawInterval = self.config.drawIntervalSeconds
2681            if drawInterval == 0.0 {
2682                return 0.0
2683            }
2684            return elapsed / drawInterval
2685        }
2686        
2687        /// Returns time remaining until next draw is available (in seconds).
2688        /// Returns 0.0 if draw can happen now.
2689        access(all) view fun getTimeUntilNextDraw(): UFix64 {
2690            let elapsed = getCurrentBlock().timestamp - self.lastDrawTimestamp
2691            let drawInterval = self.config.drawIntervalSeconds
2692            if elapsed >= drawInterval {
2693                return 0.0
2694            }
2695            return drawInterval - elapsed
2696        }
2697        
2698    }
2699    
2700    access(all) struct PoolBalance {
2701        /// User deposits + auto-compounded prizes (the "no-loss guarantee" amount, decreases on withdrawal)
2702        access(all) let deposits: UFix64
2703        /// Lifetime total of lottery prizes won (cumulative, never decreases)
2704        access(all) let totalEarnedPrizes: UFix64
2705        /// Current pending savings interest (will be included in next withdrawal)
2706        access(all) let savingsEarned: UFix64
2707        /// Total withdrawable balance: deposits + savingsEarned
2708        access(all) let totalBalance: UFix64
2709        
2710        init(deposits: UFix64, totalEarnedPrizes: UFix64, savingsEarned: UFix64) {
2711            self.deposits = deposits
2712            self.totalEarnedPrizes = totalEarnedPrizes
2713            self.savingsEarned = savingsEarned
2714            self.totalBalance = deposits + savingsEarned
2715        }
2716    }
2717    
2718    /// Public interface for PoolPositionCollection - exposes read-only functions
2719    access(all) resource interface PoolPositionCollectionPublic {
2720        access(all) view fun getRegisteredPoolIDs(): [UInt64]
2721        access(all) view fun isRegisteredWithPool(poolID: UInt64): Bool
2722        access(all) view fun getReceiverID(): UInt64
2723        access(all) view fun getPendingNFTCount(poolID: UInt64): Int
2724        access(all) fun getPendingNFTIDs(poolID: UInt64): [UInt64]
2725        access(all) view fun getPendingSavingsInterest(poolID: UInt64): UFix64
2726        access(all) fun getPoolBalance(poolID: UInt64): PoolBalance
2727        access(all) view fun getPoolEntries(poolID: UInt64): UFix64
2728    }
2729    
2730    /// PoolPositionCollection represents a user's position across all prize pools.
2731    /// 
2732    /// ⚠️ IMPORTANT: This resource's UUID serves as the account key for all deposits.
2733    /// - All funds, shares, prizes, and NFTs are keyed to this resource's UUID
2734    /// - If this resource is destroyed or the account loses access, those funds become INACCESSIBLE
2735    /// - There is NO built-in recovery mechanism without admin intervention
2736    /// - Users should treat this resource like a wallet private key
2737    /// 
2738    /// For production deployments:
2739    /// - Implement clear UI warnings about this behavior
2740    /// - Consider enabling emergency admin recovery for extreme cases
2741    /// - Backup account access is critical
2742    access(all) resource PoolPositionCollection: PoolPositionCollectionPublic {
2743        access(self) let registeredPools: {UInt64: Bool}
2744        
2745        init() {
2746            self.registeredPools = {}
2747        }
2748        
2749        access(self) fun registerWithPool(poolID: UInt64) {
2750            pre {
2751                self.registeredPools[poolID] == nil: "Already registered"
2752            }
2753            
2754            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
2755                ?? panic("Pool does not exist")
2756            
2757            // Use self.owner!.address - resource is already stored at this point
2758            poolRef.registerReceiver(receiverID: self.uuid, ownerAddress: self.owner!.address)
2759            self.registeredPools[poolID] = true
2760        }
2761        
2762        access(all) view fun getRegisteredPoolIDs(): [UInt64] {
2763            return self.registeredPools.keys
2764        }
2765        
2766        access(all) view fun isRegisteredWithPool(poolID: UInt64): Bool {
2767            return self.registeredPools[poolID] == true
2768        }
2769        
2770        access(PositionOps) fun deposit(poolID: UInt64, from: @{FungibleToken.Vault}) {
2771            if self.registeredPools[poolID] == nil {
2772                self.registerWithPool(poolID: poolID)
2773            }
2774            
2775            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
2776                ?? panic("Cannot borrow pool")
2777            
2778            poolRef.deposit(from: <- from, receiverID: self.uuid)
2779        }
2780        
2781        access(PositionOps) fun withdraw(poolID: UInt64, amount: UFix64): @{FungibleToken.Vault} {
2782            pre {
2783                self.registeredPools[poolID] == true: "Not registered with pool"
2784            }
2785            
2786            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
2787                ?? panic("Cannot borrow pool")
2788            
2789            return <- poolRef.withdraw(amount: amount, receiverID: self.uuid)
2790        }
2791        
2792        access(PositionOps) fun claimPendingNFT(poolID: UInt64, nftIndex: Int): @{NonFungibleToken.NFT} {
2793            pre {
2794                self.registeredPools[poolID] == true: "Not registered with pool"
2795            }
2796            
2797            let poolRef = PrizeSavings.borrowPoolInternal(poolID)
2798                ?? panic("Cannot borrow pool")
2799            
2800            return <- poolRef.claimPendingNFT(receiverID: self.uuid, nftIndex: nftIndex)
2801        }
2802        
2803        access(all) view fun getPendingNFTCount(poolID: UInt64): Int {
2804            if let poolRef = PrizeSavings.borrowPool(poolID: poolID) {
2805                return poolRef.getPendingNFTCount(receiverID: self.uuid)
2806            }
2807            return 0
2808        }
2809        
2810        access(all) fun getPendingNFTIDs(poolID: UInt64): [UInt64] {
2811            if let poolRef = PrizeSavings.borrowPool(poolID: poolID) {
2812                return poolRef.getPendingNFTIDs(receiverID: self.uuid)
2813            }
2814            return []
2815        }
2816        
2817        access(all) view fun getReceiverID(): UInt64 {
2818            return self.uuid
2819        }
2820        
2821        access(all) view fun getPendingSavingsInterest(poolID: UInt64): UFix64 {
2822            if let poolRef = PrizeSavings.borrowPool(poolID: poolID) {
2823                return poolRef.getPendingSavingsInterest(receiverID: self.uuid)
2824            }
2825            return 0.0
2826        }
2827        
2828        access(all) fun getPoolBalance(poolID: UInt64): PoolBalance {
2829            if self.registeredPools[poolID] == nil {
2830                return PoolBalance(deposits: 0.0, totalEarnedPrizes: 0.0, savingsEarned: 0.0)
2831            }
2832            
2833            if let poolRef = PrizeSavings.borrowPool(poolID: poolID) {
2834                return PoolBalance(
2835                    deposits: poolRef.getReceiverDeposit(receiverID: self.uuid),
2836                    totalEarnedPrizes: poolRef.getReceiverTotalEarnedPrizes(receiverID: self.uuid),
2837                    savingsEarned: poolRef.getPendingSavingsInterest(receiverID: self.uuid)
2838                )
2839            }
2840            return PoolBalance(deposits: 0.0, totalEarnedPrizes: 0.0, savingsEarned: 0.0)
2841        }
2842        
2843        /// Returns the user's entry count for the current draw.
2844        /// Entries represent lottery weight - higher entries = better odds.
2845        /// Example: $10 deposited at start = 10 entries, $10 halfway = 5 entries
2846        access(all) view fun getPoolEntries(poolID: UInt64): UFix64 {
2847            if let poolRef = PrizeSavings.borrowPool(poolID: poolID) {
2848                return poolRef.getUserEntries(receiverID: self.uuid)
2849            }
2850            return 0.0
2851        }
2852    }
2853    
2854    access(contract) fun createPool(
2855        config: PoolConfig,
2856        emergencyConfig: EmergencyConfig?,
2857        fundingPolicy: FundingPolicy?
2858    ): UInt64 {
2859        let pool <- create Pool(
2860            config: config, 
2861            emergencyConfig: emergencyConfig,
2862            fundingPolicy: fundingPolicy
2863        )
2864        
2865        let poolID = self.nextPoolID
2866        self.nextPoolID = self.nextPoolID + 1
2867        
2868        pool.setPoolID(id: poolID)
2869        emit PoolCreated(
2870            poolID: poolID,
2871            assetType: config.assetType.identifier,
2872            strategy: config.distributionStrategy.getStrategyName()
2873        )
2874        
2875        self.pools[poolID] <-! pool
2876        return poolID
2877    }
2878    
2879    /// Public view-only reference to a pool (no mutation allowed)
2880    access(all) view fun borrowPool(poolID: UInt64): &Pool? {
2881        return &self.pools[poolID]
2882    }
2883    
2884    /// Internal borrow with full authorization for Admin operations
2885    access(contract) fun borrowPoolInternal(_ poolID: UInt64): auth(CriticalOps, ConfigOps) &Pool? {
2886        return &self.pools[poolID]
2887    }
2888    
2889    access(all) view fun getAllPoolIDs(): [UInt64] {
2890        return self.pools.keys
2891    }
2892    
2893    access(all) fun createPoolPositionCollection(): @PoolPositionCollection {
2894        return <- create PoolPositionCollection()
2895    }
2896    
2897    // ============================================================
2898    // ENTRY QUERY FUNCTIONS - Contract-level convenience accessors
2899    // ============================================================
2900    // These functions provide easy access to entry information for UI/scripts.
2901    // "Entries" represent the user's lottery weight for the current draw:
2902    // - entries = projectedTWAB / drawInterval
2903    // - $10 deposited at start of draw = 10 entries
2904    // - $10 deposited halfway through = 5 entries (prorated)
2905    // ============================================================
2906    
2907    /// Get a user's entry count for the current draw.
2908    /// Projects TWAB forward to draw time assuming no balance changes.
2909    access(all) view fun getUserEntries(poolID: UInt64, receiverID: UInt64): UFix64 {
2910        if let poolRef = self.borrowPool(poolID: poolID) {
2911            return poolRef.getUserEntries(receiverID: receiverID)
2912        }
2913        return 0.0
2914    }
2915    
2916    /// Get the draw progress as a percentage (0.0 to 1.0+).
2917    access(all) view fun getDrawProgressPercent(poolID: UInt64): UFix64 {
2918        if let poolRef = self.borrowPool(poolID: poolID) {
2919            return poolRef.getDrawProgressPercent()
2920        }
2921        return 0.0
2922    }
2923    
2924    /// Get time remaining until next draw (in seconds).
2925    access(all) view fun getTimeUntilNextDraw(poolID: UInt64): UFix64 {
2926        if let poolRef = self.borrowPool(poolID: poolID) {
2927            return poolRef.getTimeUntilNextDraw()
2928        }
2929        return 0.0
2930    }
2931    
2932    /// Get time remaining in the current epoch (in seconds).
2933    /// Based on epoch start time (for TWAB projection).
2934    access(all) view fun getTimeRemainingInEpoch(poolID: UInt64): UFix64 {
2935        if let poolRef = self.borrowPool(poolID: poolID) {
2936            return poolRef.getTimeRemainingInEpoch()
2937        }
2938        return 0.0
2939    }
2940    
2941    init() {
2942        // Virtual offset constants for ERC4626 inflation attack protection.
2943        // Using 0.0001 to minimize dilution (~0.0001%) while still protecting against
2944        self.VIRTUAL_SHARES = 0.0001
2945        self.VIRTUAL_ASSETS = 0.0001
2946        
2947        self.PoolPositionCollectionStoragePath = /storage/PrizeSavingsCollection_v2
2948        self.PoolPositionCollectionPublicPath = /public/PrizeSavingsCollection_v2
2949        
2950        self.AdminStoragePath = /storage/PrizeSavingsAdmin
2951        
2952        self.pools <- {}
2953        self.nextPoolID = 0
2954        
2955        let admin <- create Admin()
2956        self.account.storage.save(<-admin, to: self.AdminStoragePath)
2957    }
2958}