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