Smart Contract

PrizeSavings

A.b2b7a4c7033eaa24.PrizeSavings

Valid From

134,199,160

Deployed

6d ago
Feb 22, 2026, 02:18:54 AM UTC

Dependents

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