DeploySEALED

■◇█░%░■▫%&▪$▫▪◆▒!●╱^%▫~◇#■@&#*▪$░▒#@▒░@&@█╳^^█&◆$╳&╲#@#@@◆╲%▒▓%^

Transaction ID

Timestamp

Nov 28, 2025, 12:02:32 AM UTC
3mo ago

Block Height

134,199,160

Computation

0

Execution Fee

0.00017369 FLOW

Transaction Summary

Deploy

Contract deployment

Contract deployment

Script Arguments

0nameString
PrizeSavings
1codeString
/* PrizeSavings - Prize-Linked Savings Protocol No-loss lottery where users deposit tokens to earn guaranteed savings interest and lottery prizes. Rewards auto-compound into deposits via ERC4626-style shares model. Architecture: - ERC4626-style shares for O(1) interest distribution - TWAB (time-weighted average balance) for fair lottery weighting - On-chain randomness via Flow's RandomConsumer - Modular yield sources via DeFi Actions interface - Configurable distribution strategies (savings/lottery/treasury split) - Pluggable winner selection (weighted single, multi-winner, fixed tiers) - Emergency mode with auto-recovery and health monitoring - NFT prize support with pending claims - Direct funding for external sponsors - Bonus lottery weights for promotions - Winner tracking integration for leaderboards Core Components: - SavingsDistributor: Shares vault with epoch-based stake tracking - LotteryDistributor: Prize pool, NFT prizes, and draw execution - TreasuryDistributor: Protocol reserves and fee collection - Pool: Deposits, withdrawals, yield processing, and prize draws */ import FungibleToken from 0xf233dcee88fe0abe import NonFungibleToken from 0x1d7e57aa55817448 import RandomConsumer from 0x45caec600164c9e6 import DeFiActions from 0x92195d814edf9cb0 import DeFiActionsUtils from 0x92195d814edf9cb0 import PrizeWinnerTracker from 0x262cf58c0b9fbcff import Xorshift128plus from 0xb2b7a4c7033eaa24 access(all) contract PrizeSavings { access(all) entitlement ConfigOps access(all) entitlement CriticalOps access(all) let PoolPositionCollectionStoragePath: StoragePath access(all) let PoolPositionCollectionPublicPath: PublicPath access(all) event PoolCreated(poolID: UInt64, assetType: String, strategy: String) access(all) event Deposited(poolID: UInt64, receiverID: UInt64, amount: UFix64) access(all) event Withdrawn(poolID: UInt64, receiverID: UInt64, amount: UFix64) access(all) event RewardsProcessed(poolID: UInt64, totalAmount: UFix64, savingsAmount: UFix64, lotteryAmount: UFix64) access(all) event SavingsYieldAccrued(poolID: UInt64, amount: UFix64) access(all) event SavingsInterestCompounded(poolID: UInt64, receiverID: UInt64, amount: UFix64) access(all) event SavingsInterestCompoundedBatch(poolID: UInt64, userCount: Int, totalAmount: UFix64, avgAmount: UFix64) access(all) event SavingsRoundingDustToTreasury(poolID: UInt64, amount: UFix64) access(all) event PrizeDrawCommitted(poolID: UInt64, prizeAmount: UFix64, commitBlock: UInt64) access(all) event PrizesAwarded(poolID: UInt64, winners: [UInt64], amounts: [UFix64], round: UInt64) access(all) event LotteryPrizePoolFunded(poolID: UInt64, amount: UFix64, source: String) access(all) event NewEpochStarted(poolID: UInt64, epochID: UInt64, startTime: UFix64) access(all) event DistributionStrategyUpdated(poolID: UInt64, oldStrategy: String, newStrategy: String, updatedBy: Address) access(all) event WinnerSelectionStrategyUpdated(poolID: UInt64, oldStrategy: String, newStrategy: String, updatedBy: Address) access(all) event WinnerTrackerUpdated(poolID: UInt64, hasOldTracker: Bool, hasNewTracker: Bool, updatedBy: Address) access(all) event DrawIntervalUpdated(poolID: UInt64, oldInterval: UFix64, newInterval: UFix64, updatedBy: Address) access(all) event MinimumDepositUpdated(poolID: UInt64, oldMinimum: UFix64, newMinimum: UFix64, updatedBy: Address) access(all) event PoolCreatedByAdmin(poolID: UInt64, assetType: String, strategy: String, createdBy: Address) access(all) event PoolPaused(poolID: UInt64, pausedBy: Address, reason: String) access(all) event PoolUnpaused(poolID: UInt64, unpausedBy: Address) access(all) event TreasuryFunded(poolID: UInt64, amount: UFix64, source: String) access(all) event TreasuryWithdrawn(poolID: UInt64, withdrawnBy: Address, amount: UFix64, purpose: String, remainingBalance: UFix64) access(all) event BonusLotteryWeightSet(poolID: UInt64, receiverID: UInt64, bonusWeight: UFix64, reason: String, setBy: Address, timestamp: UFix64) access(all) event BonusLotteryWeightAdded(poolID: UInt64, receiverID: UInt64, additionalWeight: UFix64, newTotalBonus: UFix64, reason: String, addedBy: Address, timestamp: UFix64) access(all) event BonusLotteryWeightRemoved(poolID: UInt64, receiverID: UInt64, previousBonus: UFix64, removedBy: Address, timestamp: UFix64) access(all) event NFTPrizeDeposited(poolID: UInt64, nftID: UInt64, nftType: String, depositedBy: Address) access(all) event NFTPrizeAwarded(poolID: UInt64, receiverID: UInt64, nftID: UInt64, nftType: String, round: UInt64) access(all) event NFTPrizeStored(poolID: UInt64, receiverID: UInt64, nftID: UInt64, nftType: String, reason: String) access(all) event NFTPrizeClaimed(poolID: UInt64, receiverID: UInt64, nftID: UInt64, nftType: String) access(all) event NFTPrizeWithdrawn(poolID: UInt64, nftID: UInt64, nftType: String, withdrawnBy: Address) access(all) event PoolEmergencyEnabled(poolID: UInt64, reason: String, enabledBy: Address, timestamp: UFix64) access(all) event PoolEmergencyDisabled(poolID: UInt64, disabledBy: Address, timestamp: UFix64) access(all) event PoolPartialModeEnabled(poolID: UInt64, reason: String, setBy: Address, timestamp: UFix64) access(all) event EmergencyModeAutoTriggered(poolID: UInt64, reason: String, healthScore: UFix64, timestamp: UFix64) access(all) event EmergencyModeAutoRecovered(poolID: UInt64, reason: String, healthScore: UFix64?, duration: UFix64?, timestamp: UFix64) access(all) event EmergencyConfigUpdated(poolID: UInt64, updatedBy: Address) access(all) event WithdrawalFailure(poolID: UInt64, receiverID: UInt64, amount: UFix64, consecutiveFailures: Int, yieldAvailable: UFix64) access(all) event DirectFundingReceived(poolID: UInt64, destination: UInt8, destinationName: String, amount: UFix64, sponsor: Address, purpose: String, metadata: {String: String}) access(self) var pools: @{UInt64: Pool} access(self) var nextPoolID: UInt64 access(all) enum PoolEmergencyState: UInt8 { access(all) case Normal access(all) case Paused access(all) case EmergencyMode access(all) case PartialMode } access(all) enum PoolFundingDestination: UInt8 { access(all) case Savings access(all) case Lottery access(all) case Treasury } access(all) struct BonusWeightRecord { access(all) let bonusWeight: UFix64 access(all) let reason: String access(all) let addedAt: UFix64 access(all) let addedBy: Address access(contract) init(bonusWeight: UFix64, reason: String, addedBy: Address) { self.bonusWeight = bonusWeight self.reason = reason self.addedAt = getCurrentBlock().timestamp self.addedBy = addedBy } } access(all) struct EmergencyConfig { access(all) let maxEmergencyDuration: UFix64? access(all) let autoRecoveryEnabled: Bool access(all) let minYieldSourceHealth: UFix64 access(all) let maxWithdrawFailures: Int access(all) let partialModeDepositLimit: UFix64? access(all) let minBalanceThreshold: UFix64 access(all) let minRecoveryHealth: UFix64 init( maxEmergencyDuration: UFix64?, autoRecoveryEnabled: Bool, minYieldSourceHealth: UFix64, maxWithdrawFailures: Int, partialModeDepositLimit: UFix64?, minBalanceThreshold: UFix64, minRecoveryHealth: UFix64? ) { pre { minYieldSourceHealth >= 0.0 && minYieldSourceHealth <= 1.0: "Health must be between 0.0 and 1.0" maxWithdrawFailures > 0: "Must allow at least 1 withdrawal failure" minBalanceThreshold >= 0.8 && minBalanceThreshold <= 1.0: "Balance threshold must be between 0.8 and 1.0" minRecoveryHealth == nil || (minRecoveryHealth! >= 0.0 && minRecoveryHealth! <= 1.0): "minRecoveryHealth must be between 0.0 and 1.0" } self.maxEmergencyDuration = maxEmergencyDuration self.autoRecoveryEnabled = autoRecoveryEnabled self.minYieldSourceHealth = minYieldSourceHealth self.maxWithdrawFailures = maxWithdrawFailures self.partialModeDepositLimit = partialModeDepositLimit self.minBalanceThreshold = minBalanceThreshold self.minRecoveryHealth = minRecoveryHealth ?? 0.5 } } access(all) struct FundingPolicy { access(all) let maxDirectLottery: UFix64? access(all) let maxDirectTreasury: UFix64? access(all) let maxDirectSavings: UFix64? access(all) var totalDirectLottery: UFix64 access(all) var totalDirectTreasury: UFix64 access(all) var totalDirectSavings: UFix64 init(maxDirectLottery: UFix64?, maxDirectTreasury: UFix64?, maxDirectSavings: UFix64?) { self.maxDirectLottery = maxDirectLottery self.maxDirectTreasury = maxDirectTreasury self.maxDirectSavings = maxDirectSavings self.totalDirectLottery = 0.0 self.totalDirectTreasury = 0.0 self.totalDirectSavings = 0.0 } access(contract) fun recordDirectFunding(destination: PoolFundingDestination, amount: UFix64) { switch destination { case PoolFundingDestination.Lottery: self.totalDirectLottery = self.totalDirectLottery + amount if self.maxDirectLottery != nil { assert(self.totalDirectLottery <= self.maxDirectLottery!, message: "Direct lottery funding limit exceeded") } case PoolFundingDestination.Treasury: self.totalDirectTreasury = self.totalDirectTreasury + amount if self.maxDirectTreasury != nil { assert(self.totalDirectTreasury <= self.maxDirectTreasury!, message: "Direct treasury funding limit exceeded") } case PoolFundingDestination.Savings: self.totalDirectSavings = self.totalDirectSavings + amount if self.maxDirectSavings != nil { assert(self.totalDirectSavings <= self.maxDirectSavings!, message: "Direct savings funding limit exceeded") } } } } access(all) fun createDefaultEmergencyConfig(): EmergencyConfig { return EmergencyConfig( maxEmergencyDuration: 86400.0, autoRecoveryEnabled: true, minYieldSourceHealth: 0.5, maxWithdrawFailures: 3, partialModeDepositLimit: 100.0, minBalanceThreshold: 0.95, minRecoveryHealth: 0.5 ) } access(all) fun createDefaultFundingPolicy(): FundingPolicy { return FundingPolicy( maxDirectLottery: nil, maxDirectTreasury: nil, maxDirectSavings: nil ) } access(all) resource Admin { access(contract) init() {} access(CriticalOps) fun updatePoolDistributionStrategy( poolID: UInt64, newStrategy: {DistributionStrategy}, updatedBy: Address ) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let oldStrategyName = poolRef.getDistributionStrategyName() poolRef.setDistributionStrategy(strategy: newStrategy) let newStrategyName = newStrategy.getStrategyName() emit DistributionStrategyUpdated( poolID: poolID, oldStrategy: oldStrategyName, newStrategy: newStrategyName, updatedBy: updatedBy ) } access(CriticalOps) fun updatePoolWinnerSelectionStrategy( poolID: UInt64, newStrategy: {WinnerSelectionStrategy}, updatedBy: Address ) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let oldStrategyName = poolRef.getWinnerSelectionStrategyName() poolRef.setWinnerSelectionStrategy(strategy: newStrategy) let newStrategyName = newStrategy.getStrategyName() emit WinnerSelectionStrategyUpdated( poolID: poolID, oldStrategy: oldStrategyName, newStrategy: newStrategyName, updatedBy: updatedBy ) } access(ConfigOps) fun updatePoolWinnerTracker( poolID: UInt64, newTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?, updatedBy: Address ) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let hasOldTracker = poolRef.hasWinnerTracker() poolRef.setWinnerTrackerCap(cap: newTrackerCap) let hasNewTracker = newTrackerCap != nil emit WinnerTrackerUpdated( poolID: poolID, hasOldTracker: hasOldTracker, hasNewTracker: hasNewTracker, updatedBy: updatedBy ) } access(ConfigOps) fun updatePoolDrawInterval( poolID: UInt64, newInterval: UFix64, updatedBy: Address ) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let oldInterval = poolRef.getConfig().drawIntervalSeconds poolRef.setDrawIntervalSeconds(interval: newInterval) emit DrawIntervalUpdated( poolID: poolID, oldInterval: oldInterval, newInterval: newInterval, updatedBy: updatedBy ) } access(ConfigOps) fun updatePoolMinimumDeposit( poolID: UInt64, newMinimum: UFix64, updatedBy: Address ) { pre { newMinimum >= 0.0: "Minimum deposit cannot be negative" } let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let oldMinimum = poolRef.getConfig().minimumDeposit poolRef.setMinimumDeposit(minimum: newMinimum) emit MinimumDepositUpdated( poolID: poolID, oldMinimum: oldMinimum, newMinimum: newMinimum, updatedBy: updatedBy ) } access(CriticalOps) fun enableEmergencyMode(poolID: UInt64, reason: String, enabledBy: Address) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.setEmergencyMode(reason: reason) emit PoolEmergencyEnabled(poolID: poolID, reason: reason, enabledBy: enabledBy, timestamp: getCurrentBlock().timestamp) } access(CriticalOps) fun disableEmergencyMode(poolID: UInt64, disabledBy: Address) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.clearEmergencyMode() emit PoolEmergencyDisabled(poolID: poolID, disabledBy: disabledBy, timestamp: getCurrentBlock().timestamp) } access(CriticalOps) fun setEmergencyPartialMode(poolID: UInt64, reason: String, setBy: Address) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.setPartialMode(reason: reason) emit PoolPartialModeEnabled(poolID: poolID, reason: reason, setBy: setBy, timestamp: getCurrentBlock().timestamp) } access(CriticalOps) fun updateEmergencyConfig(poolID: UInt64, newConfig: EmergencyConfig, updatedBy: Address) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.setEmergencyConfig(config: newConfig) emit EmergencyConfigUpdated(poolID: poolID, updatedBy: updatedBy) } access(CriticalOps) fun fundPoolDirect( poolID: UInt64, destination: PoolFundingDestination, from: @{FungibleToken.Vault}, sponsor: Address, purpose: String, metadata: {String: String}? ) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let amount = from.balance poolRef.fundDirectInternal(destination: destination, from: <- from, sponsor: sponsor, purpose: purpose, metadata: metadata ?? {}) emit DirectFundingReceived( poolID: poolID, destination: destination.rawValue, destinationName: self.getDestinationName(destination), amount: amount, sponsor: sponsor, purpose: purpose, metadata: metadata ?? {} ) } access(self) fun getDestinationName(_ destination: PoolFundingDestination): String { switch destination { case PoolFundingDestination.Savings: return "Savings" case PoolFundingDestination.Lottery: return "Lottery" case PoolFundingDestination.Treasury: return "Treasury" default: return "Unknown" } } access(CriticalOps) fun createPool( config: PoolConfig, emergencyConfig: EmergencyConfig?, fundingPolicy: FundingPolicy?, createdBy: Address ): UInt64 { let finalEmergencyConfig = emergencyConfig ?? PrizeSavings.createDefaultEmergencyConfig() let finalFundingPolicy = fundingPolicy ?? PrizeSavings.createDefaultFundingPolicy() let poolID = PrizeSavings.createPool( config: config, emergencyConfig: finalEmergencyConfig, fundingPolicy: finalFundingPolicy ) emit PoolCreatedByAdmin( poolID: poolID, assetType: config.assetType.identifier, strategy: config.distributionStrategy.getStrategyName(), createdBy: createdBy ) return poolID } access(ConfigOps) fun processPoolRewards(poolID: UInt64) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.processRewards() } access(CriticalOps) fun setPoolState(poolID: UInt64, state: PoolEmergencyState, reason: String?, setBy: Address) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.setState(state: state, reason: reason) switch state { case PoolEmergencyState.Normal: emit PoolUnpaused(poolID: poolID, unpausedBy: setBy) case PoolEmergencyState.Paused: emit PoolPaused(poolID: poolID, pausedBy: setBy, reason: reason ?? "Manual pause") case PoolEmergencyState.EmergencyMode: emit PoolEmergencyEnabled(poolID: poolID, reason: reason ?? "Emergency", enabledBy: setBy, timestamp: getCurrentBlock().timestamp) case PoolEmergencyState.PartialMode: emit PoolPartialModeEnabled(poolID: poolID, reason: reason ?? "Partial mode", setBy: setBy, timestamp: getCurrentBlock().timestamp) } } access(CriticalOps) fun withdrawPoolTreasury( poolID: UInt64, amount: UFix64, purpose: String, withdrawnBy: Address ): @{FungibleToken.Vault} { pre { purpose.length > 0: "Purpose must be specified for treasury withdrawal" } let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let treasuryVault <- poolRef.withdrawTreasury( amount: amount, withdrawnBy: withdrawnBy, purpose: purpose ) emit TreasuryWithdrawn( poolID: poolID, withdrawnBy: withdrawnBy, amount: amount, purpose: purpose, remainingBalance: poolRef.getTreasuryBalance() ) return <- treasuryVault } access(ConfigOps) fun setBonusLotteryWeight( poolID: UInt64, receiverID: UInt64, bonusWeight: UFix64, reason: String, setBy: Address ) { pre { bonusWeight >= 0.0: "Bonus weight cannot be negative" } let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.setBonusWeight(receiverID: receiverID, bonusWeight: bonusWeight, reason: reason, setBy: setBy) } access(ConfigOps) fun addBonusLotteryWeight( poolID: UInt64, receiverID: UInt64, additionalWeight: UFix64, reason: String, addedBy: Address ) { pre { additionalWeight > 0.0: "Additional weight must be positive" } let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.addBonusWeight(receiverID: receiverID, additionalWeight: additionalWeight, reason: reason, addedBy: addedBy) } access(ConfigOps) fun removeBonusLotteryWeight( poolID: UInt64, receiverID: UInt64, removedBy: Address ) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.removeBonusWeight(receiverID: receiverID, removedBy: removedBy) } access(ConfigOps) fun depositNFTPrize( poolID: UInt64, nft: @{NonFungibleToken.NFT}, depositedBy: Address ) { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let nftID = nft.uuid let nftType = nft.getType().identifier poolRef.depositNFTPrize(nft: <- nft) emit NFTPrizeDeposited( poolID: poolID, nftID: nftID, nftType: nftType, depositedBy: depositedBy ) } access(ConfigOps) fun withdrawNFTPrize( poolID: UInt64, nftID: UInt64, withdrawnBy: Address ): @{NonFungibleToken.NFT} { let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") let nft <- poolRef.withdrawNFTPrize(nftID: nftID) let nftType = nft.getType().identifier emit NFTPrizeWithdrawn( poolID: poolID, nftID: nftID, nftType: nftType, withdrawnBy: withdrawnBy ) return <- nft } } access(all) let AdminStoragePath: StoragePath access(all) let AdminPublicPath: PublicPath access(all) struct DistributionPlan { access(all) let savingsAmount: UFix64 access(all) let lotteryAmount: UFix64 access(all) let treasuryAmount: UFix64 init(savings: UFix64, lottery: UFix64, treasury: UFix64) { self.savingsAmount = savings self.lotteryAmount = lottery self.treasuryAmount = treasury } } access(all) struct interface DistributionStrategy { access(all) fun calculateDistribution(totalAmount: UFix64): DistributionPlan access(all) fun getStrategyName(): String } access(all) struct FixedPercentageStrategy: DistributionStrategy { access(all) let savingsPercent: UFix64 access(all) let lotteryPercent: UFix64 access(all) let treasuryPercent: UFix64 init(savings: UFix64, lottery: UFix64, treasury: UFix64) { pre { savings + lottery + treasury == 1.0: "Percentages must sum to 1.0" savings >= 0.0 && lottery >= 0.0 && treasury >= 0.0: "Must be non-negative" } self.savingsPercent = savings self.lotteryPercent = lottery self.treasuryPercent = treasury } access(all) fun calculateDistribution(totalAmount: UFix64): DistributionPlan { return DistributionPlan( savings: totalAmount * self.savingsPercent, lottery: totalAmount * self.lotteryPercent, treasury: totalAmount * self.treasuryPercent ) } access(all) fun getStrategyName(): String { return "Fixed: " .concat(self.savingsPercent.toString()) .concat(" savings, ") .concat(self.lotteryPercent.toString()) .concat(" lottery") } } /// ERC4626-style shares distributor for O(1) interest distribution /// /// Key relationship: totalAssets = what users collectively own (principal + accrued savings yield) /// This should equal Pool.totalStaked for the savings portion (funds stay in yield source) access(all) resource SavingsDistributor { /// Total shares minted across all users access(self) var totalShares: UFix64 /// Total assets owned by all shareholders (principal + accrued yield) /// Updated on: deposit (+), accrueYield (+), withdraw (-) access(self) var totalAssets: UFix64 /// Per-user share balances access(self) let userShares: {UInt64: UFix64} /// Cumulative yield distributed (for analytics) access(all) var totalDistributed: UFix64 access(self) let vaultType: Type /// Time-weighted stake tracking access(self) let userCumulativeShareSeconds: {UInt64: UFix64} access(self) let userLastUpdateTime: {UInt64: UFix64} access(self) let userEpochID: {UInt64: UInt64} access(self) var currentEpochID: UInt64 access(self) var epochStartTime: UFix64 init(vaultType: Type) { self.totalShares = 0.0 self.totalAssets = 0.0 self.userShares = {} self.totalDistributed = 0.0 self.vaultType = vaultType self.userCumulativeShareSeconds = {} self.userLastUpdateTime = {} self.userEpochID = {} self.currentEpochID = 1 self.epochStartTime = getCurrentBlock().timestamp } access(contract) fun accrueYield(amount: UFix64) { if amount == 0.0 || self.totalShares == 0.0 { return } self.totalAssets = self.totalAssets + amount self.totalDistributed = self.totalDistributed + amount } access(contract) fun accumulateTime(receiverID: UInt64) { let now = getCurrentBlock().timestamp let userEpoch = self.userEpochID[receiverID] ?? 0 // Lazy reset for stale epoch if userEpoch < self.currentEpochID { self.userCumulativeShareSeconds[receiverID] = 0.0 self.userLastUpdateTime[receiverID] = self.epochStartTime self.userEpochID[receiverID] = self.currentEpochID } let lastUpdate = self.userLastUpdateTime[receiverID] ?? self.epochStartTime let elapsed = now - lastUpdate if elapsed > 0.0 { let currentShares = self.userShares[receiverID] ?? 0.0 let currentAccum = self.userCumulativeShareSeconds[receiverID] ?? 0.0 self.userCumulativeShareSeconds[receiverID] = currentAccum + (currentShares * elapsed) self.userLastUpdateTime[receiverID] = now } } access(contract) fun getTimeWeightedStake(receiverID: UInt64): UFix64 { self.accumulateTime(receiverID: receiverID) return self.userCumulativeShareSeconds[receiverID] ?? 0.0 } access(contract) fun startNewPeriod() { self.currentEpochID = self.currentEpochID + 1 self.epochStartTime = getCurrentBlock().timestamp } access(all) view fun getCurrentEpochID(): UInt64 { return self.currentEpochID } access(all) view fun getEpochStartTime(): UFix64 { return self.epochStartTime } access(contract) fun deposit(receiverID: UInt64, amount: UFix64) { if amount == 0.0 { return } self.accumulateTime(receiverID: receiverID) let sharesToMint = self.convertToShares(amount) let currentShares = self.userShares[receiverID] ?? 0.0 self.userShares[receiverID] = currentShares + sharesToMint self.totalShares = self.totalShares + sharesToMint self.totalAssets = self.totalAssets + amount } access(contract) fun withdraw(receiverID: UInt64, amount: UFix64): UFix64 { if amount == 0.0 { return 0.0 } self.accumulateTime(receiverID: receiverID) let userShareBalance = self.userShares[receiverID] ?? 0.0 assert(userShareBalance > 0.0, message: "No shares to withdraw") assert(self.totalShares > 0.0 && self.totalAssets > 0.0, message: "Invalid distributor state") let currentAssetValue = self.convertToAssets(userShareBalance) assert(amount <= currentAssetValue, message: "Insufficient balance") let sharesToBurn = (amount * self.totalShares) / self.totalAssets self.userShares[receiverID] = userShareBalance - sharesToBurn self.totalShares = self.totalShares - sharesToBurn self.totalAssets = self.totalAssets - amount return amount } access(all) view fun convertToShares(_ assets: UFix64): UFix64 { if self.totalShares == 0.0 || self.totalAssets == 0.0 { return assets } if assets > 0.0 && self.totalShares > 0.0 { let maxSafeAssets = UFix64.max / self.totalShares assert(assets <= maxSafeAssets, message: "Deposit amount too large - would cause overflow") } return (assets * self.totalShares) / self.totalAssets } access(all) view fun convertToAssets(_ shares: UFix64): UFix64 { if self.totalShares == 0.0 { return 0.0 } if shares > 0.0 && self.totalAssets > 0.0 { let maxSafeShares = UFix64.max / self.totalAssets assert(shares <= maxSafeShares, message: "Share amount too large - would cause overflow") } return (shares * self.totalAssets) / self.totalShares } access(all) fun getUserAssetValue(receiverID: UInt64): UFix64 { let userShareBalance = self.userShares[receiverID] ?? 0.0 return self.convertToAssets(userShareBalance) } access(all) fun getTotalDistributed(): UFix64 { return self.totalDistributed } access(all) fun getTotalShares(): UFix64 { return self.totalShares } access(all) fun getTotalAssets(): UFix64 { return self.totalAssets } access(all) fun getUserShares(receiverID: UInt64): UFix64 { return self.userShares[receiverID] ?? 0.0 } access(all) view fun getUserAccumulatedRaw(receiverID: UInt64): UFix64 { return self.userCumulativeShareSeconds[receiverID] ?? 0.0 } access(all) view fun getUserLastUpdateTime(receiverID: UInt64): UFix64 { return self.userLastUpdateTime[receiverID] ?? self.epochStartTime } access(all) view fun getUserEpochID(receiverID: UInt64): UInt64 { return self.userEpochID[receiverID] ?? 0 } /// Calculate projected stake at a specific time (no state change) access(all) view fun calculateStakeAtTime(receiverID: UInt64, targetTime: UFix64): UFix64 { let userEpoch = self.userEpochID[receiverID] ?? 0 let shares = self.userShares[receiverID] ?? 0.0 if userEpoch < self.currentEpochID { if targetTime <= self.epochStartTime { return 0.0 } return shares * (targetTime - self.epochStartTime) } let lastUpdate = self.userLastUpdateTime[receiverID] ?? self.epochStartTime let accumulated = self.userCumulativeShareSeconds[receiverID] ?? 0.0 if targetTime <= lastUpdate { let overdraft = lastUpdate - targetTime let overdraftAmount = shares * overdraft return accumulated >= overdraftAmount ? accumulated - overdraftAmount : 0.0 } return accumulated + (shares * (targetTime - lastUpdate)) } } access(all) resource LotteryDistributor { access(self) var prizeVault: @{FungibleToken.Vault} access(self) var nftPrizeSavings: @{UInt64: {NonFungibleToken.NFT}} access(self) var pendingNFTClaims: @{UInt64: [{NonFungibleToken.NFT}]} access(self) var _prizeRound: UInt64 access(all) var totalPrizesDistributed: UFix64 access(all) fun getPrizeRound(): UInt64 { return self._prizeRound } access(contract) fun setPrizeRound(round: UInt64) { self._prizeRound = round } init(vaultType: Type) { self.prizeVault <- DeFiActionsUtils.getEmptyVault(vaultType) self.nftPrizeSavings <- {} self.pendingNFTClaims <- {} self._prizeRound = 0 self.totalPrizesDistributed = 0.0 } access(contract) fun fundPrizePool(vault: @{FungibleToken.Vault}) { self.prizeVault.deposit(from: <- vault) } access(all) fun getPrizePoolBalance(): UFix64 { return self.prizeVault.balance } access(contract) fun awardPrize(receiverID: UInt64, amount: UFix64, yieldSource: auth(FungibleToken.Withdraw) &{DeFiActions.Source}?): @{FungibleToken.Vault} { self.totalPrizesDistributed = self.totalPrizesDistributed + amount var result <- DeFiActionsUtils.getEmptyVault(self.prizeVault.getType()) if yieldSource != nil { let available = yieldSource!.minimumAvailable() if available >= amount { result.deposit(from: <- yieldSource!.withdrawAvailable(maxAmount: amount)) return <- result } else if available > 0.0 { result.deposit(from: <- yieldSource!.withdrawAvailable(maxAmount: available)) } } if result.balance < amount { let remaining = amount - result.balance assert(self.prizeVault.balance >= remaining, message: "Insufficient prize pool") result.deposit(from: <- self.prizeVault.withdraw(amount: remaining)) } return <- result } access(contract) fun depositNFTPrize(nft: @{NonFungibleToken.NFT}) { let nftID = nft.uuid self.nftPrizeSavings[nftID] <-! nft } access(contract) fun withdrawNFTPrize(nftID: UInt64): @{NonFungibleToken.NFT} { let nft <- self.nftPrizeSavings.remove(key: nftID) if nft == nil { panic("NFT not found in prize vault") } return <- nft! } access(contract) fun storePendingNFT(receiverID: UInt64, nft: @{NonFungibleToken.NFT}) { if self.pendingNFTClaims[receiverID] == nil { self.pendingNFTClaims[receiverID] <-! [] } let arrayRef = &self.pendingNFTClaims[receiverID] as auth(Mutate) &[{NonFungibleToken.NFT}]? if arrayRef != nil { arrayRef!.append(<- nft) } else { destroy nft panic("Failed to store NFT in pending claims") } } access(all) fun getPendingNFTCount(receiverID: UInt64): Int { return self.pendingNFTClaims[receiverID]?.length ?? 0 } access(all) fun getPendingNFTIDs(receiverID: UInt64): [UInt64] { let nfts = &self.pendingNFTClaims[receiverID] as &[{NonFungibleToken.NFT}]? if nfts == nil { return [] } let ids: [UInt64] = [] for nft in nfts! { ids.append(nft.uuid) } return ids } access(all) fun getAvailableNFTPrizeIDs(): [UInt64] { return self.nftPrizeSavings.keys } access(all) fun borrowNFTPrize(nftID: UInt64): &{NonFungibleToken.NFT}? { return &self.nftPrizeSavings[nftID] } access(contract) fun claimPendingNFT(receiverID: UInt64, nftIndex: Int): @{NonFungibleToken.NFT} { pre { self.pendingNFTClaims[receiverID] != nil: "No pending NFTs for this receiver" nftIndex < self.pendingNFTClaims[receiverID]?.length!: "Invalid NFT index" } return <- self.pendingNFTClaims[receiverID]?.remove(at: nftIndex)! } } access(all) resource TreasuryDistributor { access(self) var treasuryVault: @{FungibleToken.Vault} access(all) var totalCollected: UFix64 access(all) var totalWithdrawn: UFix64 access(self) var withdrawalHistory: [{String: AnyStruct}] init(vaultType: Type) { self.treasuryVault <- DeFiActionsUtils.getEmptyVault(vaultType) self.totalCollected = 0.0 self.totalWithdrawn = 0.0 self.withdrawalHistory = [] } access(contract) fun deposit(vault: @{FungibleToken.Vault}) { let amount = vault.balance self.totalCollected = self.totalCollected + amount self.treasuryVault.deposit(from: <- vault) } access(all) fun getBalance(): UFix64 { return self.treasuryVault.balance } access(all) fun getTotalCollected(): UFix64 { return self.totalCollected } access(all) fun getTotalWithdrawn(): UFix64 { return self.totalWithdrawn } access(all) fun getWithdrawalHistory(): [{String: AnyStruct}] { return self.withdrawalHistory } access(contract) fun withdraw(amount: UFix64, withdrawnBy: Address, purpose: String): @{FungibleToken.Vault} { pre { self.treasuryVault.balance >= amount: "Insufficient treasury balance" amount > 0.0: "Withdrawal amount must be positive" purpose.length > 0: "Purpose must be specified" } self.totalWithdrawn = self.totalWithdrawn + amount self.withdrawalHistory.append({ "address": withdrawnBy, "amount": amount, "timestamp": getCurrentBlock().timestamp, "purpose": purpose }) return <- self.treasuryVault.withdraw(amount: amount) } } access(all) resource PrizeDrawReceipt { access(all) let prizeAmount: UFix64 access(self) var request: @RandomConsumer.Request? access(all) let timeWeightedStakes: {UInt64: UFix64} init(prizeAmount: UFix64, request: @RandomConsumer.Request, timeWeightedStakes: {UInt64: UFix64}) { self.prizeAmount = prizeAmount self.request <- request self.timeWeightedStakes = timeWeightedStakes } access(all) view fun getRequestBlock(): UInt64? { return self.request?.block } access(contract) fun popRequest(): @RandomConsumer.Request { let request <- self.request <- nil return <- request! } access(all) fun getTimeWeightedStakes(): {UInt64: UFix64} { return self.timeWeightedStakes } } access(all) struct WinnerSelectionResult { access(all) let winners: [UInt64] access(all) let amounts: [UFix64] access(all) let nftIDs: [[UInt64]] init(winners: [UInt64], amounts: [UFix64], nftIDs: [[UInt64]]) { pre { winners.length == amounts.length: "Winners and amounts must have same length" winners.length == nftIDs.length: "Winners and nftIDs must have same length" } self.winners = winners self.amounts = amounts self.nftIDs = nftIDs } } access(all) struct interface WinnerSelectionStrategy { access(all) fun selectWinners( randomNumber: UInt64, receiverDeposits: {UInt64: UFix64}, totalPrizeAmount: UFix64 ): WinnerSelectionResult access(all) fun getStrategyName(): String } access(all) struct WeightedSingleWinner: WinnerSelectionStrategy { access(all) let nftIDs: [UInt64] init(nftIDs: [UInt64]) { self.nftIDs = nftIDs } access(all) fun selectWinners( randomNumber: UInt64, receiverDeposits: {UInt64: UFix64}, totalPrizeAmount: UFix64 ): WinnerSelectionResult { let receiverIDs = receiverDeposits.keys if receiverIDs.length == 0 { return WinnerSelectionResult(winners: [], amounts: [], nftIDs: []) } if receiverIDs.length == 1 { return WinnerSelectionResult( winners: [receiverIDs[0]], amounts: [totalPrizeAmount], nftIDs: [self.nftIDs] ) } var cumulativeSum: [UFix64] = [] var runningTotal: UFix64 = 0.0 for receiverID in receiverIDs { runningTotal = runningTotal + receiverDeposits[receiverID]! cumulativeSum.append(runningTotal) } if runningTotal == 0.0 { return WinnerSelectionResult( winners: [receiverIDs[0]], amounts: [totalPrizeAmount], nftIDs: [self.nftIDs] ) } let scaledRandom = UFix64(randomNumber % 1_000_000_000) / 1_000_000_000.0 let randomValue = scaledRandom * runningTotal var winnerIndex = 0 for i, cumSum in cumulativeSum { if randomValue < cumSum { winnerIndex = i break } } return WinnerSelectionResult( winners: [receiverIDs[winnerIndex]], amounts: [totalPrizeAmount], nftIDs: [self.nftIDs] ) } access(all) fun getStrategyName(): String { return "Weighted Single Winner" } } access(all) struct MultiWinnerSplit: WinnerSelectionStrategy { access(all) let winnerCount: Int access(all) let prizeSplits: [UFix64] access(all) let nftIDsPerWinner: [[UInt64]] /// nftIDs: flat array of NFT IDs to distribute (one per winner, in order) init(winnerCount: Int, prizeSplits: [UFix64], nftIDs: [UInt64]) { pre { winnerCount > 0: "Must have at least one winner" prizeSplits.length == winnerCount: "Prize splits must match winner count" } var total: UFix64 = 0.0 for split in prizeSplits { assert(split >= 0.0 && split <= 1.0, message: "Each split must be between 0 and 1") total = total + split } assert(total == 1.0, message: "Prize splits must sum to 1.0") self.winnerCount = winnerCount self.prizeSplits = prizeSplits var nftArray: [[UInt64]] = [] var nftIndex = 0 var winnerIdx = 0 while winnerIdx < winnerCount { if nftIndex < nftIDs.length { nftArray.append([nftIDs[nftIndex]]) nftIndex = nftIndex + 1 } else { nftArray.append([]) } winnerIdx = winnerIdx + 1 } self.nftIDsPerWinner = nftArray } access(all) fun selectWinners( randomNumber: UInt64, receiverDeposits: {UInt64: UFix64}, totalPrizeAmount: UFix64 ): WinnerSelectionResult { let receiverIDs = receiverDeposits.keys let depositorCount = receiverIDs.length if depositorCount == 0 { return WinnerSelectionResult(winners: [], amounts: [], nftIDs: []) } // Compute actual winner count - if fewer depositors than configured winners, // award prizes to all available depositors instead of panicking let actualWinnerCount = self.winnerCount < depositorCount ? self.winnerCount : depositorCount if depositorCount == 1 { let nftIDsForFirst: [UInt64] = self.nftIDsPerWinner.length > 0 ? self.nftIDsPerWinner[0] : [] return WinnerSelectionResult( winners: [receiverIDs[0]], amounts: [totalPrizeAmount], nftIDs: [nftIDsForFirst] ) } var cumulativeSum: [UFix64] = [] var runningTotal: UFix64 = 0.0 var depositsList: [UFix64] = [] for receiverID in receiverIDs { let deposit = receiverDeposits[receiverID]! depositsList.append(deposit) runningTotal = runningTotal + deposit cumulativeSum.append(runningTotal) } var selectedWinners: [UInt64] = [] var selectedIndices: {Int: Bool} = {} var remainingDeposits = depositsList var remainingIDs = receiverIDs var remainingCumSum = cumulativeSum var remainingTotal = runningTotal var randomBytes = randomNumber.toBigEndianBytes() while randomBytes.length < 16 { randomBytes.appendAll(randomNumber.toBigEndianBytes()) } var paddedBytes: [UInt8] = [] var padIdx = 0 while padIdx < 16 { paddedBytes.append(randomBytes[padIdx % randomBytes.length]) padIdx = padIdx + 1 } let prg = Xorshift128plus.PRG( sourceOfRandomness: paddedBytes, salt: [] ) var winnerIndex = 0 while winnerIndex < actualWinnerCount && remainingIDs.length > 0 && remainingTotal > 0.0 { let rng = prg.nextUInt64() let scaledRandom = UFix64(rng % 1_000_000_000) / 1_000_000_000.0 let randomValue = scaledRandom * remainingTotal var selectedIdx = 0 for i, cumSum in remainingCumSum { if randomValue < cumSum { selectedIdx = i break } } selectedWinners.append(remainingIDs[selectedIdx]) selectedIndices[selectedIdx] = true var newRemainingIDs: [UInt64] = [] var newRemainingDeposits: [UFix64] = [] var newCumSum: [UFix64] = [] var newRunningTotal: UFix64 = 0.0 var idx = 0 while idx < remainingIDs.length { if idx != selectedIdx { newRemainingIDs.append(remainingIDs[idx]) newRemainingDeposits.append(remainingDeposits[idx]) newRunningTotal = newRunningTotal + remainingDeposits[idx] newCumSum.append(newRunningTotal) } idx = idx + 1 } remainingIDs = newRemainingIDs remainingDeposits = newRemainingDeposits remainingCumSum = newCumSum remainingTotal = newRunningTotal winnerIndex = winnerIndex + 1 } var prizeAmounts: [UFix64] = [] var calculatedSum: UFix64 = 0.0 var idx = 0 while idx < selectedWinners.length - 1 { let split = self.prizeSplits[idx] let amount = totalPrizeAmount * split prizeAmounts.append(amount) calculatedSum = calculatedSum + amount idx = idx + 1 } let lastPrize = totalPrizeAmount - calculatedSum prizeAmounts.append(lastPrize) // Only validate deviation when we have the expected number of winners // When fewer depositors exist, the last winner gets the remainder which is expected if selectedWinners.length == self.winnerCount { let expectedLast = totalPrizeAmount * self.prizeSplits[selectedWinners.length - 1] let deviation = lastPrize > expectedLast ? lastPrize - expectedLast : expectedLast - lastPrize let maxDeviation = totalPrizeAmount * 0.01 assert(deviation <= maxDeviation, message: "Last prize deviation too large - check splits") } var nftIDsArray: [[UInt64]] = [] var idx2 = 0 while idx2 < selectedWinners.length { if idx2 < self.nftIDsPerWinner.length { nftIDsArray.append(self.nftIDsPerWinner[idx2]) } else { nftIDsArray.append([]) } idx2 = idx2 + 1 } return WinnerSelectionResult( winners: selectedWinners, amounts: prizeAmounts, nftIDs: nftIDsArray ) } access(all) fun getStrategyName(): String { var name = "Multi-Winner (".concat(self.winnerCount.toString()).concat(" winners): ") var idx = 0 while idx < self.prizeSplits.length { if idx > 0 { name = name.concat(", ") } name = name.concat((self.prizeSplits[idx] * 100.0).toString()).concat("%") idx = idx + 1 } return name } } access(all) struct PrizeTier { access(all) let prizeAmount: UFix64 access(all) let winnerCount: Int access(all) let name: String access(all) let nftIDs: [UInt64] init(amount: UFix64, count: Int, name: String, nftIDs: [UInt64]) { pre { amount > 0.0: "Prize amount must be positive" count > 0: "Winner count must be positive" nftIDs.length <= count: "Cannot have more NFTs than winners in tier" } self.prizeAmount = amount self.winnerCount = count self.name = name self.nftIDs = nftIDs } } access(all) struct FixedPrizeTiers: WinnerSelectionStrategy { access(all) let tiers: [PrizeTier] init(tiers: [PrizeTier]) { pre { tiers.length > 0: "Must have at least one prize tier" } self.tiers = tiers } access(all) fun selectWinners( randomNumber: UInt64, receiverDeposits: {UInt64: UFix64}, totalPrizeAmount: UFix64 ): WinnerSelectionResult { let receiverIDs = receiverDeposits.keys let depositorCount = receiverIDs.length if depositorCount == 0 { return WinnerSelectionResult(winners: [], amounts: [], nftIDs: []) } var totalNeeded: UFix64 = 0.0 var totalWinnersNeeded = 0 for tier in self.tiers { totalNeeded = totalNeeded + (tier.prizeAmount * UFix64(tier.winnerCount)) totalWinnersNeeded = totalWinnersNeeded + tier.winnerCount } if totalPrizeAmount < totalNeeded { return WinnerSelectionResult(winners: [], amounts: [], nftIDs: []) } if totalWinnersNeeded > depositorCount { return WinnerSelectionResult(winners: [], amounts: [], nftIDs: []) } var cumulativeSum: [UFix64] = [] var runningTotal: UFix64 = 0.0 for receiverID in receiverIDs { let deposit = receiverDeposits[receiverID]! runningTotal = runningTotal + deposit cumulativeSum.append(runningTotal) } var randomBytes = randomNumber.toBigEndianBytes() while randomBytes.length < 16 { randomBytes.appendAll(randomNumber.toBigEndianBytes()) } var paddedBytes: [UInt8] = [] var padIdx2 = 0 while padIdx2 < 16 { paddedBytes.append(randomBytes[padIdx2 % randomBytes.length]) padIdx2 = padIdx2 + 1 } let prg = Xorshift128plus.PRG( sourceOfRandomness: paddedBytes, salt: [] ) var allWinners: [UInt64] = [] var allPrizes: [UFix64] = [] var allNFTIDs: [[UInt64]] = [] var usedIndices: {Int: Bool} = {} var remainingIDs = receiverIDs var remainingCumSum = cumulativeSum var remainingTotal = runningTotal for tier in self.tiers { var tierWinnerCount = 0 while tierWinnerCount < tier.winnerCount && remainingIDs.length > 0 && remainingTotal > 0.0 { let rng = prg.nextUInt64() let scaledRandom = UFix64(rng % 1_000_000_000) / 1_000_000_000.0 let randomValue = scaledRandom * remainingTotal var selectedIdx = 0 for i, cumSum in remainingCumSum { if randomValue < cumSum { selectedIdx = i break } } let winnerID = remainingIDs[selectedIdx] allWinners.append(winnerID) allPrizes.append(tier.prizeAmount) if tierWinnerCount < tier.nftIDs.length { allNFTIDs.append([tier.nftIDs[tierWinnerCount]]) } else { allNFTIDs.append([]) } var newRemainingIDs: [UInt64] = [] var newRemainingCumSum: [UFix64] = [] var newRunningTotal: UFix64 = 0.0 var oldIdx = 0 while oldIdx < remainingIDs.length { if oldIdx != selectedIdx { newRemainingIDs.append(remainingIDs[oldIdx]) let deposit = receiverDeposits[remainingIDs[oldIdx]]! newRunningTotal = newRunningTotal + deposit newRemainingCumSum.append(newRunningTotal) } oldIdx = oldIdx + 1 } remainingIDs = newRemainingIDs remainingCumSum = newRemainingCumSum remainingTotal = newRunningTotal tierWinnerCount = tierWinnerCount + 1 } } return WinnerSelectionResult( winners: allWinners, amounts: allPrizes, nftIDs: allNFTIDs ) } access(all) fun getStrategyName(): String { var name = "Fixed Prizes (" var idx = 0 while idx < self.tiers.length { if idx > 0 { name = name.concat(", ") } let tier = self.tiers[idx] name = name.concat(tier.winnerCount.toString()) .concat("x ") .concat(tier.prizeAmount.toString()) idx = idx + 1 } return name.concat(")") } } access(all) struct PoolConfig { access(all) let assetType: Type access(all) let yieldConnector: {DeFiActions.Sink, DeFiActions.Source} access(all) var minimumDeposit: UFix64 access(all) var drawIntervalSeconds: UFix64 access(all) var distributionStrategy: {DistributionStrategy} access(all) var winnerSelectionStrategy: {WinnerSelectionStrategy} access(all) var winnerTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>? init( assetType: Type, yieldConnector: {DeFiActions.Sink, DeFiActions.Source}, minimumDeposit: UFix64, drawIntervalSeconds: UFix64, distributionStrategy: {DistributionStrategy}, winnerSelectionStrategy: {WinnerSelectionStrategy}, winnerTrackerCap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>? ) { self.assetType = assetType self.yieldConnector = yieldConnector self.minimumDeposit = minimumDeposit self.drawIntervalSeconds = drawIntervalSeconds self.distributionStrategy = distributionStrategy self.winnerSelectionStrategy = winnerSelectionStrategy self.winnerTrackerCap = winnerTrackerCap } access(contract) fun setDistributionStrategy(strategy: {DistributionStrategy}) { self.distributionStrategy = strategy } access(contract) fun setWinnerSelectionStrategy(strategy: {WinnerSelectionStrategy}) { self.winnerSelectionStrategy = strategy } access(contract) fun setWinnerTrackerCap(cap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?) { self.winnerTrackerCap = cap } access(contract) fun setDrawIntervalSeconds(interval: UFix64) { pre { interval >= 1.0: "Draw interval must be at least 1 seconds" } self.drawIntervalSeconds = interval } access(contract) fun setMinimumDeposit(minimum: UFix64) { pre { minimum >= 0.0: "Minimum deposit cannot be negative" } self.minimumDeposit = minimum } } /// Batch draw state for breaking startDraw() into multiple transactions. /// Flow: startDrawSnapshot() → captureStakesBatch() (repeat) → finalizeDrawStart() access(all) struct BatchDrawState { access(all) let drawCutoffTime: UFix64 access(all) var capturedWeights: {UInt64: UFix64} access(all) var totalWeight: UFix64 access(all) var processedCount: Int access(all) let snapshotEpochID: UInt64 init(cutoffTime: UFix64, epochID: UInt64) { self.drawCutoffTime = cutoffTime self.capturedWeights = {} self.totalWeight = 0.0 self.processedCount = 0 self.snapshotEpochID = epochID } } access(all) resource Pool { access(self) var config: PoolConfig access(self) var poolID: UInt64 access(self) var emergencyState: PoolEmergencyState access(self) var emergencyReason: String? access(self) var emergencyActivatedAt: UFix64? access(self) var emergencyConfig: EmergencyConfig access(self) var consecutiveWithdrawFailures: Int access(self) var fundingPolicy: FundingPolicy access(contract) fun setPoolID(id: UInt64) { self.poolID = id } access(self) let receiverDeposits: {UInt64: UFix64} access(self) let receiverTotalEarnedPrizes: {UInt64: UFix64} access(self) let registeredReceivers: {UInt64: Bool} access(self) let receiverBonusWeights: {UInt64: BonusWeightRecord} /// ACCOUNTING VARIABLES - Key relationships: /// /// totalDeposited: Sum of user principal deposits only (excludes earned interest) /// Updated on: deposit (+), withdraw principal (-) /// /// totalStaked: Amount tracked as being in the yield source /// Updated on: deposit (+), savings yield accrual (+), withdraw from yield source (-) /// Should equal: yieldSourceBalance (approximately) /// /// Invariant: totalStaked >= totalDeposited (difference is reinvested savings yield) /// /// Note: SavingsDistributor.totalAssets tracks what users own and should equal totalStaked access(all) var totalDeposited: UFix64 access(all) var totalStaked: UFix64 access(all) var lastDrawTimestamp: UFix64 access(all) var pendingLotteryYield: UFix64 // Lottery funds still earning in yield source access(self) let savingsDistributor: @SavingsDistributor access(self) let lotteryDistributor: @LotteryDistributor access(self) let treasuryDistributor: @TreasuryDistributor access(self) var pendingDrawReceipt: @PrizeDrawReceipt? access(self) let randomConsumer: @RandomConsumer.Consumer /// Batch draw state. When set, deposits/withdrawals are locked. access(self) var batchDrawState: BatchDrawState? init( config: PoolConfig, emergencyConfig: EmergencyConfig?, fundingPolicy: FundingPolicy? ) { self.config = config self.poolID = 0 self.emergencyState = PoolEmergencyState.Normal self.emergencyReason = nil self.emergencyActivatedAt = nil self.emergencyConfig = emergencyConfig ?? PrizeSavings.createDefaultEmergencyConfig() self.consecutiveWithdrawFailures = 0 self.fundingPolicy = fundingPolicy ?? PrizeSavings.createDefaultFundingPolicy() self.receiverDeposits = {} self.receiverTotalEarnedPrizes = {} self.registeredReceivers = {} self.receiverBonusWeights = {} self.totalDeposited = 0.0 self.totalStaked = 0.0 self.lastDrawTimestamp = 0.0 self.pendingLotteryYield = 0.0 self.savingsDistributor <- create SavingsDistributor(vaultType: config.assetType) self.lotteryDistributor <- create LotteryDistributor(vaultType: config.assetType) self.treasuryDistributor <- create TreasuryDistributor(vaultType: config.assetType) self.pendingDrawReceipt <- nil self.randomConsumer <- RandomConsumer.createConsumer() self.batchDrawState = nil } access(all) fun registerReceiver(receiverID: UInt64) { pre { self.registeredReceivers[receiverID] == nil: "Receiver already registered" } self.registeredReceivers[receiverID] = true } access(all) view fun getEmergencyState(): PoolEmergencyState { return self.emergencyState } access(all) view fun getEmergencyConfig(): EmergencyConfig { return self.emergencyConfig } access(contract) fun setState(state: PoolEmergencyState, reason: String?) { self.emergencyState = state if state != PoolEmergencyState.Normal { self.emergencyReason = reason self.emergencyActivatedAt = getCurrentBlock().timestamp } else { self.emergencyReason = nil self.emergencyActivatedAt = nil self.consecutiveWithdrawFailures = 0 } } access(contract) fun setEmergencyMode(reason: String) { self.emergencyState = PoolEmergencyState.EmergencyMode self.emergencyReason = reason self.emergencyActivatedAt = getCurrentBlock().timestamp } access(contract) fun setPartialMode(reason: String) { self.emergencyState = PoolEmergencyState.PartialMode self.emergencyReason = reason self.emergencyActivatedAt = getCurrentBlock().timestamp } access(contract) fun clearEmergencyMode() { self.emergencyState = PoolEmergencyState.Normal self.emergencyReason = nil self.emergencyActivatedAt = nil self.consecutiveWithdrawFailures = 0 } access(contract) fun setEmergencyConfig(config: EmergencyConfig) { self.emergencyConfig = config } access(all) view fun isEmergencyMode(): Bool { return self.emergencyState == PoolEmergencyState.EmergencyMode } access(all) view fun isPartialMode(): Bool { return self.emergencyState == PoolEmergencyState.PartialMode } access(all) fun getEmergencyInfo(): {String: AnyStruct}? { if self.emergencyState != PoolEmergencyState.Normal { let duration = getCurrentBlock().timestamp - (self.emergencyActivatedAt ?? 0.0) let health = self.checkYieldSourceHealth() return { "state": self.emergencyState.rawValue, "reason": self.emergencyReason ?? "Unknown", "activatedAt": self.emergencyActivatedAt ?? 0.0, "durationSeconds": duration, "yieldSourceHealth": health, "canAutoRecover": self.emergencyConfig.autoRecoveryEnabled, "maxDuration": self.emergencyConfig.maxEmergencyDuration } } return nil } access(contract) fun checkYieldSourceHealth(): UFix64 { let yieldSource = &self.config.yieldConnector as &{DeFiActions.Source} let balance = yieldSource.minimumAvailable() let threshold = self.getEmergencyConfig().minBalanceThreshold let balanceHealthy = balance >= self.totalStaked * threshold let withdrawSuccessRate = self.consecutiveWithdrawFailures == 0 ? 1.0 : (1.0 / UFix64(self.consecutiveWithdrawFailures + 1)) var health: UFix64 = 0.0 if balanceHealthy { health = health + 0.5 } health = health + (withdrawSuccessRate * 0.5) return health } access(contract) fun checkAndAutoTriggerEmergency(): Bool { if self.emergencyState != PoolEmergencyState.Normal { return false } let health = self.checkYieldSourceHealth() if health < self.emergencyConfig.minYieldSourceHealth { self.setEmergencyMode(reason: "Auto-triggered: Yield source health below threshold (".concat(health.toString()).concat(")")) emit EmergencyModeAutoTriggered(poolID: self.poolID, reason: "Low yield source health", healthScore: health, timestamp: getCurrentBlock().timestamp) return true } if self.consecutiveWithdrawFailures >= self.emergencyConfig.maxWithdrawFailures { self.setEmergencyMode(reason: "Auto-triggered: Multiple consecutive withdrawal failures") emit EmergencyModeAutoTriggered(poolID: self.poolID, reason: "Withdrawal failures", healthScore: health, timestamp: getCurrentBlock().timestamp) return true } return false } access(contract) fun checkAndAutoRecover(): Bool { if self.emergencyState != PoolEmergencyState.EmergencyMode { return false } if !self.emergencyConfig.autoRecoveryEnabled { return false } let health = self.checkYieldSourceHealth() // Health-based recovery: yield source is healthy if health >= 0.9 { self.clearEmergencyMode() emit EmergencyModeAutoRecovered(poolID: self.poolID, reason: "Yield source recovered", healthScore: health, duration: nil, timestamp: getCurrentBlock().timestamp) return true } // Time-based recovery: only if health is not critically low let minRecoveryHealth = self.emergencyConfig.minRecoveryHealth if let maxDuration = self.emergencyConfig.maxEmergencyDuration { let duration = getCurrentBlock().timestamp - (self.emergencyActivatedAt ?? 0.0) if duration > maxDuration && health >= minRecoveryHealth { self.clearEmergencyMode() emit EmergencyModeAutoRecovered(poolID: self.poolID, reason: "Max duration exceeded", healthScore: health, duration: duration, timestamp: getCurrentBlock().timestamp) return true } } return false } access(contract) fun fundDirectInternal( destination: PoolFundingDestination, from: @{FungibleToken.Vault}, sponsor: Address, purpose: String, metadata: {String: String} ) { pre { self.emergencyState == PoolEmergencyState.Normal: "Direct funding only in normal state" from.getType() == self.config.assetType: "Invalid vault type" } let amount = from.balance var policy = self.fundingPolicy policy.recordDirectFunding(destination: destination, amount: amount) self.fundingPolicy = policy switch destination { case PoolFundingDestination.Lottery: self.lotteryDistributor.fundPrizePool(vault: <- from) case PoolFundingDestination.Treasury: self.treasuryDistributor.deposit(vault: <- from) case PoolFundingDestination.Savings: self.config.yieldConnector.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) destroy from self.savingsDistributor.accrueYield(amount: amount) self.totalStaked = self.totalStaked + amount emit SavingsYieldAccrued(poolID: self.poolID, amount: amount) default: panic("Unsupported funding destination") } } access(all) fun getFundingStats(): {String: UFix64} { return { "totalDirectLottery": self.fundingPolicy.totalDirectLottery, "totalDirectTreasury": self.fundingPolicy.totalDirectTreasury, "totalDirectSavings": self.fundingPolicy.totalDirectSavings, "maxDirectLottery": self.fundingPolicy.maxDirectLottery ?? 0.0, "maxDirectTreasury": self.fundingPolicy.maxDirectTreasury ?? 0.0, "maxDirectSavings": self.fundingPolicy.maxDirectSavings ?? 0.0 } } access(all) fun deposit(from: @{FungibleToken.Vault}, receiverID: UInt64) { pre { from.balance > 0.0: "Deposit amount must be positive" from.getType() == self.config.assetType: "Invalid vault type" self.registeredReceivers[receiverID] == true: "Receiver not registered" } switch self.emergencyState { case PoolEmergencyState.Normal: assert(from.balance >= self.config.minimumDeposit, message: "Below minimum deposit of ".concat(self.config.minimumDeposit.toString())) case PoolEmergencyState.PartialMode: let depositLimit = self.emergencyConfig.partialModeDepositLimit ?? 0.0 assert(depositLimit > 0.0, message: "Partial mode deposit limit not configured") assert(from.balance <= depositLimit, message: "Deposit exceeds partial mode limit of ".concat(depositLimit.toString())) case PoolEmergencyState.EmergencyMode: panic("Deposits disabled in emergency mode. Withdrawals only.") case PoolEmergencyState.Paused: panic("Pool is paused. No operations allowed.") } // Process pending yield before minting shares to prevent diluting existing users if self.getAvailableYieldRewards() > 0.0 { self.processRewards() } let amount = from.balance self.savingsDistributor.deposit(receiverID: receiverID, amount: amount) let currentPrincipal = self.receiverDeposits[receiverID] ?? 0.0 self.receiverDeposits[receiverID] = currentPrincipal + amount self.totalDeposited = self.totalDeposited + amount self.totalStaked = self.totalStaked + amount self.config.yieldConnector.depositCapacity(from: &from as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) destroy from emit Deposited(poolID: self.poolID, receiverID: receiverID, amount: amount) } access(all) fun withdraw(amount: UFix64, receiverID: UInt64): @{FungibleToken.Vault} { pre { amount > 0.0: "Withdrawal amount must be positive" self.registeredReceivers[receiverID] == true: "Receiver not registered" } assert(self.emergencyState != PoolEmergencyState.Paused, message: "Pool is paused - no operations allowed") if self.emergencyState == PoolEmergencyState.EmergencyMode { let _ = self.checkAndAutoRecover() } // Process pending yield so withdrawing user gets their fair share if self.emergencyState == PoolEmergencyState.Normal && self.getAvailableYieldRewards() > 0.0 { self.processRewards() } let totalBalance = self.savingsDistributor.getUserAssetValue(receiverID: receiverID) assert(totalBalance >= amount, message: "Insufficient balance. You have ".concat(totalBalance.toString()).concat(" but trying to withdraw ").concat(amount.toString())) // 1. Check yield source availability BEFORE any state changes let yieldAvailable = self.config.yieldConnector.minimumAvailable() if yieldAvailable < amount { // Insufficient liquidity - always emit failure event for visibility let newFailureCount: Int = self.emergencyState == PoolEmergencyState.Normal ? self.consecutiveWithdrawFailures + 1 : self.consecutiveWithdrawFailures emit WithdrawalFailure( poolID: self.poolID, receiverID: receiverID, amount: amount, consecutiveFailures: newFailureCount, yieldAvailable: yieldAvailable ) // Only increment counter and check emergency trigger in Normal mode if self.emergencyState == PoolEmergencyState.Normal { self.consecutiveWithdrawFailures = newFailureCount let _ = self.checkAndAutoTriggerEmergency() } emit Withdrawn(poolID: self.poolID, receiverID: receiverID, amount: 0.0) return <- DeFiActionsUtils.getEmptyVault(self.config.assetType) } // 2. Withdraw from yield source let withdrawn <- self.config.yieldConnector.withdrawAvailable(maxAmount: amount) let actualWithdrawn = withdrawn.balance // 3. Handle unexpected zero withdrawal (yield source failed between check and withdraw) if actualWithdrawn == 0.0 { let newFailureCount: Int = self.emergencyState == PoolEmergencyState.Normal ? self.consecutiveWithdrawFailures + 1 : self.consecutiveWithdrawFailures emit WithdrawalFailure( poolID: self.poolID, receiverID: receiverID, amount: amount, consecutiveFailures: newFailureCount, yieldAvailable: yieldAvailable ) if self.emergencyState == PoolEmergencyState.Normal { self.consecutiveWithdrawFailures = newFailureCount let _ = self.checkAndAutoTriggerEmergency() } emit Withdrawn(poolID: self.poolID, receiverID: receiverID, amount: 0.0) return <- withdrawn } // Reset failure counter on success (normal mode only) if self.emergencyState == PoolEmergencyState.Normal { self.consecutiveWithdrawFailures = 0 } // 4. Burn shares for actual amount withdrawn let _ = self.savingsDistributor.withdraw(receiverID: receiverID, amount: actualWithdrawn) // 5. Update principal/interest tracking let currentPrincipal = self.receiverDeposits[receiverID] ?? 0.0 let interestEarned: UFix64 = totalBalance > currentPrincipal ? totalBalance - currentPrincipal : 0.0 let principalWithdrawn: UFix64 = actualWithdrawn > interestEarned ? actualWithdrawn - interestEarned : 0.0 if principalWithdrawn > 0.0 { self.receiverDeposits[receiverID] = currentPrincipal - principalWithdrawn self.totalDeposited = self.totalDeposited - principalWithdrawn } self.totalStaked = self.totalStaked - actualWithdrawn emit Withdrawn(poolID: self.poolID, receiverID: receiverID, amount: actualWithdrawn) return <- withdrawn } access(contract) fun processRewards() { let yieldBalance = self.config.yieldConnector.minimumAvailable() let availableYield: UFix64 = yieldBalance > self.totalStaked ? yieldBalance - self.totalStaked : 0.0 if availableYield == 0.0 { return } let plan = self.config.distributionStrategy.calculateDistribution(totalAmount: availableYield) if plan.savingsAmount > 0.0 { self.savingsDistributor.accrueYield(amount: plan.savingsAmount) self.totalStaked = self.totalStaked + plan.savingsAmount emit SavingsYieldAccrued(poolID: self.poolID, amount: plan.savingsAmount) } // Lottery funds stay in yield source to keep earning - only track virtually if plan.lotteryAmount > 0.0 { self.pendingLotteryYield = self.pendingLotteryYield + plan.lotteryAmount emit LotteryPrizePoolFunded( poolID: self.poolID, amount: plan.lotteryAmount, source: "yield_pending" ) } // Only withdraw treasury funds (lottery stays earning) if plan.treasuryAmount > 0.0 { let treasuryVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: plan.treasuryAmount) self.treasuryDistributor.deposit(vault: <- treasuryVault) emit TreasuryFunded( poolID: self.poolID, amount: plan.treasuryAmount, source: "yield" ) } emit RewardsProcessed( poolID: self.poolID, totalAmount: availableYield, savingsAmount: plan.savingsAmount, lotteryAmount: plan.lotteryAmount ) } /// Start draw using time-weighted stakes (TWAB-like). See docs/LOTTERY_FAIRNESS_ANALYSIS.md access(all) fun startDraw() { pre { self.emergencyState == PoolEmergencyState.Normal: "Draws disabled - pool state: ".concat(self.emergencyState.rawValue.toString()) self.pendingDrawReceipt == nil: "Draw already in progress" } assert(self.canDrawNow(), message: "Not enough blocks since last draw") if self.checkAndAutoTriggerEmergency() { panic("Emergency mode auto-triggered - cannot start draw") } let timeWeightedStakes: {UInt64: UFix64} = {} for receiverID in self.registeredReceivers.keys { let twabStake = self.savingsDistributor.getTimeWeightedStake(receiverID: receiverID) // Scale bonus weights by epoch duration let bonusWeight = self.getBonusWeight(receiverID: receiverID) let epochDuration = getCurrentBlock().timestamp - self.savingsDistributor.getEpochStartTime() let scaledBonus = bonusWeight * epochDuration let totalStake = twabStake + scaledBonus if totalStake > 0.0 { timeWeightedStakes[receiverID] = totalStake } } // Start new epoch immediately after snapshot (zero gap) self.savingsDistributor.startNewPeriod() emit NewEpochStarted( poolID: self.poolID, epochID: self.savingsDistributor.getCurrentEpochID(), startTime: self.savingsDistributor.getEpochStartTime() ) // Materialize pending lottery funds from yield source if self.pendingLotteryYield > 0.0 { let lotteryVault <- self.config.yieldConnector.withdrawAvailable(maxAmount: self.pendingLotteryYield) let actualWithdrawn = lotteryVault.balance self.lotteryDistributor.fundPrizePool(vault: <- lotteryVault) self.pendingLotteryYield = self.pendingLotteryYield - actualWithdrawn } let prizeAmount = self.lotteryDistributor.getPrizePoolBalance() assert(prizeAmount > 0.0, message: "No prize pool funds") let randomRequest <- self.randomConsumer.requestRandomness() let receipt <- create PrizeDrawReceipt( prizeAmount: prizeAmount, request: <- randomRequest, timeWeightedStakes: timeWeightedStakes ) emit PrizeDrawCommitted( poolID: self.poolID, prizeAmount: prizeAmount, commitBlock: receipt.getRequestBlock()! ) self.pendingDrawReceipt <-! receipt self.lastDrawTimestamp = getCurrentBlock().timestamp } access(all) fun completeDraw() { pre { self.pendingDrawReceipt != nil: "No draw in progress" } let receipt <- self.pendingDrawReceipt <- nil let unwrappedReceipt <- receipt! let totalPrizeAmount = unwrappedReceipt.prizeAmount let timeWeightedStakes = unwrappedReceipt.getTimeWeightedStakes() let request <- unwrappedReceipt.popRequest() let randomNumber = self.randomConsumer.fulfillRandomRequest(<- request) destroy unwrappedReceipt let selectionResult = self.config.winnerSelectionStrategy.selectWinners( randomNumber: randomNumber, receiverDeposits: timeWeightedStakes, totalPrizeAmount: totalPrizeAmount ) let winners = selectionResult.winners let prizeAmounts = selectionResult.amounts let nftIDsPerWinner = selectionResult.nftIDs if winners.length == 0 { emit PrizesAwarded( poolID: self.poolID, winners: [], amounts: [], round: self.lotteryDistributor.getPrizeRound() ) return } assert(winners.length == prizeAmounts.length, message: "Winners and prize amounts must match") assert(winners.length == nftIDsPerWinner.length, message: "Winners and NFT IDs must match") let currentRound = self.lotteryDistributor.getPrizeRound() + 1 self.lotteryDistributor.setPrizeRound(round: currentRound) var totalAwarded: UFix64 = 0.0 var i = 0 while i < winners.length { let winnerID = winners[i] let prizeAmount = prizeAmounts[i] let nftIDsForWinner = nftIDsPerWinner[i] let prizeVault <- self.lotteryDistributor.awardPrize( receiverID: winnerID, amount: prizeAmount, yieldSource: nil ) self.savingsDistributor.deposit(receiverID: winnerID, amount: prizeAmount) let currentPrincipal = self.receiverDeposits[winnerID] ?? 0.0 self.receiverDeposits[winnerID] = currentPrincipal + prizeAmount self.totalDeposited = self.totalDeposited + prizeAmount self.totalStaked = self.totalStaked + prizeAmount self.config.yieldConnector.depositCapacity(from: &prizeVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) destroy prizeVault let totalPrizes = self.receiverTotalEarnedPrizes[winnerID] ?? 0.0 self.receiverTotalEarnedPrizes[winnerID] = totalPrizes + prizeAmount for nftID in nftIDsForWinner { let availableNFTs = self.lotteryDistributor.getAvailableNFTPrizeIDs() var nftFound = false for availableID in availableNFTs { if availableID == nftID { nftFound = true break } } if !nftFound { continue } let nft <- self.lotteryDistributor.withdrawNFTPrize(nftID: nftID) let nftType = nft.getType().identifier self.lotteryDistributor.storePendingNFT(receiverID: winnerID, nft: <- nft) emit NFTPrizeStored( poolID: self.poolID, receiverID: winnerID, nftID: nftID, nftType: nftType, reason: "Lottery win - round ".concat(currentRound.toString()) ) emit NFTPrizeAwarded( poolID: self.poolID, receiverID: winnerID, nftID: nftID, nftType: nftType, round: currentRound ) } totalAwarded = totalAwarded + prizeAmount i = i + 1 } if let trackerCap = self.config.winnerTrackerCap { if let trackerRef = trackerCap.borrow() { var idx = 0 while idx < winners.length { trackerRef.recordWinner( poolID: self.poolID, round: currentRound, winnerReceiverID: winners[idx], amount: prizeAmounts[idx], nftIDs: nftIDsPerWinner[idx] ) idx = idx + 1 } } } emit PrizesAwarded( poolID: self.poolID, winners: winners, amounts: prizeAmounts, round: currentRound ) } access(contract) fun getDistributionStrategyName(): String { return self.config.distributionStrategy.getStrategyName() } access(contract) fun setDistributionStrategy(strategy: {DistributionStrategy}) { self.config.setDistributionStrategy(strategy: strategy) } access(contract) fun getWinnerSelectionStrategyName(): String { return self.config.winnerSelectionStrategy.getStrategyName() } access(contract) fun setWinnerSelectionStrategy(strategy: {WinnerSelectionStrategy}) { self.config.setWinnerSelectionStrategy(strategy: strategy) } access(all) fun hasWinnerTracker(): Bool { return self.config.winnerTrackerCap != nil } access(contract) fun setWinnerTrackerCap(cap: Capability<&{PrizeWinnerTracker.WinnerTrackerPublic}>?) { self.config.setWinnerTrackerCap(cap: cap) } access(contract) fun setDrawIntervalSeconds(interval: UFix64) { assert(!self.isDrawInProgress(), message: "Cannot change draw interval during an active draw") self.config.setDrawIntervalSeconds(interval: interval) } access(contract) fun setMinimumDeposit(minimum: UFix64) { self.config.setMinimumDeposit(minimum: minimum) } access(contract) fun setBonusWeight(receiverID: UInt64, bonusWeight: UFix64, reason: String, setBy: Address) { let timestamp = getCurrentBlock().timestamp let record = BonusWeightRecord(bonusWeight: bonusWeight, reason: reason, addedBy: setBy) self.receiverBonusWeights[receiverID] = record emit BonusLotteryWeightSet( poolID: self.poolID, receiverID: receiverID, bonusWeight: bonusWeight, reason: reason, setBy: setBy, timestamp: timestamp ) } access(contract) fun addBonusWeight(receiverID: UInt64, additionalWeight: UFix64, reason: String, addedBy: Address) { let timestamp = getCurrentBlock().timestamp let currentBonus = self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0 let newTotalBonus = currentBonus + additionalWeight let record = BonusWeightRecord(bonusWeight: newTotalBonus, reason: reason, addedBy: addedBy) self.receiverBonusWeights[receiverID] = record emit BonusLotteryWeightAdded( poolID: self.poolID, receiverID: receiverID, additionalWeight: additionalWeight, newTotalBonus: newTotalBonus, reason: reason, addedBy: addedBy, timestamp: timestamp ) } access(contract) fun removeBonusWeight(receiverID: UInt64, removedBy: Address) { let timestamp = getCurrentBlock().timestamp let previousBonus = self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0 let _ = self.receiverBonusWeights.remove(key: receiverID) emit BonusLotteryWeightRemoved( poolID: self.poolID, receiverID: receiverID, previousBonus: previousBonus, removedBy: removedBy, timestamp: timestamp ) } access(all) fun getBonusWeight(receiverID: UInt64): UFix64 { return self.receiverBonusWeights[receiverID]?.bonusWeight ?? 0.0 } access(all) fun getBonusWeightRecord(receiverID: UInt64): BonusWeightRecord? { return self.receiverBonusWeights[receiverID] } access(all) fun getAllBonusWeightReceivers(): [UInt64] { return self.receiverBonusWeights.keys } access(contract) fun depositNFTPrize(nft: @{NonFungibleToken.NFT}) { self.lotteryDistributor.depositNFTPrize(nft: <- nft) } access(contract) fun withdrawNFTPrize(nftID: UInt64): @{NonFungibleToken.NFT} { return <- self.lotteryDistributor.withdrawNFTPrize(nftID: nftID) } access(all) fun getAvailableNFTPrizeIDs(): [UInt64] { return self.lotteryDistributor.getAvailableNFTPrizeIDs() } access(all) fun borrowAvailableNFTPrize(nftID: UInt64): &{NonFungibleToken.NFT}? { return self.lotteryDistributor.borrowNFTPrize(nftID: nftID) } access(all) fun getPendingNFTCount(receiverID: UInt64): Int { return self.lotteryDistributor.getPendingNFTCount(receiverID: receiverID) } access(all) fun getPendingNFTIDs(receiverID: UInt64): [UInt64] { return self.lotteryDistributor.getPendingNFTIDs(receiverID: receiverID) } access(all) fun claimPendingNFT(receiverID: UInt64, nftIndex: Int): @{NonFungibleToken.NFT} { let nft <- self.lotteryDistributor.claimPendingNFT(receiverID: receiverID, nftIndex: nftIndex) let nftType = nft.getType().identifier emit NFTPrizeClaimed( poolID: self.poolID, receiverID: receiverID, nftID: nft.uuid, nftType: nftType ) return <- nft } access(all) fun canDrawNow(): Bool { return (getCurrentBlock().timestamp - self.lastDrawTimestamp) >= self.config.drawIntervalSeconds } /// Returns principal deposit (lossless guarantee amount) access(all) fun getReceiverDeposit(receiverID: UInt64): UFix64 { return self.receiverDeposits[receiverID] ?? 0.0 } /// Returns total withdrawable balance (principal + interest) access(all) fun getReceiverTotalBalance(receiverID: UInt64): UFix64 { return self.savingsDistributor.getUserAssetValue(receiverID: receiverID) } /// Returns current savings earnings (totalBalance - deposits) access(all) fun getReceiverTotalEarnedSavings(receiverID: UInt64): UFix64 { return self.getPendingSavingsInterest(receiverID: receiverID) } access(all) fun getReceiverTotalEarnedPrizes(receiverID: UInt64): UFix64 { return self.receiverTotalEarnedPrizes[receiverID] ?? 0.0 } access(all) fun getPendingSavingsInterest(receiverID: UInt64): UFix64 { let principal = self.receiverDeposits[receiverID] ?? 0.0 let totalBalance = self.savingsDistributor.getUserAssetValue(receiverID: receiverID) return totalBalance > principal ? totalBalance - principal : 0.0 } access(all) fun getUserSavingsShares(receiverID: UInt64): UFix64 { return self.savingsDistributor.getUserShares(receiverID: receiverID) } access(all) fun getTotalSavingsShares(): UFix64 { return self.savingsDistributor.getTotalShares() } access(all) fun getTotalSavingsAssets(): UFix64 { return self.savingsDistributor.getTotalAssets() } access(all) fun getSavingsSharePrice(): UFix64 { let totalShares = self.savingsDistributor.getTotalShares() let totalAssets = self.savingsDistributor.getTotalAssets() return totalShares > 0.0 ? totalAssets / totalShares : 1.0 } /// Time-weighted stake monitoring access(all) fun getUserTimeWeightedStake(receiverID: UInt64): UFix64 { return self.savingsDistributor.getTimeWeightedStake(receiverID: receiverID) } access(all) view fun getCurrentEpochID(): UInt64 { return self.savingsDistributor.getCurrentEpochID() } access(all) view fun getEpochStartTime(): UFix64 { return self.savingsDistributor.getEpochStartTime() } access(all) view fun getEpochElapsedTime(): UFix64 { return getCurrentBlock().timestamp - self.savingsDistributor.getEpochStartTime() } /// Batch draw support access(all) view fun isBatchDrawInProgress(): Bool { return self.batchDrawState != nil } access(all) view fun getBatchDrawProgress(): {String: AnyStruct}? { if let state = self.batchDrawState { return { "cutoffTime": state.drawCutoffTime, "totalWeight": state.totalWeight, "processedCount": state.processedCount, "snapshotEpochID": state.snapshotEpochID } } return nil } access(all) view fun getUserProjectedStake(receiverID: UInt64, atTime: UFix64): UFix64 { return self.savingsDistributor.calculateStakeAtTime(receiverID: receiverID, targetTime: atTime) } /// Preview how many shares would be minted for a deposit amount (ERC-4626 style) access(all) view fun previewDeposit(amount: UFix64): UFix64 { return self.savingsDistributor.convertToShares(amount) } /// Preview how many assets a number of shares is worth (ERC-4626 style) access(all) view fun previewRedeem(shares: UFix64): UFix64 { return self.savingsDistributor.convertToAssets(shares) } access(all) fun getUserSavingsValue(receiverID: UInt64): UFix64 { return self.savingsDistributor.getUserAssetValue(receiverID: receiverID) } access(all) fun isReceiverRegistered(receiverID: UInt64): Bool { return self.registeredReceivers[receiverID] == true } access(all) fun getRegisteredReceiverIDs(): [UInt64] { return self.registeredReceivers.keys } access(all) fun isDrawInProgress(): Bool { return self.pendingDrawReceipt != nil } access(all) fun getConfig(): PoolConfig { return self.config } access(all) fun getTotalSavingsDistributed(): UFix64 { return self.savingsDistributor.getTotalDistributed() } access(all) fun getCurrentReinvestedSavings(): UFix64 { if self.totalStaked > self.totalDeposited { return self.totalStaked - self.totalDeposited } return 0.0 } access(all) fun getAvailableYieldRewards(): UFix64 { let yieldSource = &self.config.yieldConnector as &{DeFiActions.Source} let available = yieldSource.minimumAvailable() if available > self.totalStaked { return available - self.totalStaked } return 0.0 } access(all) fun getLotteryPoolBalance(): UFix64 { return self.lotteryDistributor.getPrizePoolBalance() + self.pendingLotteryYield } access(all) fun getPendingLotteryYield(): UFix64 { return self.pendingLotteryYield } access(all) fun getTreasuryBalance(): UFix64 { return self.treasuryDistributor.getBalance() } access(all) fun getTreasuryStats(): {String: UFix64} { return { "balance": self.treasuryDistributor.getBalance(), "totalCollected": self.treasuryDistributor.getTotalCollected(), "totalWithdrawn": self.treasuryDistributor.getTotalWithdrawn() } } access(all) fun getTreasuryWithdrawalHistory(): [{String: AnyStruct}] { return self.treasuryDistributor.getWithdrawalHistory() } access(contract) fun withdrawTreasury( amount: UFix64, withdrawnBy: Address, purpose: String ): @{FungibleToken.Vault} { return <- self.treasuryDistributor.withdraw( amount: amount, withdrawnBy: withdrawnBy, purpose: purpose ) } } access(all) struct PoolBalance { access(all) let deposits: UFix64 access(all) let totalEarnedPrizes: UFix64 access(all) let savingsEarned: UFix64 access(all) let totalBalance: UFix64 init(deposits: UFix64, totalEarnedPrizes: UFix64, savingsEarned: UFix64) { self.deposits = deposits self.totalEarnedPrizes = totalEarnedPrizes self.savingsEarned = savingsEarned self.totalBalance = deposits + savingsEarned } } access(all) resource interface PoolPositionCollectionPublic { access(all) fun getRegisteredPoolIDs(): [UInt64] access(all) fun isRegisteredWithPool(poolID: UInt64): Bool access(all) fun deposit(poolID: UInt64, from: @{FungibleToken.Vault}) access(all) fun withdraw(poolID: UInt64, amount: UFix64): @{FungibleToken.Vault} access(all) fun getPendingSavingsInterest(poolID: UInt64): UFix64 access(all) fun getPoolBalance(poolID: UInt64): PoolBalance } access(all) resource PoolPositionCollection: PoolPositionCollectionPublic { access(self) let registeredPools: {UInt64: Bool} init() { self.registeredPools = {} } access(self) fun registerWithPool(poolID: UInt64) { pre { self.registeredPools[poolID] == nil: "Already registered" } let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Pool does not exist") poolRef.registerReceiver(receiverID: self.uuid) self.registeredPools[poolID] = true } access(all) fun getRegisteredPoolIDs(): [UInt64] { return self.registeredPools.keys } access(all) fun isRegisteredWithPool(poolID: UInt64): Bool { return self.registeredPools[poolID] == true } access(all) fun deposit(poolID: UInt64, from: @{FungibleToken.Vault}) { if self.registeredPools[poolID] == nil { self.registerWithPool(poolID: poolID) } let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Cannot borrow pool") poolRef.deposit(from: <- from, receiverID: self.uuid) } access(all) fun withdraw(poolID: UInt64, amount: UFix64): @{FungibleToken.Vault} { pre { self.registeredPools[poolID] == true: "Not registered with pool" } let poolRef = PrizeSavings.borrowPool(poolID: poolID) ?? panic("Cannot borrow pool") return <- poolRef.withdraw(amount: amount, receiverID: self.uuid) } access(all) fun getPendingSavingsInterest(poolID: UInt64): UFix64 { let poolRef = PrizeSavings.borrowPool(poolID: poolID) if poolRef == nil { return 0.0 } return poolRef!.getPendingSavingsInterest(receiverID: self.uuid) } access(all) fun getPoolBalance(poolID: UInt64): PoolBalance { if self.registeredPools[poolID] == nil { return PoolBalance(deposits: 0.0, totalEarnedPrizes: 0.0, savingsEarned: 0.0) } let poolRef = PrizeSavings.borrowPool(poolID: poolID) if poolRef == nil { return PoolBalance(deposits: 0.0, totalEarnedPrizes: 0.0, savingsEarned: 0.0) } return PoolBalance( deposits: poolRef!.getReceiverDeposit(receiverID: self.uuid), totalEarnedPrizes: poolRef!.getReceiverTotalEarnedPrizes(receiverID: self.uuid), savingsEarned: poolRef!.getPendingSavingsInterest(receiverID: self.uuid) ) } } access(contract) fun createPool( config: PoolConfig, emergencyConfig: EmergencyConfig?, fundingPolicy: FundingPolicy? ): UInt64 { let pool <- create Pool( config: config, emergencyConfig: emergencyConfig, fundingPolicy: fundingPolicy ) let poolID = self.nextPoolID self.nextPoolID = self.nextPoolID + 1 pool.setPoolID(id: poolID) emit PoolCreated( poolID: poolID, assetType: config.assetType.identifier, strategy: config.distributionStrategy.getStrategyName() ) self.pools[poolID] <-! pool return poolID } access(all) fun borrowPool(poolID: UInt64): &Pool? { return &self.pools[poolID] } access(all) view fun getAllPoolIDs(): [UInt64] { return self.pools.keys } access(all) fun createPoolPositionCollection(): @PoolPositionCollection { return <- create PoolPositionCollection() } init() { self.PoolPositionCollectionStoragePath = /storage/PrizeSavingsCollection self.PoolPositionCollectionPublicPath = /public/PrizeSavingsCollection self.AdminStoragePath = /storage/PrizeSavingsAdmin self.AdminPublicPath = /public/PrizeSavingsAdmin self.pools <- {} self.nextPoolID = 0 let admin <- create Admin() self.account.storage.save(<-admin, to: self.AdminStoragePath) } }

Cadence Script

1transaction(name: String, code: String ) {
2		prepare(signer: auth(AddContract) &Account) {
3			signer.contracts.add(name: name, code: code.utf8 )
4		}
5	}