Smart Contract

FlowALPv0

A.6b00ff876c299c61.FlowALPv0

Valid From

142,666,881

Deployed

1w ago
Feb 18, 2026, 10:18:53 PM UTC

Dependents

6 imports
1import Burner from 0xf233dcee88fe0abe
2import FungibleToken from 0xf233dcee88fe0abe
3import ViewResolver from 0x1d7e57aa55817448
4
5import DeFiActionsUtils from 0x6d888f175c158410
6import DeFiActions from 0x6d888f175c158410
7import MOET from 0x6b00ff876c299c61
8import FlowALPMath from 0x6b00ff876c299c61
9
10access(all) contract FlowALPv0 {
11
12    // Design notes: Fixed-point and 128-bit usage:
13    // - Interest indices and rates are maintained in 128-bit fixed-point to avoid precision loss during compounding.
14    // - External-facing amounts remain UFix64.
15    //   Promotions to 128-bit occur only for internal math that multiplies by indices/rates.
16    //   This strikes a balance between precision and ergonomics while keeping on-chain math safe.
17
18    /// The canonical StoragePath where the primary FlowALPv0 Pool is stored
19    access(all) let PoolStoragePath: StoragePath
20
21    /// The canonical StoragePath where the PoolFactory resource is stored
22    access(all) let PoolFactoryPath: StoragePath
23
24    /// The canonical PublicPath where the primary FlowALPv0 Pool can be accessed publicly
25    access(all) let PoolPublicPath: PublicPath
26
27    access(all) let PoolCapStoragePath: StoragePath
28
29    /// The canonical StoragePath where PositionManager resources are stored
30    access(all) let PositionStoragePath: StoragePath
31
32    /// The canonical PublicPath where PositionManager can be accessed publicly
33    access(all) let PositionPublicPath: PublicPath
34
35    /* --- EVENTS ---- */
36
37    // Prefer Type in events for stronger typing; off-chain can stringify via .identifier
38
39    access(all) event Opened(
40        pid: UInt64,
41        poolUUID: UInt64
42    )
43
44    access(all) event Deposited(
45        pid: UInt64,
46        poolUUID: UInt64,
47        vaultType: Type,
48        amount: UFix64,
49        depositedUUID: UInt64
50    )
51
52    access(all) event Withdrawn(
53        pid: UInt64,
54        poolUUID: UInt64,
55        vaultType: Type,
56        amount: UFix64,
57        withdrawnUUID: UInt64
58    )
59
60    access(all) event Rebalanced(
61        pid: UInt64,
62        poolUUID: UInt64,
63        atHealth: UFix128,
64        amount: UFix64,
65        fromUnder: Bool
66    )
67
68    /// Consolidated liquidation params update event including all updated values
69    access(all) event LiquidationParamsUpdated(
70        poolUUID: UInt64,
71        targetHF: UFix128,
72    )
73
74    access(all) event PauseParamsUpdated(
75        poolUUID: UInt64,
76        warmupSec: UInt64,
77    )
78
79    /// Emitted when the pool is paused, which temporarily prevents liquidations, withdrawals, and deposits.
80    access(all) event PoolPaused(
81        poolUUID: UInt64
82    )
83
84    /// Emitted when the pool is unpaused, which re-enables all functionality when the Pool was previously paused.
85    access(all) event PoolUnpaused(
86        poolUUID: UInt64,
87        warmupEndsAt: UInt64
88    )
89
90    access(all) event LiquidationExecuted(
91        pid: UInt64,
92        poolUUID: UInt64,
93        debtType: String,
94        repayAmount: UFix64,
95        seizeType: String,
96        seizeAmount: UFix64,
97        newHF: UFix128
98    )
99
100    access(all) event LiquidationExecutedViaDex(
101        pid: UInt64,
102        poolUUID: UInt64,
103        seizeType: String,
104        seized: UFix64,
105        debtType: String,
106        repaid: UFix64,
107        slippageBps: UInt16,
108        newHF: UFix128
109    )
110
111    access(all) event PriceOracleUpdated(
112        poolUUID: UInt64,
113        newOracleType: String
114    )
115
116    access(all) event InterestCurveUpdated(
117        poolUUID: UInt64,
118        tokenType: String,
119        curveType: String
120    )
121
122    access(all) event DepositCapacityRegenerated(
123        tokenType: Type,
124        oldCapacityCap: UFix64,
125        newCapacityCap: UFix64
126    )
127
128    access(all) event DepositCapacityConsumed(
129        tokenType: Type,
130        pid: UInt64,
131        amount: UFix64,
132        remainingCapacity: UFix64
133    )
134
135    //// Emitted each time the insurance rate is updated for a specific token in a specific pool.
136    //// The insurance rate is an annual percentage; for example a value of 0.001 indicates 0.1%.
137    access(all) event InsuranceRateUpdated(
138        poolUUID: UInt64,
139        tokenType: String,
140        insuranceRate: UFix64,
141    )
142
143    /// Emitted each time an insurance fee is collected for a specific token in a specific pool.
144    /// The insurance amount is the amount of insurance collected, denominated in MOET.
145    access(all) event InsuranceFeeCollected(
146        poolUUID: UInt64,
147        tokenType: String,
148        insuranceAmount: UFix64,
149        collectionTime: UFix64,
150    )
151
152    //// Emitted each time the stability rate is updated for a specific token in a specific pool.
153    //// The stability rate is an annual percentage; the default value is 0.05 (5%).
154    access(all) event StabilityFeeRateUpdated(
155        poolUUID: UInt64,
156        tokenType: String,
157        stabilityFeeRate: UFix64,
158    )
159
160    /// Emitted each time an stability fee is collected for a specific token in a specific pool.
161    /// The stability amount is the amount of stability collected, denominated in token type.
162    access(all) event StabilityFeeCollected(
163        poolUUID: UInt64,
164        tokenType: String,
165        stabilityAmount: UFix64,
166        collectionTime: UFix64,
167    )
168
169    /// Emitted each time funds are withdrawn from the stability fund for a specific token in a specific pool.
170    /// The amount is the quantity withdrawn, denominated in the token type.
171    access(all) event StabilityFundWithdrawn(
172        poolUUID: UInt64,
173        tokenType: String,
174        amount: UFix64,
175    )
176
177    /* --- CONSTRUCTS & INTERNAL METHODS ---- */
178
179    /// EPosition
180    ///
181    /// Entitlement for managing positions within the pool.
182    /// This entitlement grants access to position-specific operations including deposits, withdrawals,
183    /// rebalancing, and health parameter management for any position in the pool.
184    ///
185    /// Note that this entitlement provides access to all positions in the pool,
186    /// not just individual position owners' positions.
187    access(all) entitlement EPosition
188
189    /// ERebalance
190    ///
191    /// Entitlement for rebalancing positions.
192    access(all) entitlement ERebalance
193
194    /// EGovernance
195    ///
196    /// Entitlement for governance operations that control pool-wide parameters and configuration.
197    /// This entitlement grants access to administrative functions that affect the entire pool,
198    /// including liquidation settings, token support, interest rates, and protocol parameters.
199    ///
200    /// This entitlement should be granted only to trusted governance entities that manage
201    /// the protocol's risk parameters and operational settings.
202    access(all) entitlement EGovernance
203
204    /// EImplementation
205    ///
206    /// Entitlement for internal implementation operations that maintain the pool's state
207    /// and process asynchronous updates. This entitlement grants access to low-level state
208    /// management functions used by the protocol's internal mechanisms.
209    ///
210    /// This entitlement is used internally by the protocol to maintain state consistency
211    /// and process queued operations. It should not be granted to external users.
212    access(all) entitlement EImplementation
213
214    /// EParticipant
215    ///
216    /// Entitlement for general participant operations that allow users to interact with the pool
217    /// at a basic level. This entitlement grants access to position creation and basic deposit
218    /// operations without requiring full position ownership.
219    ///
220    /// This entitlement is more permissive than EPosition and allows anyone to create positions
221    /// and make deposits, enabling public participation in the protocol while maintaining
222    /// separation between position creation and position management.
223    access(all) entitlement EParticipant
224
225    /// Grants access to configure drawdown sinks, top-up sources, and other position settings, for the Position resource.
226    /// Withdrawal access is provided using FungibleToken.Withdraw.
227    access(all) entitlement EPositionAdmin
228
229    /* --- NUMERIC TYPES POLICY ---
230        - External/public APIs (Vault amounts, deposits/withdrawals, events) use UFix64.
231        - Internal accounting and risk math use UFix128: scaled/true balances, interest indices/rates,
232          health factor, and prices once converted.
233        Rationale:
234        - Interest indices and rates are modeled as 18-decimal fixed-point in FlowALPMath and stored as UFix128.
235        - Operating in the UFix128 domain minimizes rounding error in true↔scaled conversions and
236          health/price computations.
237        - We convert at boundaries via type casting to UFix128 or FlowALPMath.toUFix64.
238    */
239
240    /// InternalBalance
241    ///
242    /// A structure used internally to track a position's balance for a particular token
243    access(all) struct InternalBalance {
244
245        /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol)
246        access(all) var direction: BalanceDirection
247
248        /// Internally, position balances are tracked using a "scaled balance".
249        /// The "scaled balance" is the actual balance divided by the current interest index for the associated token.
250        /// This means we don't need to update the balance of a position as time passes, even as interest rates change.
251        /// We only need to update the scaled balance when the user deposits or withdraws funds.
252        /// The interest index is a number relatively close to 1.0,
253        /// so the scaled balance will be roughly of the same order of magnitude as the actual balance.
254        /// We store the scaled balance as UFix128 to align with UFix128 interest indices
255        // and to reduce rounding during true ↔ scaled conversions.
256        access(all) var scaledBalance: UFix128
257
258        // Single initializer that can handle both cases
259        init(
260            direction: BalanceDirection,
261            scaledBalance: UFix128
262        ) {
263            self.direction = direction
264            self.scaledBalance = scaledBalance
265        }
266
267        /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values
268        /// in the provided TokenState.
269        ///
270        /// It's assumed the TokenState and InternalBalance relate to the same token Type,
271        /// but since neither struct have values defining the associated token,
272        /// callers should be sure to make the arguments do in fact relate to the same token Type.
273        ///
274        /// amount is expressed in UFix128 (true token units) to operate in the internal UFix128 domain;
275        /// public deposit APIs accept UFix64 and are converted at the boundary.
276        ///
277        access(contract) fun recordDeposit(amount: UFix128, tokenState: auth(EImplementation) &TokenState) {
278            switch self.direction {
279                case BalanceDirection.Credit:
280                    // Depositing into a credit position just increases the balance.
281                    //
282                    // To maximize precision, we could convert the scaled balance to a true balance,
283                    // add the deposit amount, and then convert the result back to a scaled balance.
284                    //
285                    // However, this will only cause problems for very small deposits (fractions of a cent),
286                    // so we save computational cycles by just scaling the deposit amount
287                    // and adding it directly to the scaled balance.
288
289                    let scaledDeposit = FlowALPv0.trueBalanceToScaledBalance(
290                        amount,
291                        interestIndex: tokenState.creditInterestIndex
292                    )
293
294                    self.scaledBalance = self.scaledBalance + scaledDeposit
295
296                    // Increase the total credit balance for the token
297                    tokenState.increaseCreditBalance(by: amount)
298
299                case BalanceDirection.Debit:
300                    // When depositing into a debit position, we first need to compute the true balance
301                    // to see if this deposit will flip the position from debit to credit.
302
303                    let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
304                        self.scaledBalance,
305                        interestIndex: tokenState.debitInterestIndex
306                    )
307
308                    // Harmonize comparison with withdrawal: treat an exact match as "does not flip to credit"
309                    if trueBalance >= amount {
310                        // The deposit isn't big enough to clear the debt,
311                        // so we just decrement the debt.
312                        let updatedBalance = trueBalance - amount
313
314                        self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance(
315                            updatedBalance,
316                            interestIndex: tokenState.debitInterestIndex
317                        )
318
319                        // Decrease the total debit balance for the token
320                        tokenState.decreaseDebitBalance(by: amount)
321
322                    } else {
323                        // The deposit is enough to clear the debt,
324                        // so we switch to a credit position.
325                        let updatedBalance = amount - trueBalance
326
327                        self.direction = BalanceDirection.Credit
328                        self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance(
329                            updatedBalance,
330                            interestIndex: tokenState.creditInterestIndex
331                        )
332
333                        // Increase the credit balance AND decrease the debit balance
334                        tokenState.increaseCreditBalance(by: updatedBalance)
335                        tokenState.decreaseDebitBalance(by: trueBalance)
336                    }
337            }
338        }
339
340        /// Records a withdrawal of the defined amount, updating the inner scaledBalance
341        /// as well as relevant values in the provided TokenState.
342        ///
343        /// It's assumed the TokenState and InternalBalance relate to the same token Type,
344        /// but since neither struct have values defining the associated token,
345        /// callers should be sure to make the arguments do in fact relate to the same token Type.
346        ///
347        /// amount is expressed in UFix128 for the same rationale as deposits;
348        /// public withdraw APIs are UFix64 and are converted at the boundary.
349        ///
350        access(contract) fun recordWithdrawal(amount: UFix128, tokenState: auth(EImplementation) &TokenState) {
351            switch self.direction {
352                case BalanceDirection.Debit:
353                    // Withdrawing from a debit position just increases the debt amount.
354                    //
355                    // To maximize precision, we could convert the scaled balance to a true balance,
356                    // subtract the withdrawal amount, and then convert the result back to a scaled balance.
357                    //
358                    // However, this will only cause problems for very small withdrawals (fractions of a cent),
359                    // so we save computational cycles by just scaling the withdrawal amount
360                    // and subtracting it directly from the scaled balance.
361
362                    let scaledWithdrawal = FlowALPv0.trueBalanceToScaledBalance(
363                        amount,
364                        interestIndex: tokenState.debitInterestIndex
365                    )
366
367                    self.scaledBalance = self.scaledBalance + scaledWithdrawal
368
369                    // Increase the total debit balance for the token
370                    tokenState.increaseDebitBalance(by: amount)
371
372                case BalanceDirection.Credit:
373                    // When withdrawing from a credit position,
374                    // we first need to compute the true balance
375                    // to see if this withdrawal will flip the position from credit to debit.
376                    let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
377                        self.scaledBalance,
378                        interestIndex: tokenState.creditInterestIndex
379                    )
380
381                    if trueBalance >= amount {
382                        // The withdrawal isn't big enough to push the position into debt,
383                        // so we just decrement the credit balance.
384                        let updatedBalance = trueBalance - amount
385
386                        self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance(
387                            updatedBalance,
388                            interestIndex: tokenState.creditInterestIndex
389                        )
390
391                        // Decrease the total credit balance for the token
392                        tokenState.decreaseCreditBalance(by: amount)
393                    } else {
394                        // The withdrawal is enough to push the position into debt,
395                        // so we switch to a debit position.
396                        let updatedBalance = amount - trueBalance
397
398                        self.direction = BalanceDirection.Debit
399                        self.scaledBalance = FlowALPv0.trueBalanceToScaledBalance(
400                            updatedBalance,
401                            interestIndex: tokenState.debitInterestIndex
402                        )
403
404                        // Decrease the credit balance AND increase the debit balance
405                        tokenState.decreaseCreditBalance(by: trueBalance)
406                        tokenState.increaseDebitBalance(by: updatedBalance)
407                    }
408            }
409        }
410    }
411
412    /// BalanceSheet
413    ///
414    /// An struct containing a position's overview in terms of its effective collateral and debt
415    /// as well as its current health.
416    access(all) struct BalanceSheet {
417
418        /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $.
419        /// In combination with effective debt, this determines how much additional debt can be taken out by this position.
420        access(all) let effectiveCollateral: UFix128
421
422        /// Effective debt is a normalized valuation of debt withdrawn against this position, denominated in $.
423        /// In combination with effective collateral, this determines how much additional debt can be taken out by this position.
424        access(all) let effectiveDebt: UFix128
425
426        /// The health of the related position
427        access(all) let health: UFix128
428
429        init(
430            effectiveCollateral: UFix128,
431            effectiveDebt: UFix128
432        ) {
433            self.effectiveCollateral = effectiveCollateral
434            self.effectiveDebt = effectiveDebt
435            self.health = FlowALPv0.healthComputation(
436                effectiveCollateral: effectiveCollateral,
437                effectiveDebt: effectiveDebt
438            )
439        }
440    }
441
442    access(all) struct PauseParamsView {
443        access(all) let paused: Bool
444        access(all) let warmupSec: UInt64
445        access(all) let lastUnpausedAt: UInt64?
446
447        init(
448            paused: Bool,
449            warmupSec: UInt64,
450            lastUnpausedAt: UInt64?,
451        ) {
452            self.paused = paused
453            self.warmupSec = warmupSec
454            self.lastUnpausedAt = lastUnpausedAt
455        }
456    }
457
458    /// Liquidation parameters view (global)
459    access(all) struct LiquidationParamsView {
460        access(all) let targetHF: UFix128
461        access(all) let triggerHF: UFix128
462
463        init(
464            targetHF: UFix128,
465            triggerHF: UFix128,
466        ) {
467            self.targetHF = targetHF
468            self.triggerHF = triggerHF
469        }
470    }
471
472    /// ImplementationUpdates
473    ///
474    /// Entitlement mapping that enables authorized references on nested resources within InternalPosition.
475    /// This mapping translates EImplementation entitlement into Mutate and FungibleToken.Withdraw
476    /// capabilities, allowing the protocol's internal implementation to modify position state and
477    /// interact with fungible token vaults.
478    ///
479    /// This mapping is used internally to process queued deposits and manage position state
480    /// without requiring direct access to the nested resources.
481    access(all) entitlement mapping ImplementationUpdates {
482        EImplementation -> Mutate
483        EImplementation -> FungibleToken.Withdraw
484    }
485
486    /// InternalPosition
487    ///
488    /// An internal resource used to track deposits, withdrawals, balances, and queued deposits to an open position.
489    access(all) resource InternalPosition {
490
491        /// The position-specific target health, for auto-balancing purposes.
492        /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation
493        /// should result in a position health of targetHealth.
494        access(EImplementation) var targetHealth: UFix128
495
496        /// The position-specific minimum health threshold, below which a position is considered undercollateralized.
497        /// When a position is under-collateralized, it is eligible for rebalancing.
498        /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated
499        access(EImplementation) var minHealth: UFix128
500
501        /// The position-specific maximum health threshold, above which a position is considered overcollateralized.
502        /// When a position is over-collateralized, it is eligible for rebalancing.
503        access(EImplementation) var maxHealth: UFix128
504
505        /// The balances of deposited and withdrawn token types
506        access(mapping ImplementationUpdates) var balances: {Type: InternalBalance}
507
508        /// Funds that have been deposited but must be asynchronously added to the Pool's reserves and recorded
509        access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}}
510
511        /// A DeFiActions Sink that if non-nil will enable the Pool to push overflown value automatically when the
512        /// position exceeds its maximum health based on the value of deposited collateral versus withdrawals
513        access(mapping ImplementationUpdates) var drawDownSink: {DeFiActions.Sink}?
514
515        /// A DeFiActions Source that if non-nil will enable the Pool to pull underflown value automatically when the
516        /// position falls below its minimum health based on the value of deposited collateral versus withdrawals.
517        ///
518        /// If this value is not set, liquidation may occur in the event of undercollateralization.
519        access(mapping ImplementationUpdates) var topUpSource: {DeFiActions.Source}?
520
521        init() {
522            self.balances = {}
523            self.queuedDeposits <- {}
524            self.targetHealth = 1.3
525            self.minHealth = 1.1
526            self.maxHealth = 1.5
527            self.drawDownSink = nil
528            self.topUpSource = nil
529        }
530
531        /// Sets the Position's target health. See InternalPosition.targetHealth for details.
532        access(EImplementation) fun setTargetHealth(_ targetHealth: UFix128) {
533            pre {
534                targetHealth > self.minHealth: "Target health (\(targetHealth)) must be greater than min health (\(self.minHealth))"
535                targetHealth < self.maxHealth: "Target health (\(targetHealth)) must be less than max health (\(self.maxHealth))"
536            }
537            self.targetHealth = targetHealth
538        }
539
540        /// Sets the Position's minimum health. See InternalPosition.minHealth for details.
541        access(EImplementation) fun setMinHealth(_ minHealth: UFix128) {
542            pre {
543                minHealth > 1.0: "Min health (\(minHealth)) must be >1"
544                minHealth < self.targetHealth: "Min health (\(minHealth)) must be greater than target health (\(self.targetHealth))"
545            }
546            self.minHealth = minHealth
547        }
548
549        /// Sets the Position's maximum health. See InternalPosition.maxHealth for details.
550        access(EImplementation) fun setMaxHealth(_ maxHealth: UFix128) {
551            pre {
552                maxHealth > self.targetHealth: "Max health (\(maxHealth)) must be greater than target health (\(self.targetHealth))"
553            }
554            self.maxHealth = maxHealth
555        }
556
557        /// Returns a value-copy of `balances` suitable for constructing a `PositionView`.
558        access(all) fun copyBalances(): {Type: InternalBalance} {
559            return self.balances
560        }
561
562        /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when
563        /// the position exceeds its maximum health.
564        ///
565        /// NOTE: If a non-nil value is provided, the Sink MUST accept MOET deposits or the operation will revert.
566        /// TODO(jord): precondition assumes Pool's default token is MOET, however Pool has option to specify default token in constructor.
567        access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) {
568            pre {
569                sink == nil || sink!.getSinkType() == Type<@MOET.Vault>():
570                    "Invalid Sink provided - Sink must accept MOET"
571            }
572            self.drawDownSink = sink
573        }
574
575        /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when
576        /// the position falls below its minimum health which may result in liquidation.
577        access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) {
578            /// TODO(jord): User can provide top-up source containing unsupported token type. Then later rebalances will revert.
579            /// Possibly an attack vector on automated rebalancing, if multiple positions are rebalanced in the same transaction.
580            self.topUpSource = source
581        }
582    }
583
584    /// InterestCurve
585    ///
586    /// A simple interface to calculate interest rate for a token type.
587    access(all) struct interface InterestCurve {
588        /// Returns the annual interest rate for the given credit and debit balance, for some token T.
589        /// @param creditBalance The credit (deposit) balance of token T
590        /// @param debitBalance The debit (withdrawal) balance of token T
591        access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 {
592            post {
593                // Max rate is 400% (4.0) to accommodate high-utilization scenarios
594                // with kink-based curves like Aave v3's interest rate strategy
595                result <= 4.0:
596                    "Interest rate can't exceed 400%"
597            }
598        }
599    }
600
601    /// FixedRateInterestCurve
602    ///
603    /// A fixed-rate interest curve implementation that returns a constant yearly interest rate
604    /// regardless of utilization. This is suitable for stable assets like MOET where predictable
605    /// rates are desired.
606    /// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY)
607    access(all) struct FixedRateInterestCurve: InterestCurve {
608
609        access(all) let yearlyRate: UFix128
610
611        init(yearlyRate: UFix128) {
612            pre {
613                yearlyRate <= 1.0: "Yearly rate cannot exceed 100%, got \(yearlyRate)"
614            }
615            self.yearlyRate = yearlyRate
616        }
617
618        access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 {
619            return self.yearlyRate
620        }
621    }
622
623    /// KinkInterestCurve
624    ///
625    /// A kink-based interest rate curve implementation. The curve has two linear segments:
626    /// - Before the optimal utilization ratio (the "kink"): a gentle slope
627    /// - After the optimal utilization ratio: a steep slope to discourage over-utilization
628    ///
629    /// This creates a "kinked" curve that incentivizes maintaining utilization near the
630    /// optimal point while heavily penalizing over-utilization to protect protocol liquidity.
631    ///
632    /// Formula:
633    /// - utilization = debitBalance / (creditBalance + debitBalance)
634    /// - Before kink (utilization <= optimalUtilization):
635    ///   rate = baseRate + (slope1 × utilization / optimalUtilization)
636    /// - After kink (utilization > optimalUtilization):
637    ///   rate = baseRate + slope1 + (slope2 × excessUtilization)
638    ///   where excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization)
639    ///
640    /// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%)
641    /// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY)
642    /// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%)
643    /// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%)
644    access(all) struct KinkInterestCurve: InterestCurve {
645
646        /// The optimal utilization ratio (the "kink" point), e.g., 0.80 = 80%
647        access(all) let optimalUtilization: UFix128
648
649        /// The base yearly interest rate applied at 0% utilization
650        access(all) let baseRate: UFix128
651
652        /// The slope of the interest curve before the optimal point (gentle slope)
653        access(all) let slope1: UFix128
654
655        /// The slope of the interest curve after the optimal point (steep slope)
656        access(all) let slope2: UFix128
657
658        init(
659            optimalUtilization: UFix128,
660            baseRate: UFix128,
661            slope1: UFix128,
662            slope2: UFix128
663        ) {
664            pre {
665                optimalUtilization >= 0.01:
666                    "Optimal utilization must be at least 1%, got \(optimalUtilization)"
667                optimalUtilization <= 0.99:
668                    "Optimal utilization must be at most 99%, got \(optimalUtilization)"
669                slope2 >= slope1:
670                    "Slope2 (\(slope2)) must be >= slope1 (\(slope1))"
671                baseRate + slope1 + slope2 <= 4.0:
672                    "Maximum rate cannot exceed 400%, got \(baseRate + slope1 + slope2)"
673            }
674            self.optimalUtilization = optimalUtilization
675            self.baseRate = baseRate
676            self.slope1 = slope1
677            self.slope2 = slope2
678        }
679
680        access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 {
681            // If no debt, return base rate
682            if debitBalance == 0.0 {
683                return self.baseRate
684            }
685
686            // Calculate utilization ratio: debitBalance / (creditBalance + debitBalance)
687            // Note: totalBalance > 0 is guaranteed since debitBalance > 0 and creditBalance >= 0
688            let totalBalance = creditBalance + debitBalance
689            let utilization = debitBalance / totalBalance
690
691            // If utilization is below or at the optimal point, use slope1
692            if utilization <= self.optimalUtilization {
693                // rate = baseRate + (slope1 × utilization / optimalUtilization)
694                let utilizationFactor = utilization / self.optimalUtilization
695                let slope1Component = self.slope1 * utilizationFactor
696                return self.baseRate + slope1Component
697            } else {
698                // If utilization is above the optimal point, use slope2 for excess
699                // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization)
700                let excessUtilization = utilization - self.optimalUtilization
701                let maxExcess = FlowALPMath.one - self.optimalUtilization
702                let excessFactor = excessUtilization / maxExcess
703
704                // rate = baseRate + slope1 + (slope2 × excessFactor)
705                let slope2Component = self.slope2 * excessFactor
706                return self.baseRate + self.slope1 + slope2Component
707            }
708        }
709    }
710
711    /// TokenState
712    ///
713    /// The TokenState struct tracks values related to a single token Type within the Pool.
714    access(all) struct TokenState {
715
716        access(EImplementation) var tokenType : Type
717
718        /// The timestamp at which the TokenState was last updated
719        access(EImplementation) var lastUpdate: UFix64
720
721        /// The total credit balance for this token, in a specific Pool.
722        /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token).
723        /// In other words, it is the the sum of net deposits among positions which are net creditors in this token.
724        access(EImplementation) var totalCreditBalance: UFix128
725
726        /// The total debit balance for this token, in a specific Pool.
727        /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token).
728        /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token.
729        access(EImplementation) var totalDebitBalance: UFix128
730
731        /// The index of the credit interest for the related token.
732        ///
733        /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128
734        /// to maintain precision when converting between scaled and true balances and when compounding.
735        access(EImplementation) var creditInterestIndex: UFix128
736
737        /// The index of the debit interest for the related token.
738        ///
739        /// Interest indices are 18-decimal fixed-point values (see FlowALPMath) and are stored as UFix128
740        /// to maintain precision when converting between scaled and true balances and when compounding.
741        access(EImplementation) var debitInterestIndex: UFix128
742
743        /// The per-second interest rate for credit of the associated token.
744        ///
745        /// For example, if the per-second rate is 1%, this value is 0.01.
746        /// Stored as UFix128 to match index precision and avoid cumulative rounding during compounding.
747        access(EImplementation) var currentCreditRate: UFix128
748
749        /// The per-second interest rate for debit of the associated token.
750        ///
751        /// For example, if the per-second rate is 1%, this value is 0.01.
752        /// Stored as UFix128 for consistency with indices/rates math.
753        access(EImplementation) var currentDebitRate: UFix128
754
755        /// The interest curve implementation used to calculate interest rate
756        access(EImplementation) var interestCurve: {InterestCurve}
757
758        /// The annual insurance rate applied to total debit when computing credit interest (default 0.1%)
759        access(EImplementation) var insuranceRate: UFix64
760
761        /// Timestamp of the last insurance collection for this token.
762        access(EImplementation) var lastInsuranceCollectionTime: UFix64
763
764        /// Swapper used to convert this token to MOET for insurance collection.
765        access(EImplementation) var insuranceSwapper: {DeFiActions.Swapper}?
766
767        /// The stability fee rate to calculate stability (default 0.05, 5%).
768        access(EImplementation) var stabilityFeeRate: UFix64
769
770        /// Timestamp of the last stability collection for this token.
771        access(EImplementation) var lastStabilityFeeCollectionTime: UFix64
772
773        /// Per-position limit fraction of capacity (default 0.05 i.e., 5%)
774        access(EImplementation) var depositLimitFraction: UFix64
775
776        /// The rate at which depositCapacity can increase over time. This is a tokens per hour rate,
777        /// and should be applied to the depositCapacityCap once an hour.
778        access(EImplementation) var depositRate: UFix64
779
780        /// The timestamp of the last deposit capacity update
781        access(EImplementation) var lastDepositCapacityUpdate: UFix64
782
783        /// The limit on deposits of the related token
784        access(EImplementation) var depositCapacity: UFix64
785
786        /// The upper bound on total deposits of the related token,
787        /// limiting how much depositCapacity can reach
788        access(EImplementation) var depositCapacityCap: UFix64
789
790        /// Tracks per-user deposit usage for enforcing user deposit limits
791        /// Maps position ID -> usage amount (how much of each user's limit has been consumed for this token type)
792        access(EImplementation) var depositUsage: {UInt64: UFix64}
793
794        /// The minimum balance size for the related token T per position.
795        /// This minimum balance is denominated in units of token T.
796        /// Let this minimum balance be M. Then each position must have either:
797        /// - A balance of 0
798        /// - A credit balance greater than or equal to M
799        /// - A debit balance greater than or equal to M
800        access(EImplementation) var minimumTokenBalancePerPosition: UFix64
801
802        init(
803            tokenType: Type,
804            interestCurve: {InterestCurve},
805            depositRate: UFix64,
806            depositCapacityCap: UFix64
807        ) {
808            self.tokenType = tokenType
809            self.lastUpdate = getCurrentBlock().timestamp
810            self.totalCreditBalance = 0.0
811            self.totalDebitBalance = 0.0
812            self.creditInterestIndex = 1.0
813            self.debitInterestIndex = 1.0
814            self.currentCreditRate = 1.0
815            self.currentDebitRate = 1.0
816            self.interestCurve = interestCurve
817            self.insuranceRate = 0.0
818            self.lastInsuranceCollectionTime = getCurrentBlock().timestamp
819            self.insuranceSwapper = nil
820            self.stabilityFeeRate = 0.05
821            self.lastStabilityFeeCollectionTime = getCurrentBlock().timestamp
822            self.depositLimitFraction = 0.05
823            self.depositRate = depositRate
824            self.depositCapacity = depositCapacityCap
825            self.depositCapacityCap = depositCapacityCap
826            self.depositUsage = {}
827            self.lastDepositCapacityUpdate = getCurrentBlock().timestamp
828            self.minimumTokenBalancePerPosition = 1.0
829        }
830
831        /// Sets the insurance rate for this token state
832        access(EImplementation) fun setInsuranceRate(_ rate: UFix64) {
833            self.insuranceRate = rate
834        }
835
836        /// Sets the last insurance collection timestamp
837        access(EImplementation) fun setLastInsuranceCollectionTime(_ lastInsuranceCollectionTime: UFix64) {
838            self.lastInsuranceCollectionTime = lastInsuranceCollectionTime
839        }
840
841        /// Sets the swapper used for insurance collection (must swap from this token type to MOET)
842        access(EImplementation) fun setInsuranceSwapper(_ swapper: {DeFiActions.Swapper}?) {
843            if let swapper = swapper {
844                assert(swapper.inType() == self.tokenType, message: "Insurance swapper must accept \(self.tokenType.identifier), not \(swapper.inType().identifier)")
845                assert(swapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET")
846            }
847            self.insuranceSwapper = swapper
848        }
849
850        /// Sets the per-deposit limit fraction for this token state
851        access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) {
852            self.depositLimitFraction = frac
853        }
854
855        /// Sets the deposit rate for this token state after settling the old rate
856        /// Argument expressed astokens per hour
857        access(EImplementation) fun setDepositRate(_ hourlyRate: UFix64) {
858            // settle using old rate if for some reason too much time has passed without regeneration
859            self.regenerateDepositCapacity() 
860            self.depositRate = hourlyRate
861        }
862
863        /// Sets the deposit capacity cap for this token state
864        access(EImplementation) fun setDepositCapacityCap(_ cap: UFix64) {
865            self.depositCapacityCap = cap
866            // If current capacity exceeds the new cap, clamp it to the cap
867            if self.depositCapacity > cap {
868                self.depositCapacity = cap
869            }
870            // Reset the last update timestamp to prevent regeneration based on old timestamp
871            self.lastDepositCapacityUpdate = getCurrentBlock().timestamp
872        }
873
874        /// Sets the minimum token balance per position for this token state
875        access(EImplementation) fun setMinimumTokenBalancePerPosition(_ minimum: UFix64) {
876            self.minimumTokenBalancePerPosition = minimum
877        }
878
879        /// Sets the stability fee rate for this token state.
880        access(EImplementation) fun setStabilityFeeRate(_ rate: UFix64) {
881            self.stabilityFeeRate = rate
882        }
883        
884        /// Sets the last stability fee collection timestamp for this token state.
885        access(EImplementation) fun setLastStabilityFeeCollectionTime(_ lastStabilityFeeCollectionTime: UFix64) {
886            self.lastStabilityFeeCollectionTime = lastStabilityFeeCollectionTime
887        }
888
889        /// Calculates the per-user deposit limit cap based on depositLimitFraction * depositCapacityCap
890        access(EImplementation) fun getUserDepositLimitCap(): UFix64 {
891            return self.depositLimitFraction * self.depositCapacityCap
892        }
893
894        /// Decreases deposit capacity by the specified amount and tracks per-user deposit usage
895        /// (used when deposits are made)
896        access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64) {
897            assert(
898                amount <= self.depositCapacity,
899                message: "cannot consume more than available deposit capacity"
900            )
901            self.depositCapacity = self.depositCapacity - amount
902            
903            // Track per-user deposit usage for the accepted amount
904            let currentUserUsage = self.depositUsage[pid] ?? 0.0
905            self.depositUsage[pid] = currentUserUsage + amount
906
907            emit DepositCapacityConsumed(
908                tokenType: self.tokenType,
909                pid: pid,
910                amount: amount,
911                remainingCapacity: self.depositCapacity
912            )
913        }
914
915        /// Sets deposit capacity (used for time-based regeneration)
916        access(EImplementation) fun setDepositCapacity(_ capacity: UFix64) {
917            self.depositCapacity = capacity
918        }
919
920        /// Sets the interest curve for this token state
921        /// After updating the curve, also update the interest rates to reflect the new curve
922        access(EImplementation) fun setInterestCurve(_ curve: {InterestCurve}) {
923            self.interestCurve = curve
924            // Update rates immediately to reflect the new curve
925            self.updateInterestRates()
926        }
927
928        /// Balance update helpers used by core accounting.
929        /// All balance changes automatically trigger updateForUtilizationChange()
930        /// which recalculates interest rates based on the new utilization ratio.
931        /// This ensures rates always reflect the current state of the pool
932        /// without requiring manual rate update calls.
933        access(EImplementation) fun increaseCreditBalance(by amount: UFix128) {
934            self.totalCreditBalance = self.totalCreditBalance + amount
935            self.updateForUtilizationChange()
936        }
937
938        access(EImplementation) fun decreaseCreditBalance(by amount: UFix128) {
939            if amount >= self.totalCreditBalance {
940                self.totalCreditBalance = 0.0
941            } else {
942                self.totalCreditBalance = self.totalCreditBalance - amount
943            }
944            self.updateForUtilizationChange()
945        }
946
947        access(EImplementation) fun increaseDebitBalance(by amount: UFix128) {
948            self.totalDebitBalance = self.totalDebitBalance + amount
949            self.updateForUtilizationChange()
950        }
951
952        access(EImplementation) fun decreaseDebitBalance(by amount: UFix128) {
953            if amount >= self.totalDebitBalance {
954                self.totalDebitBalance = 0.0
955            } else {
956                self.totalDebitBalance = self.totalDebitBalance - amount
957            }
958            self.updateForUtilizationChange()
959        }
960
961        // Updates the credit and debit interest index for this token, accounting for time since the last update.
962        access(EImplementation) fun updateInterestIndices() {
963            let currentTime = getCurrentBlock().timestamp
964            let dt = currentTime - self.lastUpdate
965
966            // No time elapsed or already at cap → nothing to do
967            if dt <= 0.0 {
968                return
969            }
970
971            // Update interest indices (dt > 0 ensures sensible compounding)
972            self.creditInterestIndex = FlowALPv0.compoundInterestIndex(
973                oldIndex: self.creditInterestIndex,
974                perSecondRate: self.currentCreditRate,
975                elapsedSeconds: dt
976            )
977            self.debitInterestIndex = FlowALPv0.compoundInterestIndex(
978                oldIndex: self.debitInterestIndex,
979                perSecondRate: self.currentDebitRate,
980                elapsedSeconds: dt
981            )
982
983            // Record the moment we accounted for
984            self.lastUpdate = currentTime
985        }
986
987        /// Regenerates deposit capacity over time based on depositRate
988        /// Note: dt should be calculated before updateInterestIndices() updates lastUpdate
989        /// When capacity regenerates, all user deposit usage is reset for this token type
990        access(EImplementation) fun regenerateDepositCapacity() {
991            let currentTime = getCurrentBlock().timestamp
992            let dt = currentTime - self.lastDepositCapacityUpdate
993            let hourInSeconds = 3600.0
994            if dt >= hourInSeconds { // 1 hour
995                let multiplier = dt / hourInSeconds
996                let oldCap = self.depositCapacityCap
997                let newDepositCapacityCap = self.depositRate * multiplier + self.depositCapacityCap
998
999                self.depositCapacityCap = newDepositCapacityCap
1000
1001                // Set the deposit capacity to the new deposit capacity cap, i.e. regenerate the capacity
1002                self.setDepositCapacity(newDepositCapacityCap)
1003                
1004                // Regenerate user usage for this token type as well
1005                self.depositUsage = {}
1006
1007                self.lastDepositCapacityUpdate = currentTime
1008
1009                emit DepositCapacityRegenerated(
1010                    tokenType: self.tokenType,
1011                    oldCapacityCap: oldCap,
1012                    newCapacityCap: newDepositCapacityCap
1013                )
1014            }
1015        }
1016
1017        // Deposit limit function
1018        // Rationale: cap per-deposit size to a fraction of the time-based
1019        // depositCapacity so a single large deposit cannot monopolize capacity.
1020        // Excess is queued and drained in chunks (see asyncUpdatePosition),
1021        // enabling fair throughput across many deposits in a block. The 5%
1022        // fraction is conservative and can be tuned by protocol parameters.
1023        access(EImplementation) fun depositLimit(): UFix64 {
1024            return self.depositCapacity * self.depositLimitFraction
1025        }
1026
1027
1028        access(EImplementation) fun updateForTimeChange() {
1029            self.updateInterestIndices()
1030            self.regenerateDepositCapacity()
1031        }
1032
1033        /// Called after any action that changes utilization (deposits, withdrawals, borrows, repays).
1034        /// Recalculates interest rates based on the new credit/debit balance ratio.
1035        access(EImplementation) fun updateForUtilizationChange() {
1036            self.updateInterestRates()
1037        }
1038
1039        access(EImplementation) fun updateInterestRates() {
1040            let debitRate = self.interestCurve.interestRate(
1041                creditBalance: self.totalCreditBalance,
1042                debitBalance: self.totalDebitBalance
1043            )
1044            let insuranceRate = UFix128(self.insuranceRate)
1045            let stabilityFeeRate = UFix128(self.stabilityFeeRate)
1046
1047            var creditRate: UFix128 = 0.0
1048            // Total protocol cut as a percentage of debit interest income
1049            let protocolFeeRate = insuranceRate + stabilityFeeRate
1050
1051            // Two calculation paths based on curve type:
1052            // 1. FixedRateInterestCurve: simple spread model (creditRate = debitRate * (1 - protocolFeeRate))
1053            //    Used for stable assets like MOET where rates are governance-controlled
1054            // 2. KinkInterestCurve (and others): reserve factor model
1055            //    Insurance and stability are percentages of interest income, not a fixed spread
1056            // TODO(jord): seems like InterestCurve abstraction could be improved if we need to check specific types here.
1057            if self.interestCurve.getType() == Type<FlowALPv0.FixedRateInterestCurve>() {
1058                // FixedRate path: creditRate = debitRate * (1 - protocolFeeRate))
1059                // This provides a fixed, predictable spread between borrower and lender rates
1060                creditRate = debitRate * (1.0 - protocolFeeRate) 
1061            } else {
1062                // KinkCurve path (and any other curves): reserve factor model
1063                // protocolFeeAmount = debitIncome * protocolFeeRate (percentage of income)
1064                // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance
1065                let debitIncome = self.totalDebitBalance * debitRate
1066                let protocolFeeAmount = debitIncome * protocolFeeRate
1067
1068                if self.totalCreditBalance > 0.0 {
1069                    creditRate = (debitIncome - protocolFeeAmount) / self.totalCreditBalance
1070                }
1071            }
1072
1073            self.currentCreditRate = FlowALPv0.perSecondInterestRate(yearlyRate: creditRate)
1074            self.currentDebitRate = FlowALPv0.perSecondInterestRate(yearlyRate: debitRate)
1075        }
1076
1077        /// Collects insurance by withdrawing from reserves and swapping to MOET.
1078        /// The insurance amount is calculated based on the insurance rate applied to the total debit balance over the time elapsed.
1079        /// This should be called periodically (e.g., when updateInterestRates is called) to accumulate the insurance fund.
1080        /// CAUTION: This function will panic if no insuranceSwapper is provided.
1081        ///
1082        /// @param reserveVault: The reserve vault for this token type to withdraw insurance from
1083        /// @param oraclePrice: The current price for this token according to the Oracle, denominated in $
1084        /// @param maxDeviationBps: The max deviation between oracle/dex prices (see Pool.dexOracleDeviationBps)
1085        /// @return: A MOET vault containing the collected insurance funds, or nil if no collection occurred
1086        access(EImplementation) fun collectInsurance(
1087            reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault},
1088            oraclePrice: UFix64,
1089            maxDeviationBps: UInt16
1090        ): @MOET.Vault? {
1091            let currentTime = getCurrentBlock().timestamp
1092
1093            // If insuranceRate is 0.0 configured, skip collection but update the last insurance collection time
1094            if self.insuranceRate == 0.0 {
1095                self.setLastInsuranceCollectionTime(currentTime)
1096                return nil
1097            }
1098
1099            // Calculate accrued insurance amount based on time elapsed since last collection
1100            let timeElapsed = currentTime - self.lastInsuranceCollectionTime
1101            
1102            // If no time has elapsed, nothing to collect
1103            if timeElapsed <= 0.0 {
1104                return nil
1105            }
1106
1107            // Insurance amount is a percentage of debit income
1108            // debitIncome = debitBalance * (curentDebitRate ^ time_elapsed - 1.0)
1109            let debitIncome = self.totalDebitBalance * (FlowALPMath.powUFix128(self.currentDebitRate, timeElapsed) - 1.0)
1110            let insuranceAmount = debitIncome * UFix128(self.insuranceRate)
1111            let insuranceAmountUFix64 = FlowALPMath.toUFix64RoundDown(insuranceAmount)
1112
1113            // If calculated amount is zero, skip collection but update timestamp
1114            if insuranceAmountUFix64 == 0.0 {
1115                self.setLastInsuranceCollectionTime(currentTime)
1116                return nil
1117            }
1118            
1119            // Check if we have enough balance in reserves
1120            if reserveVault.balance == 0.0 {
1121                self.setLastInsuranceCollectionTime(currentTime)
1122                return nil
1123            }
1124
1125            // Withdraw insurance amount from reserves (use available balance if less than calculated)
1126            let amountToCollect = insuranceAmountUFix64 > reserveVault.balance ? reserveVault.balance : insuranceAmountUFix64
1127            var insuranceVault <- reserveVault.withdraw(amount: amountToCollect)
1128
1129			let insuranceSwapper = self.insuranceSwapper ?? panic("missing insurance swapper")
1130
1131            // Validate swapper input and output types (input and output types are already validated when swapper is set)
1132            assert(insuranceSwapper.inType() == reserveVault.getType(), message: "Insurance swapper input type must be same as reserveVault")
1133            assert(insuranceSwapper.outType() == Type<@MOET.Vault>(), message: "Insurance swapper must output MOET")
1134
1135            // Get quote and perform swap
1136            let quote = insuranceSwapper.quoteOut(forProvided: amountToCollect, reverse: false)
1137            let dexPrice = quote.outAmount / quote.inAmount
1138            assert(
1139                FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: dexPrice, oraclePrice: oraclePrice, maxDeviationBps: maxDeviationBps),
1140                message: "DEX/oracle price deviation too large. Dex price: \(dexPrice), Oracle price: \(oraclePrice)")
1141            var moetVault <- insuranceSwapper.swap(quote: quote, inVault: <-insuranceVault) as! @MOET.Vault
1142
1143            // Update last collection time
1144            self.setLastInsuranceCollectionTime(currentTime)
1145
1146            // Return the MOET vault for the caller to deposit
1147            return <-moetVault
1148        }
1149
1150        /// Collects stability funds by withdrawing from reserves.
1151        /// The stability amount is calculated based on the stability rate applied to the total debit balance over the time elapsed.
1152        /// This should be called periodically (e.g., when updateInterestRates is called) to accumulate the stability fund.
1153        ///
1154        /// @param reserveVault: The reserve vault for this token type to withdraw stability amount from
1155        /// @return: A token type vault containing the collected stability funds, or nil if no collection occurred
1156        access(EImplementation) fun collectStability(
1157            reserveVault: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}
1158        ): @{FungibleToken.Vault}? {
1159            let currentTime = getCurrentBlock().timestamp
1160
1161            // If stabilityFeeRate is 0.0 configured, skip collection but update the last stability collection time
1162            if self.stabilityFeeRate == 0.0 {
1163                self.setLastStabilityFeeCollectionTime(currentTime)
1164                return nil
1165            }
1166
1167            // Calculate accrued stability amount based on time elapsed since last collection
1168            let timeElapsed = currentTime - self.lastStabilityFeeCollectionTime
1169
1170            // If no time has elapsed, nothing to collect
1171            if timeElapsed <= 0.0 {
1172                return nil
1173            }
1174
1175            let stabilityFeeRate = UFix128(self.stabilityFeeRate)
1176
1177            // Calculate stability amount: is a percentage of debit income
1178            // debitIncome = debitBalance * (curentDebitRate ^ time_elapsed - 1.0)
1179            let interestIncome = self.totalDebitBalance * (FlowALPMath.powUFix128(self.currentDebitRate, timeElapsed) - 1.0)
1180            let stabilityAmount = interestIncome * stabilityFeeRate
1181            let stabilityAmountUFix64 = FlowALPMath.toUFix64RoundDown(stabilityAmount)
1182
1183            // If calculated amount is zero or negative, skip collection but update timestamp
1184            if stabilityAmountUFix64 == 0.0 {
1185                self.setLastStabilityFeeCollectionTime(currentTime)
1186                return nil
1187            }
1188
1189            // Check if we have enough balance in reserves
1190            if reserveVault.balance == 0.0 {
1191                self.setLastStabilityFeeCollectionTime(currentTime)
1192                return nil
1193            }
1194
1195            let reserveVaultBalance = reserveVault.balance
1196            // Withdraw stability amount from reserves (use available balance if less than calculated)
1197            let amountToCollect = stabilityAmountUFix64 > reserveVaultBalance ? reserveVaultBalance : stabilityAmountUFix64
1198            let stabilityVault <- reserveVault.withdraw(amount: amountToCollect)
1199
1200            // Update last collection time
1201            self.setLastStabilityFeeCollectionTime(currentTime)
1202
1203            // Return the vault for the caller to deposit
1204            return <-stabilityVault
1205        }
1206    }
1207
1208    /// Risk parameters for a token used in effective collateral/debt computations.
1209    /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token.
1210    /// The size of this discount indicates a subjective assessment of risk for the token.
1211    /// The difference between the effective value and "true" value represents the safety buffer available to prevent loss.
1212    /// - collateralFactor: the factor used to derive effective collateral
1213    /// - borrowFactor: the factor used to derive effective debt
1214    access(all) struct RiskParams {
1215        /// The factor (Fc) used to determine effective collateral, in the range [0, 1]
1216        /// See FlowALPv0.effectiveCollateral for additional detail.
1217        access(all) let collateralFactor: UFix128
1218        /// The factor (Fd) used to determine effective debt, in the range [0, 1]
1219        /// See FlowALPv0.effectiveDebt for additional detail.
1220        access(all) let borrowFactor: UFix128
1221
1222        init(
1223            collateralFactor: UFix128,
1224            borrowFactor: UFix128,
1225        ) {
1226            pre {
1227                collateralFactor <= 1.0: "collateral factor must be <=1"
1228                borrowFactor <= 1.0: "borrow factor must be <=1"
1229            }
1230            self.collateralFactor = collateralFactor
1231            self.borrowFactor = borrowFactor
1232        }
1233    }
1234
1235    /// Immutable snapshot of token-level data required for pure math operations
1236    access(all) struct TokenSnapshot {
1237        access(all) let price: UFix128
1238        access(all) let creditIndex: UFix128
1239        access(all) let debitIndex: UFix128
1240        access(all) let risk: RiskParams
1241
1242        init(
1243            price: UFix128,
1244            credit: UFix128,
1245            debit: UFix128,
1246            risk: RiskParams
1247        ) {
1248            self.price = price
1249            self.creditIndex = credit
1250            self.debitIndex = debit
1251            self.risk = risk
1252        }
1253
1254        /// Returns the effective debt (denominated in $) for the given debit balance of this snapshot's token.
1255        /// See FlowALPv0.effectiveDebt for additional details.
1256        access(all) view fun effectiveDebt(debitBalance: UFix128): UFix128 {
1257            return FlowALPv0.effectiveDebt(debit: debitBalance, price: self.price, borrowFactor: self.risk.borrowFactor)
1258        }
1259
1260        /// Returns the effective collateral (denominated in $) for the given credit balance of this snapshot's token.
1261        /// See FlowALPv0.effectiveCollateral for additional details.
1262        access(all) view fun effectiveCollateral(creditBalance: UFix128): UFix128 {
1263            return FlowALPv0.effectiveCollateral(credit: creditBalance, price: self.price, collateralFactor: self.risk.collateralFactor)
1264        }
1265    }
1266
1267    /// Copy-only representation of a position used by pure math (no storage refs)
1268    access(all) struct PositionView {
1269        /// Set of all non-zero balances in the position.
1270        /// If the position does not have a balance for a supported token, no entry for that token exists in this map.
1271        access(all) let balances: {Type: InternalBalance}
1272        /// Set of all token snapshots for which this position has a non-zero balance.
1273        /// If the position does not have a balance for a supported token, no entry for that token exists in this map.
1274        access(all) let snapshots: {Type: TokenSnapshot}
1275        access(all) let defaultToken: Type
1276        access(all) let minHealth: UFix128
1277        access(all) let maxHealth: UFix128
1278
1279        init(
1280            balances: {Type: InternalBalance},
1281            snapshots: {Type: TokenSnapshot},
1282            defaultToken: Type,
1283            min: UFix128,
1284            max: UFix128
1285        ) {
1286            self.balances = balances
1287            self.snapshots = snapshots
1288            self.defaultToken = defaultToken
1289            self.minHealth = min
1290            self.maxHealth = max
1291        }
1292
1293        /// Returns the true balance of the given token in this position, accounting for interest.
1294        /// Returns balance 0.0 if the position has no balance stored for the given token.
1295        access(all) view fun trueBalance(ofToken: Type): UFix128 {
1296            if let balance = self.balances[ofToken] {
1297                if let tokenSnapshot = self.snapshots[ofToken] {
1298                    switch balance.direction {
1299                    case BalanceDirection.Debit:
1300                        return FlowALPv0.scaledBalanceToTrueBalance(
1301                            balance.scaledBalance, interestIndex: tokenSnapshot.debitIndex)
1302                    case BalanceDirection.Credit:
1303                        return FlowALPv0.scaledBalanceToTrueBalance(
1304                            balance.scaledBalance, interestIndex: tokenSnapshot.creditIndex)
1305                    }
1306                    panic("unreachable")
1307                }
1308            } 
1309            // If the token doesn't exist in the position, the balance is 0
1310            return 0.0
1311        }
1312    }
1313
1314    // PURE HELPERS -------------------------------------------------------------
1315
1316    /// Returns the effective collateral (denominated in $) for the given credit balance of some token T.
1317    /// Effective Collateral is defined:
1318    ///   Ce = (Nc)(Pc)(Fc)
1319    /// Where:
1320    /// Ce = Effective Collateral 
1321    /// Nc = Number of Collateral Tokens
1322    /// Pc = Collateral Token Price
1323    /// Fc = Collateral Factor
1324    ///
1325    /// @param credit           The credit balance of the position for token T.
1326    /// @param price            The price of token T ($/T).
1327    /// @param collateralFactor The collateral factor for token T (see RiskParams for details).
1328    access(all) view fun effectiveCollateral(credit: UFix128, price: UFix128, collateralFactor: UFix128): UFix128 {
1329        return (credit * price) * collateralFactor
1330    }
1331
1332    /// Returns the effective debt (denominated in $) for the given debit balance of some token T.
1333    /// Effective Debt is defined:
1334    ///   De = (Nd)(Pd)(Fd)
1335    /// Where:
1336    /// De = Effective Debt 
1337    /// Nd = Number of Debt Tokens
1338    /// Pd = Debt Token Price
1339    /// Fd = Borrow Factor
1340    ///
1341    /// @param debit       The debit balance of the position for token T.
1342    /// @param price       The price of token T ($/T).
1343    /// @param borowFactor The borrow factor for token T (see RiskParams for details).
1344    access(all) view fun effectiveDebt(debit: UFix128, price: UFix128, borrowFactor: UFix128): UFix128 {
1345        return (debit * price) / borrowFactor
1346    }
1347
1348    /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0)
1349    // TODO: return BalanceSheet, this seems like a dupe of _getUpdatedBalanceSheet
1350    access(all) view fun healthFactor(view: PositionView): UFix128 {
1351        // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet
1352        // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations.
1353        var effectiveCollateralTotal: UFix128 = 0.0
1354        var effectiveDebtTotal: UFix128 = 0.0
1355
1356        for tokenType in view.balances.keys {
1357            let balance = view.balances[tokenType]!
1358            let snap = view.snapshots[tokenType]!
1359
1360            switch balance.direction {
1361                case BalanceDirection.Credit:
1362                    let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1363                        balance.scaledBalance,
1364                        interestIndex: snap.creditIndex
1365                    )
1366                    effectiveCollateralTotal = effectiveCollateralTotal
1367                        + snap.effectiveCollateral(creditBalance: trueBalance)
1368
1369                case BalanceDirection.Debit:
1370                    let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1371                        balance.scaledBalance,
1372                        interestIndex: snap.debitIndex
1373                    )
1374                    effectiveDebtTotal = effectiveDebtTotal
1375                        + snap.effectiveDebt(debitBalance: trueBalance)
1376            }
1377        }
1378        return FlowALPv0.healthComputation(
1379            effectiveCollateral: effectiveCollateralTotal,
1380            effectiveDebt: effectiveDebtTotal
1381        )
1382    }
1383
1384    /// Amount of `withdrawSnap` token that can be withdrawn while staying ≥ targetHealth
1385    access(all) view fun maxWithdraw(
1386        view: PositionView,
1387        withdrawSnap: TokenSnapshot,
1388        withdrawBal: InternalBalance?,
1389        targetHealth: UFix128
1390    ): UFix128 {
1391        let preHealth = FlowALPv0.healthFactor(view: view)
1392        if preHealth <= targetHealth {
1393            return 0.0
1394        }
1395
1396        // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet
1397        // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations.
1398        var effectiveCollateralTotal: UFix128 = 0.0
1399        var effectiveDebtTotal: UFix128 = 0.0
1400
1401        for tokenType in view.balances.keys {
1402            let balance = view.balances[tokenType]!
1403            let snap = view.snapshots[tokenType]!
1404
1405            switch balance.direction {
1406                case BalanceDirection.Credit:
1407                    let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1408                        balance.scaledBalance,
1409                        interestIndex: snap.creditIndex
1410                    )
1411                    effectiveCollateralTotal = effectiveCollateralTotal
1412                        + snap.effectiveCollateral(creditBalance: trueBalance)
1413
1414                case BalanceDirection.Debit:
1415                    let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1416                        balance.scaledBalance,
1417                        interestIndex: snap.debitIndex
1418                    )
1419                    effectiveDebtTotal = effectiveDebtTotal
1420                        + snap.effectiveDebt(debitBalance: trueBalance)
1421            }
1422        }
1423
1424        let collateralFactor = withdrawSnap.risk.collateralFactor
1425        let borrowFactor = withdrawSnap.risk.borrowFactor
1426
1427        if withdrawBal == nil || withdrawBal!.direction == BalanceDirection.Debit {
1428            // withdrawing increases debt
1429            let numerator = effectiveCollateralTotal
1430            let denominatorTarget = numerator / targetHealth
1431            let deltaDebt = denominatorTarget > effectiveDebtTotal
1432                ? denominatorTarget - effectiveDebtTotal
1433                : 0.0 as UFix128
1434            return (deltaDebt * borrowFactor) / withdrawSnap.price
1435        } else {
1436            // withdrawing reduces collateral
1437            let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1438                withdrawBal!.scaledBalance,
1439                interestIndex: withdrawSnap.creditIndex
1440            )
1441            let maxPossible = trueBalance
1442            let requiredCollateral = effectiveDebtTotal * targetHealth
1443            if effectiveCollateralTotal <= requiredCollateral {
1444                return 0.0
1445            }
1446            let deltaCollateralEffective = effectiveCollateralTotal - requiredCollateral
1447            let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.price
1448            return deltaTokens > maxPossible ? maxPossible : deltaTokens
1449        }
1450    }
1451
1452    /// Pool
1453    ///
1454    /// A Pool is the primary logic for protocol operations. It contains the global state of all positions,
1455    /// credit and debit balances for each supported token type, and reserves as they are deposited to positions.
1456    access(all) resource Pool {
1457
1458        /// Enable or disable verbose contract logging for debugging.
1459        access(self) var debugLogging: Bool
1460
1461        /// Global state for tracking each token
1462        access(self) var globalLedger: {Type: TokenState}
1463
1464        /// Individual user positions
1465        access(self) var positions: @{UInt64: InternalPosition}
1466
1467        /// The actual reserves of each token
1468        access(self) var reserves: @{Type: {FungibleToken.Vault}}
1469
1470        /// The insurance fund vault storing MOET tokens collected from insurance rates
1471        access(self) var insuranceFund: @MOET.Vault
1472
1473        /// Auto-incrementing position identifier counter
1474        access(self) var nextPositionID: UInt64
1475
1476        /// The default token type used as the "unit of account" for the pool.
1477        access(self) let defaultToken: Type
1478
1479        /// A price oracle that will return the price of each token in terms of the default token.
1480        access(self) var priceOracle: {DeFiActions.PriceOracle}
1481
1482        /// Together with borrowFactor, collateralFactor determines borrowing limits for each token.
1483        ///
1484        /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle)
1485        /// is multiplied by the collateral factor.
1486        ///
1487        /// The total "effective collateral" for a position is the value of each token deposited to the position
1488        /// multiplied by its collateral factor.
1489        access(self) var collateralFactor: {Type: UFix64}
1490
1491        /// Together with collateralFactor, borrowFactor determines borrowing limits for each token.
1492        ///
1493        /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a
1494        /// percentage between 0.0 and 1.0
1495        access(self) var borrowFactor: {Type: UFix64}
1496
1497        /// The count of positions to update per asynchronous update
1498        access(self) var positionsProcessedPerCallback: UInt64
1499
1500        /// The stability fund vaults storing tokens collected from stability fee rates.
1501        access(self) var stabilityFunds: @{Type: {FungibleToken.Vault}}
1502
1503        /// Position update queue to be processed as an asynchronous update
1504        access(EImplementation) var positionsNeedingUpdates: [UInt64]
1505
1506        /// Liquidation target health and controls (global)
1507
1508        /// The target health factor when liquidating a position, which limits how much collateral can be liquidated.
1509        /// After a liquidation, the position's health factor must be less than or equal to this target value.
1510        access(self) var liquidationTargetHF: UFix128
1511
1512        /// Whether the pool is currently paused, which prevents all user actions from occurring.
1513        /// The pool can be paused by the governance committee to protect user and protocol safety.
1514        access(self) var paused: Bool
1515        /// Period (s) following unpause in which liquidations are still not allowed
1516        access(self) var warmupSec: UInt64
1517        /// Time this pool most recently was unpaused
1518        access(self) var lastUnpausedAt: UInt64?
1519
1520        /// A trusted DEX (or set of DEXes) used by FlowALPv0 as a pricing oracle and trading counterparty for liquidations.
1521        /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens.
1522        /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: 
1523        ///   (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j
1524        ///
1525        /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers.
1526        /// It relies directly on the Swapper's returned by the configured SwapperProvider.
1527        access(self) var dex: {DeFiActions.SwapperProvider}
1528
1529        /// Max allowed deviation in basis points between DEX-implied price and oracle price.
1530        access(self) var dexOracleDeviationBps: UInt16
1531
1532        /// Reentrancy guards keyed by position id.
1533        /// When a position is locked, it means an operation on the position is in progress.
1534        /// While a position is locked, no new operation can begin on the locked position.
1535        /// All positions must be unlocked at the end of each transaction.
1536        /// A locked position is indicated by the presence of an entry {pid: True} in the map.
1537        /// An unlocked position is indicated by the lack of entry for the pid in the map.
1538        access(self) var positionLock: {UInt64: Bool}
1539
1540        init(
1541        	defaultToken: Type,
1542        	priceOracle: {DeFiActions.PriceOracle},
1543        	dex: {DeFiActions.SwapperProvider}
1544        ) {
1545            pre {
1546                priceOracle.unitOfAccount() == defaultToken:
1547                    "Price oracle must return prices in terms of the default token"
1548            }
1549
1550            self.debugLogging = false
1551            self.globalLedger = {
1552                defaultToken: TokenState(
1553                    tokenType: defaultToken,
1554                    interestCurve: FixedRateInterestCurve(yearlyRate: 0.0),
1555                    depositRate: 1_000_000.0,        // Default: no rate limiting for default token
1556                    depositCapacityCap: 1_000_000.0  // Default: high capacity cap
1557                )
1558            }
1559            self.positions <- {}
1560            self.reserves <- {}
1561            self.insuranceFund <- MOET.createEmptyVault(vaultType: Type<@MOET.Vault>())
1562            self.stabilityFunds <- {}
1563            self.defaultToken = defaultToken
1564            self.priceOracle = priceOracle
1565            self.collateralFactor = {defaultToken: 1.0}
1566            self.borrowFactor = {defaultToken: 1.0}
1567            self.nextPositionID = 0
1568            self.positionsNeedingUpdates = []
1569            self.positionsProcessedPerCallback = 100
1570            self.liquidationTargetHF = 1.05
1571            self.paused = false
1572            self.warmupSec = 300
1573            self.lastUnpausedAt = nil
1574            self.dex = dex
1575            self.dexOracleDeviationBps = 300 // 3% default
1576            self.positionLock = {}
1577
1578            // The pool starts with an empty reserves map.
1579            // Vaults will be created when tokens are first deposited.
1580        }
1581
1582        /// Marks the position as locked. Panics if the position is already locked.
1583        access(self) fun _lockPosition(_ pid: UInt64) {
1584            // If key absent => unlocked
1585            let locked = self.positionLock[pid] ?? false
1586            assert(!locked, message: "Reentrancy: position \(pid) is locked")
1587            self.positionLock[pid] = true
1588        }
1589
1590        /// Marks the position as unlocked. No-op if the position is already unlocked.
1591        access(self) fun _unlockPosition(_ pid: UInt64) {
1592            // Always unlock (even if missing)
1593            self.positionLock.remove(key: pid)
1594        }
1595
1596        /// Locks a position. Used by Position resources to acquire the position lock.
1597        access(EPosition) fun lockPosition(_ pid: UInt64) {
1598            self._lockPosition(pid)
1599        }
1600
1601        /// Unlocks a position. Used by Position resources to release the position lock.
1602        access(EPosition) fun unlockPosition(_ pid: UInt64) {
1603            self._unlockPosition(pid)
1604        }
1605
1606        ///////////////
1607        // GETTERS
1608        ///////////////
1609
1610        /// Returns whether sensitive pool actions are paused by governance,
1611        /// including withdrawals, deposits, and liquidations
1612        access(all) view fun isPaused(): Bool {
1613            return self.paused
1614        }
1615
1616        /// Returns whether withdrawals and liquidations are paused.
1617        /// Both have a warmup period after a global pause is ended, to allow users time to improve position health and avoid liquidation.
1618        /// The warmup period provides an opportunity for users to deposit to unhealthy positions before liquidations start,
1619        /// and also disallows withdrawing while liquidations are disabled, because liquidations can be needed to satisfy withdrawal requests.
1620        access(all) view fun isPausedOrWarmup(): Bool {
1621            if self.paused {
1622                return true
1623            }
1624            if let lastUnpausedAt = self.lastUnpausedAt {
1625                let now = UInt64(getCurrentBlock().timestamp)
1626                return now < lastUnpausedAt + self.warmupSec
1627            }
1628            return false
1629        }
1630
1631        /// Returns an array of the supported token Types
1632        access(all) view fun getSupportedTokens(): [Type] {
1633            return self.globalLedger.keys
1634        }
1635
1636        /// Returns whether a given token Type is supported or not
1637        access(all) view fun isTokenSupported(tokenType: Type): Bool {
1638            return self.globalLedger[tokenType] != nil
1639        } 
1640
1641        /// Returns the current balance of the stability fund for a given token type.
1642        /// Returns nil if the token type is not supported.
1643        access(all) view fun getStabilityFundBalance(tokenType: Type): UFix64? {
1644            if let fundRef = &self.stabilityFunds[tokenType] as &{FungibleToken.Vault}? {
1645                return fundRef.balance
1646            }
1647            
1648            return nil
1649        }
1650
1651        /// Returns the stability fee rate for a given token type.
1652        /// Returns nil if the token type is not supported.
1653        access(all) view fun getStabilityFeeRate(tokenType: Type): UFix64? {
1654            if let tokenState = self.globalLedger[tokenType] {
1655                return tokenState.stabilityFeeRate
1656            }
1657
1658            return nil
1659        }
1660
1661        /// Returns the timestamp of the last stability collection for a given token type.
1662        /// Returns nil if the token type is not supported.
1663        access(all) view fun getLastStabilityCollectionTime(tokenType: Type): UFix64? {
1664            if let tokenState = self.globalLedger[tokenType] {
1665                return tokenState.lastStabilityFeeCollectionTime
1666            }
1667
1668            return nil
1669        }
1670
1671        /// Returns whether an insurance swapper is configured for a given token type
1672        access(all) view fun isInsuranceSwapperConfigured(tokenType: Type): Bool {
1673            if let tokenState = self.globalLedger[tokenType] {
1674                return tokenState.insuranceSwapper != nil
1675            }
1676            return false
1677        }
1678
1679        /// Returns the timestamp of the last insurance collection for a given token type
1680        /// Returns nil if the token type is not supported
1681        access(all) view fun getLastInsuranceCollectionTime(tokenType: Type): UFix64? {
1682            if let tokenState = self.globalLedger[tokenType] {
1683                return tokenState.lastInsuranceCollectionTime
1684            }
1685            return nil
1686        }
1687
1688        /// Returns current pause parameters
1689        access(all) fun getPauseParams(): FlowALPv0.PauseParamsView {
1690            return FlowALPv0.PauseParamsView(
1691                paused: self.paused,
1692                warmupSec: self.warmupSec,
1693                lastUnpausedAt: self.lastUnpausedAt,
1694            )
1695        }
1696
1697        /// Returns current liquidation parameters
1698        access(all) fun getLiquidationParams(): FlowALPv0.LiquidationParamsView {
1699            return FlowALPv0.LiquidationParamsView(
1700                targetHF: self.liquidationTargetHF,
1701                triggerHF: 1.0,
1702            )
1703        }
1704
1705        /// Returns Oracle-DEX guards and allowlists for frontends/keepers
1706        access(all) fun getDexLiquidationConfig(): {String: AnyStruct} {
1707            return {
1708                "dexOracleDeviationBps": self.dexOracleDeviationBps
1709            }
1710        }
1711
1712        /// Returns true if the position is under the global liquidation trigger (health < 1.0)
1713        access(all) fun isLiquidatable(pid: UInt64): Bool {
1714            let health = self.positionHealth(pid: pid)
1715            return health < 1.0
1716        }
1717
1718        /// Returns the current reserve balance for the specified token type.
1719        access(all) view fun reserveBalance(type: Type): UFix64 {
1720            let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?
1721            return vaultRef?.balance ?? 0.0
1722        }
1723
1724        /// Returns the balance of the MOET insurance fund
1725        access(all) view fun insuranceFundBalance(): UFix64 {
1726            return self.insuranceFund.balance
1727        }
1728
1729        /// Returns the insurance rate for a given token type
1730        access(all) view fun getInsuranceRate(tokenType: Type): UFix64? {
1731            if let tokenState = self.globalLedger[tokenType] {
1732                return tokenState.insuranceRate
1733            }
1734            
1735            return nil
1736        }
1737
1738        /// Returns a reference to the reserve vault for the given type, if the token type is supported.
1739        /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created.
1740        access(self) fun _borrowOrCreateReserveVault(type: Type): &{FungibleToken.Vault} {
1741            pre {
1742                self.isTokenSupported(tokenType: type): "Cannot borrow reserve for unsupported token \(type.identifier)"
1743            }
1744            if self.reserves[type] == nil {
1745                self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type)
1746            }
1747            let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?
1748            return vaultRef!
1749        }
1750
1751        /// Returns a position's balance available for withdrawal of a given Vault type.
1752        /// Phase 0 refactor: compute via pure helpers using a PositionView and TokenSnapshot for the base path.
1753        /// When `pullFromTopUpSource` is true and a topUpSource exists, preserve deposit-assisted semantics.
1754        access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 {
1755            if self.debugLogging {
1756                log("    [CONTRACT] availableBalance(pid: \(pid), type: \(type.contractName!), pullFromTopUpSource: \(pullFromTopUpSource))")
1757            }
1758            let position = self._borrowPosition(pid: pid)
1759
1760            if pullFromTopUpSource {
1761                if let topUpSource = position.topUpSource {
1762                    let sourceType = topUpSource.getSourceType()
1763                    let sourceAmount = topUpSource.minimumAvailable()
1764                    if self.debugLogging {
1765                        log("    [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.minHealth)")
1766                    }
1767
1768                    return self.fundsAvailableAboveTargetHealthAfterDepositing(
1769                        pid: pid,
1770                        withdrawType: type,
1771                        targetHealth: position.minHealth,
1772                        depositType: sourceType,
1773                        depositAmount: sourceAmount
1774                    )
1775                }
1776            }
1777
1778            let view = self.buildPositionView(pid: pid)
1779
1780            // Build a TokenSnapshot for the requested withdraw type (may not exist in view.snapshots)
1781            let tokenState = self._borrowUpdatedTokenState(type: type)
1782            let snap = FlowALPv0.TokenSnapshot(
1783                price: UFix128(self.priceOracle.price(ofToken: type)!),
1784                credit: tokenState.creditInterestIndex,
1785                debit: tokenState.debitInterestIndex,
1786                risk: FlowALPv0.RiskParams(
1787                    collateralFactor: UFix128(self.collateralFactor[type]!),
1788                    borrowFactor: UFix128(self.borrowFactor[type]!),
1789                )
1790            )
1791
1792            let withdrawBal = view.balances[type]
1793            let uintMax = FlowALPv0.maxWithdraw(
1794                view: view,
1795                withdrawSnap: snap,
1796                withdrawBal: withdrawBal,
1797                targetHealth: view.minHealth
1798            )
1799            return FlowALPMath.toUFix64Round(uintMax)
1800        }
1801
1802        /// Returns the health of the given position, which is the ratio of the position's effective collateral
1803        /// to its debt as denominated in the Pool's default token.
1804        /// "Effective collateral" means the value of each credit balance times the liquidation threshold
1805        /// for that token, i.e. the maximum borrowable amount
1806        // TODO: make this output enumeration of effective debts/collaterals (or provide option that does)
1807        access(all) fun positionHealth(pid: UInt64): UFix128 {
1808            let position = self._borrowPosition(pid: pid)
1809
1810            // Get the position's collateral and debt values in terms of the default token.
1811            var effectiveCollateral: UFix128 = 0.0
1812            var effectiveDebt: UFix128 = 0.0
1813
1814            for type in position.balances.keys {
1815                let balance = position.balances[type]!
1816                let tokenState = self._borrowUpdatedTokenState(type: type)
1817
1818                let collateralFactor = UFix128(self.collateralFactor[type]!)
1819                let borrowFactor = UFix128(self.borrowFactor[type]!)
1820                let price = UFix128(self.priceOracle.price(ofToken: type)!)
1821                switch balance.direction {
1822                    case BalanceDirection.Credit:
1823                        let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1824                            balance.scaledBalance,
1825                            interestIndex: tokenState.creditInterestIndex
1826                        )
1827
1828                        let value = price * trueBalance
1829                        let effectiveCollateralValue = value * collateralFactor
1830                        effectiveCollateral = effectiveCollateral + effectiveCollateralValue
1831
1832                    case BalanceDirection.Debit:
1833                        let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1834                            balance.scaledBalance,
1835                            interestIndex: tokenState.debitInterestIndex
1836                        )
1837
1838                        let value = price * trueBalance
1839                        let effectiveDebtValue = value / borrowFactor
1840                        effectiveDebt = effectiveDebt + effectiveDebtValue
1841                }
1842            }
1843
1844            // Calculate the health as the ratio of collateral to debt.
1845            return FlowALPv0.healthComputation(
1846                effectiveCollateral: effectiveCollateral,
1847                effectiveDebt: effectiveDebt
1848            )
1849        }
1850
1851        /// Returns the quantity of funds of a specified token which would need to be deposited
1852        /// to bring the position to the provided target health.
1853        ///
1854        /// This function will return 0.0 if the position is already at or over that health value.
1855        access(all) fun fundsRequiredForTargetHealth(pid: UInt64, type: Type, targetHealth: UFix128): UFix64 {
1856            return self.fundsRequiredForTargetHealthAfterWithdrawing(
1857                pid: pid,
1858                depositType: type,
1859                targetHealth: targetHealth,
1860                withdrawType: self.defaultToken,
1861                withdrawAmount: 0.0
1862            )
1863        }
1864
1865        /// Returns the details of a given position as a PositionDetails external struct
1866        access(all) fun getPositionDetails(pid: UInt64): PositionDetails {
1867            if self.debugLogging {
1868                log("    [CONTRACT] getPositionDetails(pid: \(pid))")
1869            }
1870            let position = self._borrowPosition(pid: pid)
1871            let balances: [PositionBalance] = []
1872
1873            for type in position.balances.keys {
1874                let balance = position.balances[type]!
1875                let tokenState = self._borrowUpdatedTokenState(type: type)
1876                let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
1877                    balance.scaledBalance,
1878                    interestIndex: balance.direction == BalanceDirection.Credit
1879                        ? tokenState.creditInterestIndex
1880                        : tokenState.debitInterestIndex
1881                )
1882
1883                balances.append(PositionBalance(
1884                    vaultType: type,
1885                    direction: balance.direction,
1886                    balance: FlowALPMath.toUFix64Round(trueBalance)
1887                ))
1888            }
1889
1890            let health = self.positionHealth(pid: pid)
1891            let defaultTokenAvailable = self.availableBalance(
1892                pid: pid,
1893                type: self.defaultToken,
1894                pullFromTopUpSource: false
1895            )
1896
1897            return PositionDetails(
1898                balances: balances,
1899                poolDefaultToken: self.defaultToken,
1900                defaultTokenAvailableBalance: defaultTokenAvailable,
1901                health: health
1902            )
1903        }
1904
1905        /// Any external party can perform a manual liquidation on a position under the following circumstances:
1906        /// - the position has health < 1
1907        /// - the liquidation price offered is better than what is available on a DEX
1908        /// - the liquidation results in a health <= liquidationTargetHF
1909        ///
1910        /// If a liquidation attempt is successful, the balance of the input `repayment` vault is deposited to the pool
1911        /// and a vault containing a balance of `seizeAmount` collateral tokens are returned to the caller.
1912        ///
1913        /// Terminology:
1914        /// - N means number of some token: Nc means number of collateral tokens, Nd means number of debt tokens
1915        /// - P means price of some token: Pc means price of collateral, Pd means price of debt
1916        /// - C means collateral: Ce is effective collateral, Ct is true collateral, measured in $
1917        /// - D means debt: De is effective debt, Dt is true debt, measured in $
1918        /// - Fc, Fd are collateral and debt factors
1919        access(all) fun manualLiquidation(
1920            pid: UInt64,
1921            debtType: Type,
1922            seizeType: Type,
1923            seizeAmount: UFix64,
1924            repayment: @{FungibleToken.Vault}
1925        ): @{FungibleToken.Vault} {
1926            pre {
1927                !self.isPausedOrWarmup(): "Liquidations are paused by governance"
1928                self.isTokenSupported(tokenType: debtType): "Debt token type unsupported: \(debtType.identifier)"
1929                self.isTokenSupported(tokenType: seizeType): "Collateral token type unsupported: \(seizeType.identifier)"
1930                debtType == repayment.getType(): "Repayment vault does not match debt type: \(debtType.identifier)!=\(repayment.getType().identifier)"
1931                // TODO(jord): liquidation paused / post-pause warm
1932            }
1933            post {
1934                self.positionLock[pid] == nil: "Position is not unlocked"
1935            }
1936            
1937            self._lockPosition(pid)
1938
1939            let positionView = self.buildPositionView(pid: pid)
1940            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1941            let initialHealth = balanceSheet.health
1942            assert(initialHealth < 1.0, message: "Cannot liquidate healthy position: \(initialHealth)>=1")
1943
1944            // Ensure liquidation amounts don't exceed position amounts
1945            let repayAmount = repayment.balance
1946            let Nc = positionView.trueBalance(ofToken: seizeType) // number of collateral tokens (true balance)
1947            let Nd = positionView.trueBalance(ofToken: debtType)  // number of debt tokens (true balance)
1948            assert(UFix128(seizeAmount) <= Nc, message: "Cannot seize more collateral than is in position: collateral balance (\(Nc)) is less than seize amount (\(seizeAmount))")
1949            assert(UFix128(repayAmount) <= Nd, message: "Cannot repay more debt than is in position: debt balance (\(Nd)) is less than repay amount (\(repayAmount))")
1950
1951            // Oracle prices
1952            let Pd_oracle = self.priceOracle.price(ofToken: debtType)!  // debt price given by oracle ($/D)
1953            let Pc_oracle = self.priceOracle.price(ofToken: seizeType)! // collateral price given by oracle ($/C)
1954            // Price of collateral, denominated in debt token, implied by oracle (D/C)
1955            // Oracle says: "1 unit of collateral is worth `Pcd_oracle` units of debt"
1956            let Pcd_oracle = Pc_oracle / Pd_oracle 
1957
1958            // Compute the health factor which would result if we were to accept this liquidation
1959            let Ce_pre = balanceSheet.effectiveCollateral // effective collateral pre-liquidation
1960            let De_pre = balanceSheet.effectiveDebt       // effective debt pre-liquidation
1961            let Fc = positionView.snapshots[seizeType]!.risk.collateralFactor
1962            let Fd = positionView.snapshots[debtType]!.risk.borrowFactor
1963
1964            // Ce_seize = effective value of seized collateral ($)
1965            let Ce_seize = FlowALPv0.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc)
1966            // De_seize = effective value of repaid debt ($)
1967            let De_seize = FlowALPv0.effectiveDebt(debit: UFix128(repayAmount), price:  UFix128(Pd_oracle), borrowFactor: Fd) 
1968            let Ce_post = Ce_pre - Ce_seize // position's total effective collateral after liquidation ($)
1969            let De_post = De_pre - De_seize // position's total effective debt after liquidation ($)
1970            let postHealth = FlowALPv0.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post)
1971            assert(postHealth <= self.liquidationTargetHF, message: "Liquidation must not exceed target health: post-liquidation health (\(postHealth)) is greater than target health (\(self.liquidationTargetHF))")
1972
1973            // Compare the liquidation offer to liquidation via DEX. If the DEX would provide a better price, reject the offer.
1974            let swapper = self._getSwapperForLiquidation(seizeType: seizeType, debtType: debtType)
1975            // Get a quote: "how much collateral do I need to give you to get `repayAmount` debt tokens"
1976            let quote = swapper.quoteIn(forDesired: repayAmount, reverse: false)
1977            assert(seizeAmount < quote.inAmount, message: "Liquidation offer must be better than that offered by DEX")
1978
1979            // Compare the DEX price to the oracle price and revert if they diverge beyond configured threshold.
1980            let Pcd_dex = quote.outAmount / quote.inAmount // price of collateral, denominated in debt token, implied by dex quote (D/C)
1981            assert(
1982                FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: Pcd_dex, oraclePrice: Pcd_oracle, maxDeviationBps: self.dexOracleDeviationBps),
1983                message: "DEX/oracle price deviation too large. Dex price: \(Pcd_dex), Oracle price: \(Pcd_oracle)")
1984            // Execute the liquidation
1985            let seizedCollateral <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount)
1986            
1987            self._unlockPosition(pid)
1988            
1989            return <- seizedCollateral
1990        }
1991
1992        /// Gets a swapper from the DEX for the given token pair.
1993        ///
1994        /// This function is used during liquidations to compare the liquidator's offer against the DEX price.
1995        /// It expects that a swapper has been configured for every supported collateral-to-debt token pair.
1996        ///
1997        /// Panics if:
1998        /// - No swapper is configured for the given token pair (seizeType -> debtType)
1999        ///
2000        /// @param seizeType: The collateral token type to swap from
2001        /// @param debtType: The debt token type to swap to
2002        /// @return The swapper for the given token pair
2003        access(self) fun _getSwapperForLiquidation(seizeType: Type, debtType: Type): {DeFiActions.Swapper} {
2004            return self.dex.getSwapper(inType: seizeType, outType: debtType)
2005                ?? panic("No DEX swapper configured for liquidation pair: \(seizeType.identifier) -> \(debtType.identifier)")
2006        }
2007
2008        /// Internal liquidation function which performs a liquidation.
2009        /// The balance of `repayment` is deposited to the debt token reserve, and `seizeAmount` units of collateral are returned.
2010        /// Callers are responsible for checking preconditions.
2011        access(self) fun _doLiquidation(pid: UInt64, repayment: @{FungibleToken.Vault}, debtType: Type, seizeType: Type, seizeAmount: UFix64): @{FungibleToken.Vault} {
2012            pre {
2013                !self.isPausedOrWarmup(): "Liquidations are paused by governance"
2014                // position must have debt and collateral balance 
2015            }
2016
2017            let repayAmount = repayment.balance
2018            assert(repayment.getType() == debtType, message: "Vault type mismatch for repay. Repayment type is \(repayment.getType().identifier) but debt type is \(debtType.identifier)")
2019            let debtReserveRef = self._borrowOrCreateReserveVault(type: debtType)
2020            debtReserveRef.deposit(from: <-repayment)
2021
2022            // Reduce borrower's debt position by repayAmount
2023            let position = self._borrowPosition(pid: pid)
2024            let debtState = self._borrowUpdatedTokenState(type: debtType)
2025
2026            if position.balances[debtType] == nil {
2027                position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0)
2028            }
2029            position.balances[debtType]!.recordDeposit(amount: UFix128(repayAmount), tokenState: debtState)
2030
2031            // Withdraw seized collateral from position and send to liquidator
2032            let seizeState = self._borrowUpdatedTokenState(type: seizeType)
2033            if position.balances[seizeType] == nil {
2034                position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0)
2035            }
2036            position.balances[seizeType]!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState)
2037            let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
2038            let seizedCollateral <- seizeReserveRef.withdraw(amount: seizeAmount)
2039
2040            let newHealth = self.positionHealth(pid: pid)
2041            // TODO: sanity check health here? for auto-liquidating, we may need to perform a bounded search which could result in unbounded error in the final health
2042
2043            emit LiquidationExecuted(
2044            	pid: pid,
2045            	poolUUID: self.uuid,
2046            	debtType: debtType.identifier,
2047            	repayAmount: repayAmount,
2048            	seizeType: seizeType.identifier,
2049            	seizeAmount: seizeAmount,
2050            	newHF: newHealth
2051            )
2052
2053            return <-seizedCollateral
2054        }
2055
2056        /// Returns the quantity of funds of a specified token which would need to be deposited
2057        /// in order to bring the position to the target health
2058        /// assuming we also withdraw a specified amount of another token.
2059        ///
2060        /// This function will return 0.0 if the position would already be at or over the target health value
2061        /// after the proposed withdrawal.
2062        access(all) fun fundsRequiredForTargetHealthAfterWithdrawing(
2063            pid: UInt64,
2064            depositType: Type,
2065            targetHealth: UFix128,
2066            withdrawType: Type,
2067            withdrawAmount: UFix64
2068        ): UFix64 {
2069            pre {
2070                targetHealth >= 1.0: "Target health (\(targetHealth)) must be >=1 after any withdrawal"
2071            }
2072
2073            if self.debugLogging {
2074                log("    [CONTRACT] fundsRequiredForTargetHealthAfterWithdrawing(pid: \(pid), depositType: \(depositType.contractName!), targetHealth: \(targetHealth), withdrawType: \(withdrawType.contractName!), withdrawAmount: \(withdrawAmount))")
2075            }
2076
2077            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
2078            let position = self._borrowPosition(pid: pid)
2079
2080            let adjusted = self.computeAdjustedBalancesAfterWithdrawal(
2081                balanceSheet: balanceSheet,
2082                position: position,
2083                withdrawType: withdrawType,
2084                withdrawAmount: withdrawAmount
2085            )
2086
2087            return self.computeRequiredDepositForHealth(
2088                position: position,
2089                depositType: depositType,
2090                withdrawType: withdrawType,
2091                effectiveCollateral: adjusted.effectiveCollateral,
2092                effectiveDebt: adjusted.effectiveDebt,
2093                targetHealth: targetHealth
2094            )
2095        }
2096
2097        // TODO: documentation
2098        access(self) fun computeAdjustedBalancesAfterWithdrawal(
2099            balanceSheet: BalanceSheet,
2100            position: &InternalPosition,
2101            withdrawType: Type,
2102            withdrawAmount: UFix64
2103        ): BalanceSheet {
2104            var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral
2105            var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt
2106
2107            if withdrawAmount == 0.0 {
2108                return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal)
2109            }
2110            if self.debugLogging {
2111                log("    [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)")
2112                log("    [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)")
2113            }
2114
2115            let withdrawAmountU = UFix128(withdrawAmount)
2116            let withdrawPrice2 = UFix128(self.priceOracle.price(ofToken: withdrawType)!)
2117            let withdrawBorrowFactor2 = UFix128(self.borrowFactor[withdrawType]!)
2118            let balance = position.balances[withdrawType]
2119            let direction = balance?.direction ?? BalanceDirection.Debit
2120            let scaledBalance = balance?.scaledBalance ?? 0.0
2121
2122            switch direction {
2123                case BalanceDirection.Debit:
2124                    // If the position doesn't have any collateral for the withdrawn token,
2125                    // we can just compute how much additional effective debt the withdrawal will create.
2126                    effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt +
2127                        (withdrawAmountU * withdrawPrice2) / withdrawBorrowFactor2
2128
2129                case BalanceDirection.Credit:
2130                    let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType)
2131
2132                    // The user has a collateral position in the given token, we need to figure out if this withdrawal
2133                    // will flip over into debt, or just draw down the collateral.
2134                    let trueCollateral = FlowALPv0.scaledBalanceToTrueBalance(
2135                        scaledBalance,
2136                        interestIndex: withdrawTokenState.creditInterestIndex
2137                    )
2138                    let collateralFactor = UFix128(self.collateralFactor[withdrawType]!)
2139                    if trueCollateral >= withdrawAmountU {
2140                        // This withdrawal will draw down collateral, but won't create debt, we just need to account
2141                        // for the collateral decrease.
2142                        effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral -
2143                            (withdrawAmountU * withdrawPrice2) * collateralFactor
2144                    } else {
2145                        // The withdrawal will wipe out all of the collateral, and create some debt.
2146                        effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt +
2147                            ((withdrawAmountU - trueCollateral) * withdrawPrice2) / withdrawBorrowFactor2
2148                        effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral -
2149                            (trueCollateral * withdrawPrice2) * collateralFactor
2150                    }
2151            }
2152
2153            return BalanceSheet(
2154                effectiveCollateral: effectiveCollateralAfterWithdrawal,
2155                effectiveDebt: effectiveDebtAfterWithdrawal
2156            )
2157        }
2158
2159        // TODO(jord): ~100-line function - consider refactoring
2160        // TODO: documentation
2161         access(self) fun computeRequiredDepositForHealth(
2162            position: &InternalPosition,
2163            depositType: Type,
2164            withdrawType: Type,
2165            effectiveCollateral: UFix128,
2166            effectiveDebt: UFix128,
2167            targetHealth: UFix128
2168        ): UFix64 {
2169            let effectiveCollateralAfterWithdrawal = effectiveCollateral
2170            var effectiveDebtAfterWithdrawal = effectiveDebt
2171
2172            if self.debugLogging {
2173                log("    [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)")
2174                log("    [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)")
2175            }
2176
2177            // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!)
2178            // Now we can figure out how many of the given token would need to be deposited to bring the position
2179            // to the target health value.
2180            var healthAfterWithdrawal = FlowALPv0.healthComputation(
2181                effectiveCollateral: effectiveCollateralAfterWithdrawal,
2182                effectiveDebt: effectiveDebtAfterWithdrawal
2183            )
2184            if self.debugLogging {
2185                log("    [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)")
2186            }
2187
2188            if healthAfterWithdrawal >= targetHealth {
2189                // The position is already at or above the target health, so we don't need to deposit anything.
2190                return 0.0
2191            }
2192
2193            // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep
2194            // track of the number of tokens that went towards paying off debt.
2195            var debtTokenCount: UFix128 = 0.0
2196            let depositPrice = UFix128(self.priceOracle.price(ofToken: depositType)!)
2197            let depositBorrowFactor = UFix128(self.borrowFactor[depositType]!)
2198            let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!)
2199            let maybeBalance = position.balances[depositType]
2200            if maybeBalance?.direction == BalanceDirection.Debit {
2201                // The user has a debt position in the given token, we start by looking at the health impact of paying off
2202                // the entire debt.
2203                let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
2204                let debtBalance = maybeBalance!.scaledBalance
2205                let trueDebtTokenCount = FlowALPv0.scaledBalanceToTrueBalance(
2206                    debtBalance,
2207                    interestIndex: depositTokenState.debitInterestIndex
2208                )
2209                let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor
2210
2211                // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal,
2212                // it means we can pay off all debt
2213                var effectiveDebtAfterPayment: UFix128 = 0.0
2214                if debtEffectiveValue <= effectiveDebtAfterWithdrawal {
2215                    effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue
2216                }
2217
2218                // Check what the new health would be if we paid off all of this debt
2219                let potentialHealth = FlowALPv0.healthComputation(
2220                    effectiveCollateral: effectiveCollateralAfterWithdrawal,
2221                    effectiveDebt: effectiveDebtAfterPayment
2222                )
2223
2224                // Does paying off all of the debt reach the target health? Then we're done.
2225                if potentialHealth >= targetHealth {
2226                    // We can reach the target health by paying off some or all of the debt. We can easily
2227                    // compute how many units of the token would be needed to reach the target health.
2228                    let healthChange = targetHealth - healthAfterWithdrawal
2229                    let requiredEffectiveDebt = effectiveDebtAfterWithdrawal
2230                        - (effectiveCollateralAfterWithdrawal / targetHealth)
2231
2232                    // The amount of the token to pay back, in units of the token.
2233                    let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice
2234
2235                    if self.debugLogging {
2236                        log("    [CONTRACT] paybackAmount: \(paybackAmount)")
2237                    }
2238
2239                    return FlowALPMath.toUFix64RoundUp(paybackAmount)
2240                } else {
2241                    // We can pay off the entire debt, but we still need to deposit more to reach the target health.
2242                    // We have logic below that can determine the collateral deposition required to reach the target health
2243                    // from this new health position. Rather than copy that logic here, we fall through into it. But first
2244                    // we have to record the amount of tokens that went towards debt payback and adjust the effective
2245                    // debt to reflect that it has been paid off.
2246                    debtTokenCount = trueDebtTokenCount
2247                    // Ensure we don't underflow
2248                    if debtEffectiveValue <= effectiveDebtAfterWithdrawal {
2249                        effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue
2250                    } else {
2251                        effectiveDebtAfterWithdrawal = 0.0
2252                    }
2253                    healthAfterWithdrawal = potentialHealth
2254                }
2255            }
2256
2257            // At this point, we're either dealing with a position that didn't have a debt position in the deposit
2258            // token, or we've accounted for the debt payoff and adjusted the effective debt above.
2259            // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the
2260            // target health. We can rearrange the health equation to solve for the required collateral:
2261
2262            // We need to increase the effective collateral from its current value to the required value, so we
2263            // multiply the required health change by the effective debt, and turn that into a token amount.
2264            let healthChangeU = targetHealth - healthAfterWithdrawal
2265            // TODO: apply the same logic as below to the early return blocks above
2266            let depositCollateralFactor = UFix128(self.collateralFactor[depositType]!)
2267            let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor
2268
2269            // The amount of the token to deposit, in units of the token.
2270            let collateralTokenCount = requiredEffectiveCollateral / depositPrice
2271            if self.debugLogging {
2272                log("    [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)")
2273                log("    [CONTRACT] collateralTokenCount: \(collateralTokenCount)")
2274                log("    [CONTRACT] debtTokenCount: \(debtTokenCount)")
2275                log("    [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)")
2276            }
2277
2278            // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt.
2279            return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount)
2280        }
2281
2282        /// Returns the quantity of the specified token that could be withdrawn
2283        /// while still keeping the position's health at or above the provided target.
2284        access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix128): UFix64 {
2285            return self.fundsAvailableAboveTargetHealthAfterDepositing(
2286                pid: pid,
2287                withdrawType: type,
2288                targetHealth: targetHealth,
2289                depositType: self.defaultToken,
2290                depositAmount: 0.0
2291            )
2292        }
2293
2294        /// Returns the quantity of the specified token that could be withdrawn
2295        /// while still keeping the position's health at or above the provided target,
2296        /// assuming we also deposit a specified amount of another token.
2297        access(all) fun fundsAvailableAboveTargetHealthAfterDepositing(
2298            pid: UInt64,
2299            withdrawType: Type,
2300            targetHealth: UFix128,
2301            depositType: Type,
2302            depositAmount: UFix64
2303        ): UFix64 {
2304            if self.debugLogging {
2305                log("    [CONTRACT] fundsAvailableAboveTargetHealthAfterDepositing(pid: \(pid), withdrawType: \(withdrawType.contractName!), targetHealth: \(targetHealth), depositType: \(depositType.contractName!), depositAmount: \(depositAmount))")
2306            }
2307            if depositType == withdrawType && depositAmount > 0.0 {
2308                // If the deposit and withdrawal types are the same, we compute the available funds assuming
2309                // no deposit (which is less work) and increase that by the deposit amount at the end
2310                let fundsAvailable = self.fundsAvailableAboveTargetHealth(
2311                    pid: pid,
2312                    type: withdrawType,
2313                    targetHealth: targetHealth
2314                )
2315                return fundsAvailable + depositAmount
2316            }
2317
2318            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
2319            let position = self._borrowPosition(pid: pid)
2320
2321            let adjusted = self.computeAdjustedBalancesAfterDeposit(
2322                balanceSheet: balanceSheet,
2323                position: position,
2324                depositType: depositType,
2325                depositAmount: depositAmount
2326            )
2327
2328            return self.computeAvailableWithdrawal(
2329                position: position,
2330                withdrawType: withdrawType,
2331                effectiveCollateral: adjusted.effectiveCollateral,
2332                effectiveDebt: adjusted.effectiveDebt,
2333                targetHealth: targetHealth
2334            )
2335        }
2336
2337        // Helper function to compute balances after deposit
2338        access(self) fun computeAdjustedBalancesAfterDeposit(
2339            balanceSheet: BalanceSheet,
2340            position: &InternalPosition,
2341            depositType: Type,
2342            depositAmount: UFix64
2343        ): BalanceSheet {
2344            var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral
2345            var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt
2346
2347            if self.debugLogging {
2348                log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
2349                log("    [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)")
2350            }
2351            if depositAmount == 0.0 {
2352                return BalanceSheet(
2353                    effectiveCollateral: effectiveCollateralAfterDeposit,
2354                    effectiveDebt: effectiveDebtAfterDeposit
2355                )
2356            }
2357
2358            let depositAmountCasted = UFix128(depositAmount)
2359            let depositPriceCasted = UFix128(self.priceOracle.price(ofToken: depositType)!)
2360            let depositBorrowFactorCasted = UFix128(self.borrowFactor[depositType]!)
2361            let depositCollateralFactorCasted = UFix128(self.collateralFactor[depositType]!)
2362            let balance = position.balances[depositType]
2363            let direction = balance?.direction ?? BalanceDirection.Credit
2364            let scaledBalance = balance?.scaledBalance ?? 0.0
2365
2366            switch direction {
2367                case BalanceDirection.Credit:
2368                    // If there's no debt for the deposit token,
2369                    // we can just compute how much additional effective collateral the deposit will create.
2370                    effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral +
2371                        (depositAmountCasted * depositPriceCasted) * depositCollateralFactorCasted
2372
2373                case BalanceDirection.Debit:
2374                    let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
2375
2376                    // The user has a debt position in the given token, we need to figure out if this deposit
2377                    // will result in net collateral, or just bring down the debt.
2378                    let trueDebt = FlowALPv0.scaledBalanceToTrueBalance(
2379                        scaledBalance,
2380                        interestIndex: depositTokenState.debitInterestIndex
2381                    )
2382                    if self.debugLogging {
2383                        log("    [CONTRACT] trueDebt: \(trueDebt)")
2384                    }
2385
2386                    if trueDebt >= depositAmountCasted {
2387                        // This deposit will pay down some debt, but won't result in net collateral, we
2388                        // just need to account for the debt decrease.
2389                        // TODO - validate if this should deal with withdrawType or depositType
2390                        effectiveDebtAfterDeposit = balanceSheet.effectiveDebt -
2391                            (depositAmountCasted * depositPriceCasted) / depositBorrowFactorCasted
2392                    } else {
2393                        // The deposit will wipe out all of the debt, and create some collateral.
2394                        // TODO - validate if this should deal with withdrawType or depositType
2395                        effectiveDebtAfterDeposit = balanceSheet.effectiveDebt -
2396                            (trueDebt * depositPriceCasted) / depositBorrowFactorCasted
2397                        effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral +
2398                            (depositAmountCasted - trueDebt) * depositPriceCasted * depositCollateralFactorCasted
2399                    }
2400            }
2401
2402            if self.debugLogging {
2403                log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
2404                log("    [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)")
2405            }
2406
2407            // We now have new effective collateral and debt values that reflect the proposed deposit (if any!).
2408            // Now we can figure out how many of the withdrawal token are available while keeping the position
2409            // at or above the target health value.
2410            return BalanceSheet(
2411                effectiveCollateral: effectiveCollateralAfterDeposit,
2412                effectiveDebt: effectiveDebtAfterDeposit
2413            )
2414        }
2415
2416        // Helper function to compute available withdrawal
2417        // TODO(jord): ~100-line function - consider refactoring
2418        access(self) fun computeAvailableWithdrawal(
2419            position: &InternalPosition,
2420            withdrawType: Type,
2421            effectiveCollateral: UFix128,
2422            effectiveDebt: UFix128,
2423            targetHealth: UFix128
2424        ): UFix64 {
2425            var effectiveCollateralAfterDeposit = effectiveCollateral
2426            let effectiveDebtAfterDeposit = effectiveDebt
2427
2428            let healthAfterDeposit = FlowALPv0.healthComputation(
2429                effectiveCollateral: effectiveCollateralAfterDeposit,
2430                effectiveDebt: effectiveDebtAfterDeposit
2431            )
2432            if self.debugLogging {
2433                log("    [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)")
2434            }
2435
2436            if healthAfterDeposit <= targetHealth {
2437                // The position is already at or below the provided target health, so we can't withdraw anything.
2438                return 0.0
2439            }
2440
2441            // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep
2442            // track of the number of tokens that are available from collateral
2443            var collateralTokenCount: UFix128 = 0.0
2444
2445            let withdrawPrice = UFix128(self.priceOracle.price(ofToken: withdrawType)!)
2446            let withdrawCollateralFactor = UFix128(self.collateralFactor[withdrawType]!)
2447            let withdrawBorrowFactor = UFix128(self.borrowFactor[withdrawType]!)
2448
2449            let maybeBalance = position.balances[withdrawType]
2450            if maybeBalance?.direction == BalanceDirection.Credit {
2451                // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all
2452                // of that collateral
2453                let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType)
2454                let creditBalance = maybeBalance!.scaledBalance
2455                let trueCredit = FlowALPv0.scaledBalanceToTrueBalance(
2456                    creditBalance,
2457                    interestIndex: withdrawTokenState.creditInterestIndex
2458                )
2459                let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor
2460
2461                // Check what the new health would be if we took out all of this collateral
2462                let potentialHealth = FlowALPv0.healthComputation(
2463                    effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract?
2464                    effectiveDebt: effectiveDebtAfterDeposit
2465                )
2466
2467                // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only.
2468                if potentialHealth <= targetHealth {
2469                    // We will hit the health target before using up all of the withdraw token credit. We can easily
2470                    // compute how many units of the token would bring the position down to the target health.
2471                    // We will hit the health target before using up all available withdraw credit.
2472
2473                    let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit)
2474                    if self.debugLogging {
2475                        log("    [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)")
2476                    }
2477
2478                    // The amount of the token we can take using that amount of health
2479                    let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice
2480                    if self.debugLogging {
2481                        log("    [CONTRACT] availableTokenCount: \(availableTokenCount)")
2482                    }
2483
2484                    return FlowALPMath.toUFix64RoundDown(availableTokenCount)
2485                } else {
2486                    // We can flip this credit position into a debit position, before hitting the target health.
2487                    // We have logic below that can determine health changes for debit positions. We've copied it here
2488                    // with an added handling for the case where the health after deposit is an edgecase
2489                    collateralTokenCount = trueCredit
2490                    effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue
2491                    if self.debugLogging {
2492                        log("    [CONTRACT] collateralTokenCount: \(collateralTokenCount)")
2493                        log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
2494                    }
2495
2496                    // We can calculate the available debt increase that would bring us to the target health
2497                    let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit
2498                    let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice
2499                    if self.debugLogging {
2500                        log("    [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)")
2501                        log("    [CONTRACT] availableTokens: \(availableTokens)")
2502                        log("    [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)")
2503                    }
2504                    return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount)
2505                }
2506            }
2507
2508            // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw
2509            // token, or we've accounted for the credit balance and adjusted the effective collateral above.
2510
2511            // We can calculate the available debt increase that would bring us to the target health
2512            let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit
2513            let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice
2514            if self.debugLogging {
2515                log("    [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)")
2516                log("    [CONTRACT] availableTokens: \(availableTokens)")
2517                log("    [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)")
2518            }
2519            return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount)
2520        }
2521
2522        /// Returns the position's health if the given amount of the specified token were deposited
2523        access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix128 {
2524            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
2525            let position = self._borrowPosition(pid: pid)
2526            let tokenState = self._borrowUpdatedTokenState(type: type)
2527
2528            var effectiveCollateralIncrease: UFix128 = 0.0
2529            var effectiveDebtDecrease: UFix128 = 0.0
2530
2531            let amountU = UFix128(amount)
2532            let price = UFix128(self.priceOracle.price(ofToken: type)!)
2533            let collateralFactor = UFix128(self.collateralFactor[type]!)
2534            let borrowFactor = UFix128(self.borrowFactor[type]!)
2535            let balance = position.balances[type]
2536            let direction = balance?.direction ?? BalanceDirection.Credit
2537            let scaledBalance = balance?.scaledBalance ?? 0.0
2538            switch direction {
2539                case BalanceDirection.Credit:
2540                    // Since the user has no debt in the given token,
2541                    // we can just compute how much additional collateral this deposit will create.
2542                    effectiveCollateralIncrease = (amountU * price) * collateralFactor
2543
2544                case BalanceDirection.Debit:
2545                    // The user has a debit position in the given token,
2546                    // we need to figure out if this deposit will only pay off some of the debt,
2547                    // or if it will also create new collateral.
2548                    let trueDebt = FlowALPv0.scaledBalanceToTrueBalance(
2549                        scaledBalance,
2550                        interestIndex: tokenState.debitInterestIndex
2551                    )
2552
2553                    if trueDebt >= amountU {
2554                        // This deposit will wipe out some or all of the debt, but won't create new collateral,
2555                        // we just need to account for the debt decrease.
2556                        effectiveDebtDecrease = (amountU * price) / borrowFactor
2557                    } else {
2558                        // This deposit will wipe out all of the debt, and create new collateral.
2559                        effectiveDebtDecrease = (trueDebt * price) / borrowFactor
2560                        effectiveCollateralIncrease = (amountU - trueDebt) * price * collateralFactor
2561                    }
2562            }
2563
2564            return FlowALPv0.healthComputation(
2565                effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease,
2566                effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease
2567            )
2568        }
2569
2570        // Returns health value of this position if the given amount of the specified token were withdrawn without
2571        // using the top up source.
2572        // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates
2573        // that the proposed withdrawal would fail (unless a top up source is available and used).
2574        access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix128 {
2575            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
2576            let position = self._borrowPosition(pid: pid)
2577            let tokenState = self._borrowUpdatedTokenState(type: type)
2578
2579            var effectiveCollateralDecrease: UFix128 = 0.0
2580            var effectiveDebtIncrease: UFix128 = 0.0
2581
2582            let amountU = UFix128(amount)
2583            let price = UFix128(self.priceOracle.price(ofToken: type)!)
2584            let collateralFactor = UFix128(self.collateralFactor[type]!)
2585            let borrowFactor = UFix128(self.borrowFactor[type]!)
2586            let balance = position.balances[type]
2587            let direction = balance?.direction ?? BalanceDirection.Debit
2588            let scaledBalance = balance?.scaledBalance ?? 0.0
2589
2590            switch direction {
2591                case BalanceDirection.Debit:
2592                    // The user has no credit position in the given token,
2593                    // we can just compute how much additional effective debt this withdrawal will create.
2594                    effectiveDebtIncrease = (amountU * price) / borrowFactor
2595
2596                case BalanceDirection.Credit:
2597                    // The user has a credit position in the given token,
2598                    // we need to figure out if this withdrawal will only draw down some of the collateral,
2599                    // or if it will also create new debt.
2600                    let trueCredit = FlowALPv0.scaledBalanceToTrueBalance(
2601                        scaledBalance,
2602                        interestIndex: tokenState.creditInterestIndex
2603                    )
2604
2605                    if trueCredit >= amountU {
2606                        // This withdrawal will draw down some collateral, but won't create new debt,
2607                        // we just need to account for the collateral decrease.
2608                        effectiveCollateralDecrease = (amountU * price) * collateralFactor
2609                    } else {
2610                        // The withdrawal will wipe out all of the collateral, and create new debt.
2611                        effectiveDebtIncrease = ((amountU - trueCredit) * price) / borrowFactor
2612                        effectiveCollateralDecrease = (trueCredit * price) * collateralFactor
2613                    }
2614            }
2615
2616            return FlowALPv0.healthComputation(
2617                effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease,
2618                effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease
2619            )
2620        }
2621
2622        ///////////////////////////
2623        // POSITION MANAGEMENT
2624        ///////////////////////////
2625
2626        /// Creates a lending position against the provided collateral funds,
2627        /// depositing the loaned amount to the given Sink.
2628        /// If a Source is provided, the position will be configured to pull loan repayment
2629        /// when the loan becomes undercollateralized, preferring repayment to outright liquidation.
2630        ///
2631        /// Returns a Position resource that provides fine-grained access control through entitlements.
2632        /// The caller must store the Position resource in their account and manage access to it.
2633        /// Clients are recommended to use the PositionManager collection type to manage their Positions.
2634        access(EParticipant) fun createPosition(
2635            funds: @{FungibleToken.Vault},
2636            issuanceSink: {DeFiActions.Sink},
2637            repaymentSource: {DeFiActions.Source}?,
2638            pushToDrawDownSink: Bool
2639        ): @Position {
2640            pre {
2641                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
2642                self.globalLedger[funds.getType()] != nil:
2643                    "Invalid token type \(funds.getType().identifier) - not supported by this Pool"
2644                self.positionSatisfiesMinimumBalance(type: funds.getType(), balance: UFix128(funds.balance)):
2645                    "Insufficient funds to create position. Minimum deposit of \(funds.getType().identifier) is \(self.globalLedger[funds.getType()]!.minimumTokenBalancePerPosition)"
2646                // TODO(jord): Sink/source should be valid
2647            }
2648            post {
2649                self.positionLock[result.id] == nil: "Position is not unlocked"
2650            }
2651            // construct a new InternalPosition, assigning it the current position ID
2652            let id = self.nextPositionID
2653            self.nextPositionID = self.nextPositionID + 1
2654            self.positions[id] <-! create InternalPosition()
2655
2656            self._lockPosition(id)
2657
2658            emit Opened(
2659                pid: id,
2660                poolUUID: self.uuid
2661            )
2662
2663            // assign issuance & repayment connectors within the InternalPosition
2664            let iPos = self._borrowPosition(pid: id)
2665            let fundsType = funds.getType()
2666            iPos.setDrawDownSink(issuanceSink)
2667            if repaymentSource != nil {
2668                iPos.setTopUpSource(repaymentSource)
2669            }
2670
2671            // deposit the initial funds
2672            self._depositEffectsOnly(pid: id, from: <-funds)
2673
2674            // Rebalancing and queue management
2675            if pushToDrawDownSink {
2676                self._rebalancePositionNoLock(pid: id, force: true)
2677            }
2678
2679            // Create a capability to the Pool for the Position resource
2680            // The Pool is stored in the FlowALPv0 contract account
2681            let poolCap = FlowALPv0.account.capabilities.storage.issue<auth(EPosition) &Pool>(
2682                FlowALPv0.PoolStoragePath
2683            )
2684
2685            // Create and return the Position resource
2686
2687            let position <- create Position(id: id, pool: poolCap)
2688
2689            self._unlockPosition(id)
2690            return <-position
2691        }
2692
2693        /// Checks if a balance meets the minimum token balance requirement for a given token type.
2694        ///
2695        /// This function is used to validate that positions maintain a minimum balance to prevent
2696        /// dust positions and ensure operational efficiency. The minimum requirement applies to
2697        /// credit (deposit) balances and is enforced at position creation and during withdrawals.
2698        ///
2699        /// @param type: The token type to check (e.g., Type<@FlowToken.Vault>())
2700        /// @param balance: The balance amount to validate
2701        /// @return true if the balance meets or exceeds the minimum requirement, false otherwise
2702        access(self) view fun positionSatisfiesMinimumBalance(type: Type, balance: UFix128): Bool {
2703            return balance >= UFix128(self.globalLedger[type]!.minimumTokenBalancePerPosition)
2704        }
2705
2706        /// Allows anyone to deposit funds into any position.
2707        /// If the provided Vault is not supported by the Pool, the operation reverts.
2708        access(EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) {
2709            pre {
2710                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
2711            }
2712            self.depositAndPush(
2713                pid: pid,
2714                from: <-from,
2715                pushToDrawDownSink: false
2716            )
2717        }
2718        /// Applies the state transitions for depositing `from` into `pid`, without doing any of the
2719        /// surrounding orchestration (locking, health checks, rebalancing, or caller authorization).
2720        ///
2721        /// This helper is intentionally effects-only: it *mutates* Pool/Position state and consumes `from`,
2722        /// but assumes all higher-level preconditions have already been enforced by the caller.
2723        ///
2724        /// TODO(jord): ~100-line function - consider refactoring.
2725        access(self) fun _depositEffectsOnly(
2726            pid: UInt64,
2727            from: @{FungibleToken.Vault}
2728        ) {
2729            pre {
2730                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
2731            }
2732            // NOTE: caller must have already validated pid + token support
2733            let amount = from.balance
2734            if amount == 0.0 {
2735                Burner.burn(<-from)
2736                return
2737            }
2738
2739            // Get a reference to the user's position and global token state for the affected token.
2740            let type = from.getType()
2741            let depositedUUID = from.uuid
2742            let position = self._borrowPosition(pid: pid)
2743            let tokenState = self._borrowUpdatedTokenState(type: type)
2744
2745            // Time-based state is handled by the tokenState() helper function
2746
2747            // Deposit rate limiting: prevent a single large deposit from monopolizing capacity.
2748            // Excess is queued to be processed asynchronously (see asyncUpdatePosition).
2749            let depositAmount = from.balance
2750            let depositLimit = tokenState.depositLimit()
2751
2752            if depositAmount > depositLimit {
2753                // The deposit is too big, so we need to queue the excess
2754                let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit)
2755
2756                if position.queuedDeposits[type] == nil {
2757                    position.queuedDeposits[type] <-! queuedDeposit
2758                } else {
2759                    position.queuedDeposits[type]!.deposit(from: <-queuedDeposit)
2760                }
2761            }
2762
2763            // Per-user deposit limit: check if user has exceeded their per-user limit
2764            let userDepositLimitCap = tokenState.getUserDepositLimitCap()
2765            let currentUsage = tokenState.depositUsage[pid] ?? 0.0
2766            let remainingUserLimit = userDepositLimitCap - currentUsage
2767            
2768            // If the deposit would exceed the user's limit, queue or reject the excess
2769            if from.balance > remainingUserLimit {
2770                let excessAmount = from.balance - remainingUserLimit
2771                let queuedForUserLimit <- from.withdraw(amount: excessAmount)
2772                
2773                if position.queuedDeposits[type] == nil {
2774                    position.queuedDeposits[type] <-! queuedForUserLimit
2775                } else {
2776                    position.queuedDeposits[type]!.deposit(from: <-queuedForUserLimit)
2777                }
2778            }
2779
2780            // If this position doesn't currently have an entry for this token, create one.
2781            if position.balances[type] == nil {
2782                position.balances[type] = InternalBalance(
2783                    direction: BalanceDirection.Credit,
2784                    scaledBalance: 0.0
2785                )
2786            }
2787
2788            // Create vault if it doesn't exist yet
2789            if self.reserves[type] == nil {
2790                self.reserves[type] <-! from.createEmptyVault()
2791            }
2792            let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
2793
2794            // Reflect the deposit in the position's balance.
2795            //
2796            // This only records the portion of the deposit that was accepted, not any queued portions,
2797            // as the queued deposits will be processed later (by this function being called again), and therefore
2798            // will be recorded at that time.
2799            let acceptedAmount = from.balance
2800            position.balances[type]!.recordDeposit(
2801                amount: UFix128(acceptedAmount),
2802                tokenState: tokenState
2803            )
2804
2805            // Consume deposit capacity for the accepted deposit amount and track per-user usage
2806            // Only the accepted amount consumes capacity; queued portions will consume capacity when processed later
2807            tokenState.consumeDepositCapacity(acceptedAmount, pid: pid)
2808
2809            // Add the money to the reserves
2810            reserveVault.deposit(from: <-from)
2811
2812            self._queuePositionForUpdateIfNecessary(pid: pid)
2813
2814            emit Deposited(
2815                pid: pid,
2816                poolUUID: self.uuid,
2817                vaultType: type,
2818                amount: amount,
2819                depositedUUID: depositedUUID
2820            )
2821
2822        }
2823
2824        /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option.
2825        /// If `pushToDrawDownSink` is true, excess value putting the position above its max health
2826        /// is pushed to the position's configured `drawDownSink`.
2827        access(EPosition) fun depositAndPush(
2828            pid: UInt64,
2829            from: @{FungibleToken.Vault},
2830            pushToDrawDownSink: Bool
2831        ) {
2832            pre {
2833                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
2834                self.positions[pid] != nil:
2835                    "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool"
2836                self.globalLedger[from.getType()] != nil:
2837                    "Invalid token type \(from.getType().identifier) - not supported by this Pool"
2838            }
2839            post {
2840                self.positionLock[pid] == nil: "Position is not unlocked"
2841            }
2842            if self.debugLogging {
2843                log("    [CONTRACT] depositAndPush(pid: \(pid), pushToDrawDownSink: \(pushToDrawDownSink))")
2844            }
2845
2846            self._lockPosition(pid)
2847
2848            self._depositEffectsOnly(pid: pid, from: <-from)
2849
2850            // Rebalancing and queue management
2851            if pushToDrawDownSink {
2852                self._rebalancePositionNoLock(pid: pid, force: true)
2853            }
2854
2855            self._unlockPosition(pid)
2856        }
2857
2858        /// Withdraws the requested funds from the specified position.
2859        ///
2860        /// Callers should be careful that the withdrawal does not put their position under its target health,
2861        /// especially if the position doesn't have a configured `topUpSource` from which to repay borrowed funds
2862        /// in the event of undercollaterlization.
2863        access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} {
2864            pre {
2865                !self.isPausedOrWarmup(): "Withdrawals are paused by governance"
2866            }
2867            // Call the enhanced function with pullFromTopUpSource = false for backward compatibility
2868            return <- self.withdrawAndPull(
2869                pid: pid,
2870                type: type,
2871                amount: amount,
2872                pullFromTopUpSource: false
2873            )
2874        }
2875
2876        /// Withdraws the requested funds from the specified position
2877        /// with the configurable `pullFromTopUpSource` option.
2878        ///
2879        /// If `pullFromTopUpSource` is true, deficient value putting the position below its min health
2880        /// is pulled from the position's configured `topUpSource`.
2881        /// TODO(jord): ~150-line function - consider refactoring.
2882        access(EPosition) fun withdrawAndPull(
2883            pid: UInt64,
2884            type: Type,
2885            amount: UFix64,
2886            pullFromTopUpSource: Bool
2887        ): @{FungibleToken.Vault} {
2888            pre {
2889                !self.isPausedOrWarmup(): "Withdrawals are paused by governance"
2890                self.positions[pid] != nil:
2891                    "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool"
2892                self.globalLedger[type] != nil:
2893                    "Invalid token type \(type.identifier) - not supported by this Pool"
2894            }
2895            post {
2896                self.positionLock[pid] == nil: "Position is not unlocked"
2897            }
2898            self._lockPosition(pid)
2899            if self.debugLogging {
2900                log("    [CONTRACT] withdrawAndPull(pid: \(pid), type: \(type.identifier), amount: \(amount), pullFromTopUpSource: \(pullFromTopUpSource))")
2901            }
2902            if amount == 0.0 {
2903                self._unlockPosition(pid)
2904                return <- DeFiActionsUtils.getEmptyVault(type)
2905            }
2906
2907            // Get a reference to the user's position and global token state for the affected token.
2908            let position = self._borrowPosition(pid: pid)
2909            let tokenState = self._borrowUpdatedTokenState(type: type)
2910
2911            // Global interest indices are updated via tokenState() helper
2912
2913            // Preflight to see if the funds are available
2914            let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}?
2915            let topUpType = topUpSource?.getSourceType() ?? self.defaultToken
2916
2917            let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
2918                pid: pid,
2919                depositType: topUpType,
2920                targetHealth: position.minHealth,
2921                withdrawType: type,
2922                withdrawAmount: amount
2923            )
2924
2925            var canWithdraw = false
2926
2927            if requiredDeposit == 0.0 {
2928                // We can service this withdrawal without any top up
2929                canWithdraw = true
2930            } else if pullFromTopUpSource {
2931                // We need more funds to service this withdrawal, see if they are available from the top up source
2932                if let topUpSource = topUpSource {
2933                    // If we have to rebalance, let's try to rebalance to the target health, not just the minimum
2934                    let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
2935                        pid: pid,
2936                        depositType: topUpType,
2937                        targetHealth: position.targetHealth,
2938                        withdrawType: type,
2939                        withdrawAmount: amount
2940                    )
2941
2942                    let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit)
2943                    assert(pulledVault.getType() == topUpType, message: "topUpSource returned unexpected token type")
2944                    let pulledAmount = pulledVault.balance
2945
2946
2947                    // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here.
2948                    // The top up source may not have enough funds get us to the target health, but could have
2949                    // enough to keep us over the minimum.
2950                    if pulledAmount >= requiredDeposit {
2951                        // We can service this withdrawal if we deposit funds from our top up source
2952                        self._depositEffectsOnly(
2953                            pid: pid,
2954                            from: <-pulledVault
2955                        )
2956                        canWithdraw = true
2957                    } else {
2958                        // We can't get the funds required to service this withdrawal, so we need to redeposit what we got
2959                        self._depositEffectsOnly(
2960                            pid: pid,
2961                            from: <-pulledVault
2962                        )
2963                    }
2964                }
2965            }
2966
2967            if !canWithdraw {
2968                // Log detailed information about the failed withdrawal (only if debugging enabled)
2969                if self.debugLogging {
2970                    let availableBalance = self.availableBalance(pid: pid, type: type, pullFromTopUpSource: false)
2971                    log("    [CONTRACT] WITHDRAWAL FAILED:")
2972                    log("    [CONTRACT] Position ID: \(pid)")
2973                    log("    [CONTRACT] Token type: \(type.identifier)")
2974                    log("    [CONTRACT] Requested amount: \(amount)")
2975                    log("    [CONTRACT] Available balance (without topUp): \(availableBalance)")
2976                    log("    [CONTRACT] Required deposit for minHealth: \(requiredDeposit)")
2977                    log("    [CONTRACT] Pull from topUpSource: \(pullFromTopUpSource)")
2978                }
2979                // We can't service this withdrawal, so we just abort
2980                panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal")
2981            }
2982
2983            // If this position doesn't currently have an entry for this token, create one.
2984            if position.balances[type] == nil {
2985                position.balances[type] = InternalBalance(
2986                    direction: BalanceDirection.Credit,
2987                    scaledBalance: 0.0
2988                )
2989            }
2990
2991            let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
2992
2993            // Reflect the withdrawal in the position's balance
2994            let uintAmount = UFix128(amount)
2995            position.balances[type]!.recordWithdrawal(
2996                amount: uintAmount,
2997                tokenState: tokenState
2998            )
2999            // Attempt to pull additional collateral from the top-up source (if configured)
3000            // to keep the position above minHealth after the withdrawal.
3001            // Regardless of whether a top-up occurs, the position must be healthy post-withdrawal.
3002            let postHealth = self.positionHealth(pid: pid)
3003            assert(
3004                postHealth >= 1.0,
3005                message: "Post-withdrawal position health (\(postHealth)) is unhealthy"
3006            )
3007
3008            // Ensure that the remaining balance meets the minimum requirement (or is zero)
3009            // Building the position view does require copying the balances, so it's less efficient than accessing the balance directly.
3010            // Since most positions will have a single token type, we're okay with this for now.
3011            let positionView = self.buildPositionView(pid: pid)
3012            let remainingBalance = positionView.trueBalance(ofToken: type)
3013
3014            // This is applied to both credit and debit balances, with the main goal being to avoid dust positions.
3015            assert(
3016                remainingBalance == 0.0 || self.positionSatisfiesMinimumBalance(type: type, balance: remainingBalance),
3017                message: "Withdrawal would leave position below minimum balance requirement of \(self.globalLedger[type]!.minimumTokenBalancePerPosition). Remaining balance would be \(remainingBalance)."
3018            )
3019
3020            // Queue for update if necessary
3021            self._queuePositionForUpdateIfNecessary(pid: pid)
3022
3023            let withdrawn <- reserveVault.withdraw(amount: amount)
3024
3025            emit Withdrawn(
3026                pid: pid,
3027                poolUUID: self.uuid,
3028                vaultType: type,
3029                amount: withdrawn.balance,
3030                withdrawnUUID: withdrawn.uuid
3031            )
3032
3033            self._unlockPosition(pid)
3034            return <- withdrawn
3035        }
3036
3037        ///////////////////////
3038        // POOL MANAGEMENT
3039        ///////////////////////
3040
3041        /// Updates liquidation-related parameters
3042        access(EGovernance) fun setLiquidationParams(
3043            targetHF: UFix128,
3044        ) {
3045            assert(
3046                targetHF > 1.0,
3047                message: "targetHF must be > 1.0"
3048            )
3049            self.liquidationTargetHF = targetHF
3050            emit LiquidationParamsUpdated(
3051                poolUUID: self.uuid,
3052                targetHF: targetHF,
3053            )
3054        }
3055
3056        /// Updates pause-related parameters
3057        access(EGovernance) fun setPauseParams(
3058            warmupSec: UInt64,
3059        ) {
3060            self.warmupSec = warmupSec
3061            emit PauseParamsUpdated(
3062                poolUUID: self.uuid,
3063                warmupSec: warmupSec,
3064            )
3065        }
3066
3067        /// Updates the maximum allowed price deviation (in basis points) between the oracle and configured DEX.
3068        access(EGovernance) fun setDexOracleDeviationBps(dexOracleDeviationBps: UInt16) {
3069            pre {
3070                // TODO(jord): sanity check here?
3071            }
3072            self.dexOracleDeviationBps = dexOracleDeviationBps
3073        }
3074
3075        /// Updates the DEX (AMM) interface used for liquidations and insurance collection.
3076        ///
3077        /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens.
3078        /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: 
3079        ///   (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j
3080        ///
3081        /// FlowALPv0 does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers.
3082        /// It relies directly on the Swapper's returned by the configured SwapperProvider.
3083        access(EGovernance) fun setDEX(dex: {DeFiActions.SwapperProvider}) {
3084            self.dex = dex
3085        }
3086
3087        /// Pauses the pool, temporarily preventing further withdrawals, deposits, and liquidations
3088        access(EGovernance) fun pausePool() {
3089            if self.paused {
3090                return
3091            }
3092            self.paused = true
3093            emit PoolPaused(poolUUID: self.uuid)
3094        }
3095
3096        /// Unpauses the pool, and starts the warm-up window
3097        access(EGovernance) fun unpausePool() {
3098            if !self.paused {
3099                return
3100            }
3101            self.paused = false
3102            let now = UInt64(getCurrentBlock().timestamp)
3103            self.lastUnpausedAt = now
3104            emit PoolUnpaused(
3105                poolUUID: self.uuid,
3106                warmupEndsAt: now + self.warmupSec
3107            )
3108        }
3109
3110        /// Adds a new token type to the pool with the given parameters defining borrowing limits on collateral,
3111        /// interest accumulation, deposit rate limiting, and deposit size capacity
3112        access(EGovernance) fun addSupportedToken(
3113            tokenType: Type,
3114            collateralFactor: UFix64,
3115            borrowFactor: UFix64,
3116            interestCurve: {InterestCurve},
3117            depositRate: UFix64,
3118            depositCapacityCap: UFix64
3119        ) {
3120            pre {
3121                self.globalLedger[tokenType] == nil:
3122                    "Token type already supported"
3123                tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()):
3124                    "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation"
3125                collateralFactor > 0.0 && collateralFactor <= 1.0:
3126                    "Collateral factor must be between 0 and 1"
3127                borrowFactor > 0.0 && borrowFactor <= 1.0:
3128                    "Borrow factor must be between 0 and 1"
3129                depositRate > 0.0:
3130                    "Deposit rate must be positive"
3131                depositCapacityCap > 0.0:
3132                    "Deposit capacity cap must be positive"
3133                DeFiActionsUtils.definingContractIsFungibleToken(tokenType):
3134                    "Invalid token contract definition for tokenType \(tokenType.identifier) - defining contract is not FungibleToken conformant"
3135            }
3136
3137            // Add token to global ledger with its interest curve and deposit parameters
3138            self.globalLedger[tokenType] = TokenState(
3139                tokenType: tokenType,
3140                interestCurve: interestCurve,
3141                depositRate: depositRate,
3142                depositCapacityCap: depositCapacityCap
3143            )
3144
3145            // Set collateral factor (what percentage of value can be used as collateral)
3146            self.collateralFactor[tokenType] = collateralFactor
3147
3148            // Set borrow factor (risk adjustment for borrowed amounts)
3149            self.borrowFactor[tokenType] = borrowFactor
3150        }
3151
3152        /// Updates the insurance rate for a given token (fraction in [0,1])
3153        access(EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) {
3154            pre {
3155                self.isTokenSupported(tokenType: tokenType):
3156                    "Unsupported token type \(tokenType.identifier)"
3157                insuranceRate >= 0.0 && insuranceRate < 1.0:
3158                    "insuranceRate must be in range [0, 1)"
3159                insuranceRate + (self.getStabilityFeeRate(tokenType: tokenType) ?? 0.0) < 1.0:
3160                    "insuranceRate + stabilityFeeRate must be in range [0, 1) to avoid underflow in credit rate calculation"
3161            }
3162            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3163                ?? panic("Invariant: token state missing")
3164
3165            // Validate constraint: non-zero rate requires swapper
3166            if insuranceRate > 0.0 {
3167                assert(
3168                    tsRef.insuranceSwapper != nil, 
3169                    message:"Cannot set non-zero insurance rate without an insurance swapper configured for \(tokenType.identifier)",
3170                )
3171            }
3172            tsRef.setInsuranceRate(insuranceRate)
3173
3174            emit InsuranceRateUpdated(
3175                poolUUID: self.uuid,
3176                tokenType: tokenType.identifier,
3177                insuranceRate: insuranceRate,
3178            )
3179        }
3180
3181        /// Sets the insurance swapper for a given token type (must swap from tokenType to MOET)
3182        access(EGovernance) fun setInsuranceSwapper(tokenType: Type, swapper: {DeFiActions.Swapper}?) {
3183            pre {
3184                self.isTokenSupported(tokenType: tokenType): "Unsupported token type"
3185            }
3186            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3187                ?? panic("Invariant: token state missing")   
3188
3189            if let swapper = swapper {
3190                // Validate swapper types match
3191                assert(swapper.inType() == tokenType, message: "Swapper input type must match token type")
3192                assert(swapper.outType() == Type<@MOET.Vault>(), message: "Swapper output type must be MOET")
3193            
3194            } else {
3195                // cannot remove swapper if insurance rate > 0
3196                assert(
3197                    tsRef.insuranceRate == 0.0,
3198                    message: "Cannot remove insurance swapper while insurance rate is non-zero for \(tokenType.identifier)"
3199                )
3200            }
3201
3202            tsRef.setInsuranceSwapper(swapper)
3203        }
3204
3205        /// Manually triggers insurance collection for a given token type.
3206        /// This is useful for governance to collect accrued insurance on-demand.
3207        /// Insurance is calculated based on time elapsed since last collection.
3208        access(EGovernance) fun collectInsurance(tokenType: Type) {
3209            pre {
3210                self.isTokenSupported(tokenType: tokenType): "Unsupported token type"
3211            }
3212            self.updateInterestRatesAndCollectInsurance(tokenType: tokenType)
3213        }
3214
3215        /// Updates the per-deposit limit fraction for a given token (fraction in [0,1])
3216        access(EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) {
3217            pre {
3218                self.isTokenSupported(tokenType: tokenType):
3219                    "Unsupported token type \(tokenType.identifier)"
3220                fraction > 0.0 && fraction <= 1.0:
3221                    "fraction must be in (0,1]"
3222            }
3223            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3224                ?? panic("Invariant: token state missing")
3225            tsRef.setDepositLimitFraction(fraction)
3226        }
3227
3228        /// Updates the deposit rate for a given token (tokens per hour)
3229        access(EGovernance) fun setDepositRate(tokenType: Type, hourlyRate: UFix64) {
3230            pre {
3231                self.isTokenSupported(tokenType: tokenType): "Unsupported token type"
3232            }
3233            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3234                ?? panic("Invariant: token state missing")
3235            tsRef.setDepositRate(hourlyRate)
3236        }
3237
3238        /// Updates the deposit capacity cap for a given token
3239        access(EGovernance) fun setDepositCapacityCap(tokenType: Type, cap: UFix64) {
3240            pre {
3241                self.isTokenSupported(tokenType: tokenType): "Unsupported token type"
3242            }
3243            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3244                ?? panic("Invariant: token state missing")
3245            tsRef.setDepositCapacityCap(cap)
3246        }
3247
3248        /// Updates the minimum token balance per position for a given token
3249        access(EGovernance) fun setMinimumTokenBalancePerPosition(tokenType: Type, minimum: UFix64) {
3250            pre {
3251                self.isTokenSupported(tokenType: tokenType): "Unsupported token type"
3252            }
3253            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3254                ?? panic("Invariant: token state missing")
3255            tsRef.setMinimumTokenBalancePerPosition(minimum)
3256        }
3257
3258        /// Updates the stability fee rate for a given token (fraction in [0,1]).
3259        ///
3260        /// @param tokenTypeIdentifier: The fully qualified type identifier of the token (e.g., "A.0x1.FlowToken.Vault")
3261        /// @param stabilityFeeRate: The fee rate as a fraction in [0, 1]
3262        ///
3263        ///
3264        /// Emits: StabilityFeeRateUpdated
3265        access(EGovernance) fun setStabilityFeeRate(tokenType: Type, stabilityFeeRate: UFix64) {
3266            pre {
3267                self.isTokenSupported(tokenType: tokenType):
3268                    "Unsupported token type \(tokenType.identifier)"
3269                stabilityFeeRate >= 0.0 && stabilityFeeRate < 1.0:
3270                    "stability fee rate must be in range [0, 1)"
3271                stabilityFeeRate + (self.getInsuranceRate(tokenType: tokenType) ?? 0.0) < 1.0:
3272                    "stabilityFeeRate + insuranceRate must be in range [0, 1) to avoid underflow in credit rate calculation"
3273            }
3274            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3275                ?? panic("Invariant: token state missing")
3276            tsRef.setStabilityFeeRate(stabilityFeeRate)
3277            
3278            emit StabilityFeeRateUpdated(
3279                poolUUID: self.uuid,
3280                tokenType: tokenType.identifier,
3281                stabilityFeeRate: stabilityFeeRate,
3282            )
3283        }
3284
3285        /// Withdraws stability funds collected from the stability fee for a given token
3286        ///
3287        /// Emits: StabilityFundWithdrawn
3288        access(EGovernance) fun withdrawStabilityFund(tokenType: Type, amount: UFix64, recipient: &{FungibleToken.Receiver}) {
3289            pre {
3290                self.stabilityFunds[tokenType] != nil: "No stability fund exists for token type \(tokenType.identifier)"
3291                amount > 0.0: "Withdrawal amount must be positive"
3292            }
3293            let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
3294            assert(
3295                fundRef.balance >= amount,
3296                message: "Insufficient stability fund balance. Available: \(fundRef.balance), requested: \(amount)"
3297            )
3298            
3299            let withdrawn <- fundRef.withdraw(amount: amount)
3300            recipient.deposit(from: <-withdrawn)
3301
3302            emit StabilityFundWithdrawn(
3303                poolUUID: self.uuid,
3304                tokenType: tokenType.identifier,
3305                amount: amount,
3306            )
3307        }
3308
3309        /// Manually triggers fee collection for a given token type.
3310        /// This is useful for governance to collect accrued stability on-demand.
3311        /// Fee is calculated based on time elapsed since last collection.
3312        access(EGovernance) fun collectStability(tokenType: Type) {
3313            pre {
3314                self.isTokenSupported(tokenType: tokenType): "Unsupported token type"
3315            }
3316            self.updateInterestRatesAndCollectStability(tokenType: tokenType)
3317        }
3318
3319        /// Regenerates deposit capacity for all supported token types
3320        /// Each token type's capacity regenerates independently based on its own depositRate,
3321        /// approximately once per hour, up to its respective depositCapacityCap
3322        /// When capacity regenerates, user deposit usage is reset for that token type
3323        access(EImplementation) fun regenerateAllDepositCapacities() {
3324            for tokenType in self.globalLedger.keys {
3325                let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
3326                    ?? panic("Invariant: token state missing")
3327                tsRef.regenerateDepositCapacity()
3328            }
3329        }
3330
3331        /// Updates the interest curve for a given token
3332        /// This allows governance to change the interest rate model for a token after it has been added
3333        /// to the pool. For example, switching from a fixed rate to a kink-based model, or updating
3334        /// the parameters of an existing kink model.
3335        ///
3336        /// Important: Before changing the curve, we must first compound any accrued interest at the
3337        /// OLD rate. Otherwise, interest that accrued since lastUpdate would be calculated using the
3338        /// new rate, which would be incorrect.
3339        access(EGovernance) fun setInterestCurve(tokenType: Type, interestCurve: {InterestCurve}) {
3340            pre {
3341                self.isTokenSupported(tokenType: tokenType): "Unsupported token type"
3342            }
3343            // First, update interest indices to compound any accrued interest at the OLD rate
3344            // This "finalizes" all interest accrued up to this moment before switching curves
3345            let tsRef = self._borrowUpdatedTokenState(type: tokenType)
3346            // Now safe to set the new curve - subsequent interest will accrue at the new rate
3347            tsRef.setInterestCurve(interestCurve)
3348            emit InterestCurveUpdated(
3349                poolUUID: self.uuid,
3350                tokenType: tokenType.identifier,
3351                curveType: interestCurve.getType().identifier
3352            )
3353        }
3354
3355        /// Enables or disables verbose logging inside the Pool for testing and diagnostics
3356        access(EGovernance) fun setDebugLogging(_ enabled: Bool) {
3357            self.debugLogging = enabled
3358        }
3359
3360        /// Rebalances the position to the target health value, if the position is under- or over-collateralized,
3361        /// as defined by the position-specific min/max health thresholds.
3362        /// If force=true, the position will be rebalanced regardless of its current health.
3363        ///
3364        /// When rebalancing, funds are withdrawn from the position's topUpSource or deposited to its drawDownSink.
3365        /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source,
3366        /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will
3367        /// not cause the position to reach its target health.
3368        access(EPosition | ERebalance) fun rebalancePosition(pid: UInt64, force: Bool) {
3369            pre {
3370                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
3371            }
3372            post {
3373                self.positionLock[pid] == nil: "Position is not unlocked"
3374            }
3375            self._lockPosition(pid)
3376            self._rebalancePositionNoLock(pid: pid, force: force)
3377            self._unlockPosition(pid)
3378        }
3379
3380        /// Attempts to rebalance a position toward its configured `targetHealth` without acquiring
3381        /// or releasing the position lock. This function performs *best-effort* rebalancing and may
3382        /// partially rebalance or no-op depending on available sinks/sources and their capacity.
3383        ///
3384        /// This helper is intentionally "no-lock" and "effects-only" with respect to orchestration.
3385        /// Callers are responsible for acquiring and releasing the position lock and for enforcing
3386        /// any higher-level invariants.
3387        access(self) fun _rebalancePositionNoLock(pid: UInt64, force: Bool) {
3388            pre {
3389                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
3390            }
3391            if self.debugLogging {
3392                log("    [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))")
3393            }
3394            let position = self._borrowPosition(pid: pid)
3395            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
3396
3397            if !force && (position.minHealth <= balanceSheet.health && balanceSheet.health <= position.maxHealth) {
3398                // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do!
3399                return
3400            }
3401
3402            if balanceSheet.health < position.targetHealth {
3403                // The position is undercollateralized,
3404                // see if the source can get more collateral to bring it up to the target health.
3405                if let topUpSource = position.topUpSource {
3406                    let topUpSource = topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}
3407                    let idealDeposit = self.fundsRequiredForTargetHealth(
3408                        pid: pid,
3409                        type: topUpSource.getSourceType(),
3410                        targetHealth: position.targetHealth
3411                    )
3412                    if self.debugLogging {
3413                        log("    [CONTRACT] idealDeposit: \(idealDeposit)")
3414                    }
3415
3416                    let topUpType = topUpSource.getSourceType()
3417                    let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit)
3418                    assert(pulledVault.getType() == topUpType, message: "topUpSource returned unexpected token type")
3419
3420                    emit Rebalanced(
3421                        pid: pid,
3422                        poolUUID: self.uuid,
3423                        atHealth: balanceSheet.health,
3424                        amount: pulledVault.balance,
3425                        fromUnder: true
3426                        )
3427
3428                    self._depositEffectsOnly(
3429                        pid: pid,
3430                        from: <-pulledVault,
3431                    )
3432                }
3433            } else if balanceSheet.health > position.targetHealth {
3434                // The position is overcollateralized,
3435                // we'll withdraw funds to match the target health and offer it to the sink.
3436                if self.isPausedOrWarmup() {
3437                    // Withdrawals (including pushing to the drawDownSink) are disabled during the warmup period
3438                    return
3439                }
3440                if let drawDownSink = position.drawDownSink {
3441                    let drawDownSink = drawDownSink as auth(FungibleToken.Withdraw) &{DeFiActions.Sink}
3442                    let sinkType = drawDownSink.getSinkType()
3443                    let idealWithdrawal = self.fundsAvailableAboveTargetHealth(
3444                        pid: pid,
3445                        type: sinkType,
3446                        targetHealth: position.targetHealth
3447                    )
3448                    if self.debugLogging {
3449                        log("    [CONTRACT] idealWithdrawal: \(idealWithdrawal)")
3450                    }
3451
3452                    // Compute how many tokens of the sink's type are available to hit our target health.
3453                    let sinkCapacity = drawDownSink.minimumCapacity()
3454                    let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal
3455
3456                    // TODO(jord): we enforce in setDrawDownSink that the type is MOET -> we should panic here if that does not hold (currently silently fail)
3457                    if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() {
3458                        let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>())
3459                        if position.balances[Type<@MOET.Vault>()] == nil {
3460                            position.balances[Type<@MOET.Vault>()] = InternalBalance(
3461                                direction: BalanceDirection.Credit,
3462                                scaledBalance: 0.0
3463                            )
3464                        }
3465                        // record the withdrawal and mint the tokens
3466                        let uintSinkAmount = UFix128(sinkAmount)
3467                        position.balances[Type<@MOET.Vault>()]!.recordWithdrawal(
3468                            amount: uintSinkAmount,
3469                            tokenState: tokenState
3470                        )
3471                        let sinkVault <- FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount)
3472
3473                        emit Rebalanced(
3474                            pid: pid,
3475                            poolUUID: self.uuid,
3476                            atHealth: balanceSheet.health,
3477                            amount: sinkVault.balance,
3478                            fromUnder: false
3479                        )
3480
3481                        // Push what we can into the sink, and redeposit the rest
3482                        drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
3483                        if sinkVault.balance > 0.0 {
3484                            self._depositEffectsOnly(
3485                                pid: pid,
3486                                from: <-sinkVault,
3487                            )
3488                        } else {
3489                            Burner.burn(<-sinkVault)
3490                        }
3491                    }
3492                }
3493            }
3494
3495        }
3496
3497        /// Executes asynchronous updates on positions that have been queued up to the lesser of the queue length or
3498        /// the configured positionsProcessedPerCallback value
3499        access(EImplementation) fun asyncUpdate() {
3500            pre {
3501                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
3502            }
3503            // TODO: In the production version, this function should only process some positions (limited by positionsProcessedPerCallback) AND
3504            // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or
3505            // sink aborts) won't prevent other positions from being updated.
3506            var processed: UInt64 = 0
3507            while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback {
3508                let pid = self.positionsNeedingUpdates.removeFirst()
3509                self.asyncUpdatePosition(pid: pid)
3510                self._queuePositionForUpdateIfNecessary(pid: pid)
3511                processed = processed + 1
3512            }
3513        }
3514
3515        /// Executes an asynchronous update on the specified position
3516        access(EImplementation) fun asyncUpdatePosition(pid: UInt64) {
3517            pre {
3518                !self.isPaused(): "Withdrawal, deposits, and liquidations are paused by governance"
3519            }
3520            post {
3521                self.positionLock[pid] == nil: "Position is not unlocked"
3522            }
3523            self._lockPosition(pid)
3524            let position = self._borrowPosition(pid: pid)
3525
3526            // store types to avoid iterating while mutating
3527            let depositTypes = position.queuedDeposits.keys
3528            // First check queued deposits, their addition could affect the rebalance we attempt later
3529            for depositType in depositTypes {
3530                let queuedVault <- position.queuedDeposits.remove(key: depositType)!
3531                let queuedAmount = queuedVault.balance
3532                let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
3533                let maxDeposit = depositTokenState.depositLimit()
3534
3535                if maxDeposit >= queuedAmount {
3536                    // We can deposit all of the queued deposit, so just do it and remove it from the queue
3537
3538                    self._depositEffectsOnly(pid: pid, from: <-queuedVault)
3539                } else {
3540                    // We can only deposit part of the queued deposit, so do that and leave the rest in the queue
3541                    // for the next time we run.
3542                    let depositVault <- queuedVault.withdraw(amount: maxDeposit)
3543                    self._depositEffectsOnly(pid: pid, from: <-depositVault)
3544
3545                    // We need to update the queued vault to reflect the amount we used up
3546                    if let existing <- position.queuedDeposits.remove(key: depositType) {
3547                        existing.deposit(from: <-queuedVault)
3548                        position.queuedDeposits[depositType] <-! existing
3549                    } else {
3550                        position.queuedDeposits[depositType] <-! queuedVault
3551                    }
3552                }
3553            }
3554
3555            // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance
3556            // the position if necessary.
3557            self._rebalancePositionNoLock(pid: pid, force: false)
3558            self._unlockPosition(pid)
3559        }
3560
3561        /// Updates interest rates for a token and collects stability fee.
3562        /// This method should be called periodically to ensure rates are current and fee amounts are collected.
3563        ///
3564        /// @param tokenType: The token type to update rates for
3565        access(self) fun updateInterestRatesAndCollectStability(tokenType: Type) {
3566            let tokenState = self._borrowUpdatedTokenState(type: tokenType)
3567            tokenState.updateInterestRates()
3568
3569            // Ensure reserves exist for this token type
3570            if self.reserves[tokenType] == nil {
3571                return
3572            }
3573
3574            // Get reference to reserves
3575            let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
3576
3577            // Collect stability and get token vault
3578            if let collectedVault <- tokenState.collectStability(reserveVault: reserveRef) {  
3579                let collectedBalance = collectedVault.balance     
3580                // Deposit collected token into stability fund
3581                if self.stabilityFunds[tokenType] == nil {
3582                    self.stabilityFunds[tokenType] <-! collectedVault
3583                } else {
3584                    let fundRef = (&self.stabilityFunds[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
3585                    fundRef.deposit(from: <-collectedVault)
3586                }
3587                
3588                emit StabilityFeeCollected(
3589                    poolUUID: self.uuid,
3590                    tokenType: tokenType.identifier,
3591                    stabilityAmount: collectedBalance,
3592                    collectionTime: tokenState.lastStabilityFeeCollectionTime
3593                )
3594            }
3595        }
3596
3597        ////////////////
3598        // INTERNAL
3599        ////////////////
3600
3601        /// Queues a position for asynchronous updates if the position has been marked as requiring an update
3602        access(self) fun _queuePositionForUpdateIfNecessary(pid: UInt64) {
3603            if self.positionsNeedingUpdates.contains(pid) {
3604                // If this position is already queued for an update, no need to check anything else
3605                return
3606            }
3607
3608            // If this position is not already queued for an update, we need to check if it needs one
3609            let position = self._borrowPosition(pid: pid)
3610
3611            if position.queuedDeposits.length > 0 {
3612                // This position has deposits that need to be processed, so we need to queue it for an update
3613                self.positionsNeedingUpdates.append(pid)
3614                return
3615            }
3616
3617            let positionHealth = self.positionHealth(pid: pid)
3618
3619            if positionHealth < position.minHealth || positionHealth > position.maxHealth {
3620                // This position is outside the configured health bounds, we queue it for an update
3621                self.positionsNeedingUpdates.append(pid)
3622                return
3623            }
3624        }
3625
3626        /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health
3627        /// TODO(jord): in all cases callers already are calling _borrowPosition, more efficient to pass in PositionView?
3628        access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet {
3629            let position = self._borrowPosition(pid: pid)
3630
3631            // Get the position's collateral and debt values in terms of the default token.
3632            var effectiveCollateral: UFix128 = 0.0
3633            var effectiveDebt: UFix128 = 0.0
3634
3635            for type in position.balances.keys {
3636                let balance = position.balances[type]!
3637                let tokenState = self._borrowUpdatedTokenState(type: type)
3638
3639                switch balance.direction {
3640                    case BalanceDirection.Credit:
3641                        let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
3642                            balance.scaledBalance,
3643                            interestIndex: tokenState.creditInterestIndex
3644                        )
3645
3646                        let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!)
3647                        let value = convertedPrice * trueBalance
3648
3649                        let convertedCollateralFactor = UFix128(self.collateralFactor[type]!)
3650                        effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor)
3651
3652                    case BalanceDirection.Debit:
3653                        let trueBalance = FlowALPv0.scaledBalanceToTrueBalance(
3654                            balance.scaledBalance,
3655                            interestIndex: tokenState.debitInterestIndex
3656                        )
3657
3658                        let convertedPrice = UFix128(self.priceOracle.price(ofToken: type)!)
3659                        let value = convertedPrice * trueBalance
3660
3661                        let convertedBorrowFactor = UFix128(self.borrowFactor[type]!)
3662                        effectiveDebt = effectiveDebt + (value / convertedBorrowFactor)
3663
3664                }
3665            }
3666
3667            return BalanceSheet(
3668                effectiveCollateral: effectiveCollateral,
3669                effectiveDebt: effectiveDebt
3670            )
3671        }
3672
3673        /// A convenience function that returns a reference to a particular token state, making sure it's up-to-date for
3674        /// the passage of time. This should always be used when accessing a token state to avoid missing interest
3675        /// updates (duplicate calls to updateForTimeChange() are a nop within a single block).
3676        access(self) fun _borrowUpdatedTokenState(type: Type): auth(EImplementation) &TokenState {
3677            let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState
3678            state.updateForTimeChange()
3679            return state
3680        }
3681
3682        /// Updates interest rates for a token and collects insurance if a swapper is configured for the token.
3683        /// This method should be called periodically to ensure rates are current and insurance is collected.
3684        ///
3685        /// @param tokenType: The token type to update rates for
3686        access(self) fun updateInterestRatesAndCollectInsurance(tokenType: Type) {
3687            let tokenState = self._borrowUpdatedTokenState(type: tokenType)
3688            tokenState.updateInterestRates()
3689            
3690            // Collect insurance if swapper is configured
3691            // Ensure reserves exist for this token type
3692            if self.reserves[tokenType] == nil {
3693                return
3694            }
3695
3696            // Get reference to reserves
3697            if let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?) {
3698                // Collect insurance and get MOET vault
3699                let oraclePrice = self.priceOracle.price(ofToken: tokenType)!
3700                if let collectedMOET <- tokenState.collectInsurance(
3701                    reserveVault: reserveRef,
3702                    oraclePrice: oraclePrice,
3703                    maxDeviationBps: self.dexOracleDeviationBps
3704                ) {
3705                    let collectedMOETBalance = collectedMOET.balance
3706                    // Deposit collected MOET into insurance fund
3707                    self.insuranceFund.deposit(from: <-collectedMOET)
3708
3709                    emit InsuranceFeeCollected(
3710                        poolUUID: self.uuid,
3711                        tokenType: tokenType.identifier,
3712                        insuranceAmount: collectedMOETBalance,
3713                        collectionTime: tokenState.lastInsuranceCollectionTime
3714                    )
3715                }
3716            }
3717        }
3718
3719        /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist
3720        access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition {
3721            return &self.positions[pid] as auth(EImplementation) &InternalPosition?
3722                ?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool")
3723        }
3724
3725        /// Returns an authorized reference to the InternalPosition for the given position ID.
3726        /// Used by Position resources to directly access their InternalPosition.
3727        access(EPosition) view fun borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition {
3728            return self._borrowPosition(pid: pid)
3729        }
3730
3731        /// Build a PositionView for the given position ID.
3732        access(all) fun buildPositionView(pid: UInt64): FlowALPv0.PositionView {
3733            let position = self._borrowPosition(pid: pid)
3734            let snaps: {Type: FlowALPv0.TokenSnapshot} = {}
3735            let balancesCopy = position.copyBalances()
3736            for t in position.balances.keys {
3737                let tokenState = self._borrowUpdatedTokenState(type: t)
3738                snaps[t] = FlowALPv0.TokenSnapshot(
3739                    price: UFix128(self.priceOracle.price(ofToken: t)!),
3740                    credit: tokenState.creditInterestIndex,
3741                    debit: tokenState.debitInterestIndex,
3742                    risk: FlowALPv0.RiskParams(
3743                        collateralFactor: UFix128(self.collateralFactor[t]!),
3744                        borrowFactor: UFix128(self.borrowFactor[t]!),
3745                    )
3746                )
3747            }
3748            return FlowALPv0.PositionView(
3749                balances: balancesCopy,
3750                snapshots: snaps,
3751                defaultToken: self.defaultToken,
3752                min: position.minHealth,
3753                max: position.maxHealth
3754            )
3755        }
3756
3757        access(EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) {
3758            pre {
3759                newOracle.unitOfAccount() == self.defaultToken:
3760                    "Price oracle must return prices in terms of the pool's default token"
3761            }
3762            self.priceOracle = newOracle
3763            self.positionsNeedingUpdates = self.positions.keys
3764
3765            emit PriceOracleUpdated(
3766                poolUUID: self.uuid,
3767                newOracleType: newOracle.getType().identifier
3768            )
3769        }
3770
3771        access(all) fun getDefaultToken(): Type {
3772            return self.defaultToken
3773        }
3774        
3775        /// Returns the deposit capacity and deposit capacity cap for a given token type
3776        access(all) fun getDepositCapacityInfo(type: Type): {String: UFix64} {
3777            let tokenState = self._borrowUpdatedTokenState(type: type)
3778            return {
3779                "depositCapacity": tokenState.depositCapacity,
3780                "depositCapacityCap": tokenState.depositCapacityCap,
3781                "depositRate": tokenState.depositRate,
3782                "depositLimitFraction": tokenState.depositLimitFraction,
3783                "lastDepositCapacityUpdate": tokenState.lastDepositCapacityUpdate
3784            }
3785        }
3786    }
3787
3788    /// PoolFactory
3789    ///
3790    /// Resource enabling the contract account to create the contract's Pool. This pattern is used in place of contract
3791    /// methods to ensure limited access to pool creation. While this could be done in contract's init, doing so here
3792    /// will allow for the setting of the Pool's PriceOracle without the introduction of a concrete PriceOracle defining
3793    /// contract which would include an external contract dependency.
3794    ///
3795    access(all) resource PoolFactory {
3796        /// Creates the contract-managed Pool and saves it to the canonical path, reverting if one is already stored
3797        access(all) fun createPool(
3798        	defaultToken: Type,
3799        	priceOracle: {DeFiActions.PriceOracle},
3800        	dex: {DeFiActions.SwapperProvider}
3801        ) {
3802            pre {
3803                FlowALPv0.account.storage.type(at: FlowALPv0.PoolStoragePath) == nil:
3804                    "Storage collision - Pool has already been created & saved to \(FlowALPv0.PoolStoragePath)"
3805            }
3806            let pool <- create Pool(
3807            	defaultToken: defaultToken,
3808            	priceOracle: priceOracle,
3809            	dex: dex
3810            )
3811            FlowALPv0.account.storage.save(<-pool, to: FlowALPv0.PoolStoragePath)
3812            let cap = FlowALPv0.account.capabilities.storage.issue<&Pool>(FlowALPv0.PoolStoragePath)
3813            FlowALPv0.account.capabilities.unpublish(FlowALPv0.PoolPublicPath)
3814            FlowALPv0.account.capabilities.publish(cap, at: FlowALPv0.PoolPublicPath)
3815        }
3816    }
3817
3818    /// Position
3819    ///
3820    /// A Position is a resource representing ownership of value deposited to the protocol.
3821    /// From a Position, a user can deposit and withdraw funds as well as construct DeFiActions components enabling
3822    /// value flows in and out of the Position from within the context of DeFiActions stacks.
3823    /// Unauthorized Position references allow depositing only, and are considered safe to publish.
3824    /// The EPositionAdmin entitlement protects sensitive withdrawal and configuration methods.
3825    ///
3826    /// Position resources are held in user accounts and provide access to one position (by pid).
3827    /// Clients are recommended to use PositionManager to manage access to Positions.
3828    ///
3829    access(all) resource Position {
3830
3831        /// The unique ID of the Position used to track deposits and withdrawals to the Pool
3832        access(all) let id: UInt64
3833
3834        /// An authorized Capability to the Pool for which this Position was opened.
3835        access(self) let pool: Capability<auth(EPosition) &Pool>
3836
3837        init(
3838            id: UInt64,
3839            pool: Capability<auth(EPosition) &Pool>
3840        ) {
3841            pre {
3842                pool.check():
3843                    "Invalid Pool Capability provided - cannot construct Position"
3844            }
3845            self.id = id
3846            self.pool = pool
3847        }
3848
3849        /// Returns the balances (both positive and negative) for all tokens in this position.
3850        access(all) fun getBalances(): [PositionBalance] {
3851            let pool = self.pool.borrow()!
3852            return pool.getPositionDetails(pid: self.id).balances
3853        }
3854
3855        /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the
3856        /// calculation will be made assuming the position is topped up if the withdrawal amount puts the Position
3857        /// below its min health. If pullFromTopUpSource is false, the calculation will return the balance currently
3858        /// available without topping up the position.
3859        access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 {
3860            let pool = self.pool.borrow()!
3861            return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource)
3862        }
3863
3864        /// Returns the current health of the position
3865        access(all) fun getHealth(): UFix128 {
3866            let pool = self.pool.borrow()!
3867            return pool.positionHealth(pid: self.id)
3868        }
3869
3870        /// Returns the Position's target health (unitless ratio ≥ 1.0)
3871        access(all) fun getTargetHealth(): UFix64 {
3872            let pool = self.pool.borrow()!
3873            let pos = pool.borrowPosition(pid: self.id)
3874            return FlowALPMath.toUFix64Round(pos.targetHealth)
3875        }
3876
3877        /// Sets the target health of the Position
3878        access(EPositionAdmin) fun setTargetHealth(targetHealth: UFix64) {
3879            let pool = self.pool.borrow()!
3880            let pos = pool.borrowPosition(pid: self.id)
3881            pos.setTargetHealth(UFix128(targetHealth))
3882        }
3883
3884        /// Returns the minimum health of the Position
3885        access(all) fun getMinHealth(): UFix64 {
3886            let pool = self.pool.borrow()!
3887            let pos = pool.borrowPosition(pid: self.id)
3888            return FlowALPMath.toUFix64Round(pos.minHealth)
3889        }
3890
3891        /// Sets the minimum health of the Position
3892        access(EPositionAdmin) fun setMinHealth(minHealth: UFix64) {
3893            let pool = self.pool.borrow()!
3894            let pos = pool.borrowPosition(pid: self.id)
3895            pos.setMinHealth(UFix128(minHealth))
3896        }
3897
3898        /// Returns the maximum health of the Position
3899        access(all) fun getMaxHealth(): UFix64 {
3900            let pool = self.pool.borrow()!
3901            let pos = pool.borrowPosition(pid: self.id)
3902            return FlowALPMath.toUFix64Round(pos.maxHealth)
3903        }
3904
3905        /// Sets the maximum health of the position
3906        access(EPositionAdmin) fun setMaxHealth(maxHealth: UFix64) {
3907            let pool = self.pool.borrow()!
3908            let pos = pool.borrowPosition(pid: self.id)
3909            pos.setMaxHealth(UFix128(maxHealth))
3910        }
3911
3912        /// Returns the maximum amount of the given token type that could be deposited into this position
3913        access(all) fun getDepositCapacity(type: Type): UFix64 {
3914            // There's no limit on deposits from the position's perspective
3915            return UFix64.max
3916        }
3917
3918        /// Deposits funds to the Position without immediately pushing to the drawDownSink if the deposit puts the Position above its maximum health.
3919        /// NOTE: Anyone is allowed to deposit to any position.
3920        access(all) fun deposit(from: @{FungibleToken.Vault}) {
3921            self.depositAndPush(
3922                from: <-from,
3923                pushToDrawDownSink: false
3924            )
3925        }
3926
3927        /// Deposits funds to the Position enabling the caller to configure whether excess value
3928        /// should be pushed to the drawDownSink if the deposit puts the Position above its maximum health
3929        /// NOTE: Anyone is allowed to deposit to any position.
3930        access(all) fun depositAndPush(
3931            from: @{FungibleToken.Vault},
3932            pushToDrawDownSink: Bool
3933        ) {
3934            let pool = self.pool.borrow()!
3935            pool.depositAndPush(
3936                pid: self.id,
3937                from: <-from,
3938                pushToDrawDownSink: pushToDrawDownSink
3939            )
3940        }
3941
3942        /// Withdraws funds from the Position without pulling from the topUpSource
3943        /// if the withdrawal puts the Position below its minimum health
3944        access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} {
3945            return <- self.withdrawAndPull(
3946                type: type,
3947                amount: amount,
3948                pullFromTopUpSource: false
3949            )
3950        }
3951
3952        /// Withdraws funds from the Position enabling the caller to configure whether insufficient value
3953        /// should be pulled from the topUpSource if the withdrawal puts the Position below its minimum health
3954        access(FungibleToken.Withdraw) fun withdrawAndPull(
3955            type: Type,
3956            amount: UFix64,
3957            pullFromTopUpSource: Bool
3958        ): @{FungibleToken.Vault} {
3959            let pool = self.pool.borrow()!
3960            return <- pool.withdrawAndPull(
3961                pid: self.id,
3962                type: type,
3963                amount: amount,
3964                pullFromTopUpSource: pullFromTopUpSource
3965            )
3966        }
3967
3968        /// Returns a new Sink for the given token type that will accept deposits of that token
3969        /// and update the position's collateral and/or debt accordingly.
3970        ///
3971        /// Note that calling this method multiple times will create multiple sinks,
3972        /// each of which will continue to work regardless of how many other sinks have been created.
3973        access(all) fun createSink(type: Type): {DeFiActions.Sink} {
3974            // create enhanced sink with pushToDrawDownSink option
3975            return self.createSinkWithOptions(
3976                type: type,
3977                pushToDrawDownSink: false
3978            )
3979        }
3980
3981        /// Returns a new Sink for the given token type and pushToDrawDownSink option
3982        /// that will accept deposits of that token and update the position's collateral and/or debt accordingly.
3983        ///
3984        /// Note that calling this method multiple times will create multiple sinks,
3985        /// each of which will continue to work regardless of how many other sinks have been created.
3986        access(all) fun createSinkWithOptions(
3987            type: Type,
3988            pushToDrawDownSink: Bool
3989        ): {DeFiActions.Sink} {
3990            let pool = self.pool.borrow()!
3991            return PositionSink(
3992                id: self.id,
3993                pool: self.pool,
3994                type: type,
3995                pushToDrawDownSink: pushToDrawDownSink
3996            )
3997        }
3998
3999        /// Returns a new Source for the given token type that will service withdrawals of that token
4000        /// and update the position's collateral and/or debt accordingly.
4001        ///
4002        /// Note that calling this method multiple times will create multiple sources,
4003        /// each of which will continue to work regardless of how many other sources have been created.
4004        access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source} {
4005            // Create source with pullFromTopUpSource = false
4006            return self.createSourceWithOptions(
4007                type: type,
4008                pullFromTopUpSource: false
4009            )
4010        }
4011
4012        /// Returns a new Source for the given token type and pullFromTopUpSource option
4013        /// that will service withdrawals of that token and update the position's collateral and/or debt accordingly.
4014        ///
4015        /// Note that calling this method multiple times will create multiple sources,
4016        /// each of which will continue to work regardless of how many other sources have been created.
4017        access(FungibleToken.Withdraw) fun createSourceWithOptions(
4018            type: Type,
4019            pullFromTopUpSource: Bool
4020        ): {DeFiActions.Source} {
4021            let pool = self.pool.borrow()!
4022            return PositionSource(
4023                id: self.id,
4024                pool: self.pool,
4025                type: type,
4026                pullFromTopUpSource: pullFromTopUpSource
4027            )
4028        }
4029
4030        /// Provides a sink to the Position that will have tokens proactively pushed into it
4031        /// when the position has excess collateral.
4032        /// (Remember that sinks do NOT have to accept all tokens provided to them;
4033        /// the sink can choose to accept only some (or none) of the tokens provided,
4034        /// leaving the position overcollateralized).
4035        ///
4036        /// Each position can have only one sink, and the sink must accept the default token type
4037        /// configured for the pool. Providing a new sink will replace the existing sink.
4038        ///
4039        /// Pass nil to configure the position to not push tokens when the Position exceeds its maximum health.
4040        access(EPositionAdmin) fun provideSink(sink: {DeFiActions.Sink}?) {
4041            let pool = self.pool.borrow()!
4042            pool.lockPosition(self.id)
4043            let pos = pool.borrowPosition(pid: self.id)
4044            pos.setDrawDownSink(sink)
4045            pool.unlockPosition(self.id)
4046        }
4047
4048        /// Provides a source to the Position that will have tokens proactively pulled from it
4049        /// when the position has insufficient collateral.
4050        /// If the source can cover the position's debt, the position will not be liquidated.
4051        ///
4052        /// Each position can have only one source, and the source must accept the default token type
4053        /// configured for the pool. Providing a new source will replace the existing source.
4054        ///
4055        /// Pass nil to configure the position to not pull tokens.
4056        access(EPositionAdmin) fun provideSource(source: {DeFiActions.Source}?) {
4057            let pool = self.pool.borrow()!
4058            pool.lockPosition(self.id)
4059            let pos = pool.borrowPosition(pid: self.id)
4060            pos.setTopUpSource(source)
4061            pool.unlockPosition(self.id)
4062        }
4063
4064        /// Rebalances the position to the target health value, if the position is under- or over-collateralized,
4065        /// as defined by the position-specific min/max health thresholds.
4066        /// If force=true, the position will be rebalanced regardless of its current health.
4067        ///
4068        /// When rebalancing, funds are withdrawn from the position's topUpSource or deposited to its drawDownSink.
4069        /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source,
4070        /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will
4071        /// not cause the position to reach its target health.
4072        access(EPosition | ERebalance) fun rebalance(force: Bool) {
4073            let pool = self.pool.borrow()!
4074            pool.rebalancePosition(pid: self.id, force: force)
4075        }
4076    }
4077
4078    /// PositionManager
4079    ///
4080    /// A collection resource that manages multiple Position resources for an account.
4081    /// This allows users to have multiple positions while using a single, constant storage path.
4082    ///
4083    access(all) resource PositionManager {
4084
4085        /// Dictionary storing all positions owned by this manager, keyed by position ID
4086        access(self) let positions: @{UInt64: Position}
4087
4088        init() {
4089            self.positions <- {}
4090        }
4091
4092        /// Adds a new position to the manager.
4093        access(EPositionAdmin) fun addPosition(position: @Position) {
4094            let pid = position.id
4095            let old <- self.positions[pid] <- position
4096            if old != nil {
4097                panic("Cannot add position with same pid (\(pid)) as existing position: must explicitly remove existing position first")
4098            }
4099            destroy old
4100        }
4101
4102        /// Removes and returns a position from the manager.
4103        access(EPositionAdmin) fun removePosition(pid: UInt64): @Position {
4104            if let position <- self.positions.remove(key: pid) {
4105                return <-position
4106            }
4107            panic("Position with pid=\(pid) not found in PositionManager")
4108        }
4109
4110        /// Internal method that returns a reference to a position authorized with all entitlements.
4111        /// Callers who wish to provide a partially authorized reference can downcast the result as needed.
4112        access(EPositionAdmin) fun borrowAuthorizedPosition(pid: UInt64): auth(FungibleToken.Withdraw, EPositionAdmin) &Position {
4113            return (&self.positions[pid] as auth(FungibleToken.Withdraw, EPositionAdmin) &Position?)
4114                ?? panic("Position with pid=\(pid) not found in PositionManager")
4115        }
4116
4117        /// Returns a public reference to a position with no entitlements.
4118        access(all) fun borrowPosition(pid: UInt64): &Position {
4119            return (&self.positions[pid] as &Position?)
4120                ?? panic("Position with pid=\(pid) not found in PositionManager")
4121        }
4122
4123        /// Returns the IDs of all positions in this manager
4124        access(all) fun getPositionIDs(): [UInt64] {
4125            return self.positions.keys
4126        }
4127    }
4128
4129    /// Creates and returns a new PositionManager resource
4130    access(all) fun createPositionManager(): @PositionManager {
4131        return <- create PositionManager()
4132    }
4133
4134    /// PositionSink
4135    ///
4136    /// A DeFiActions connector enabling deposits to a Position from within a DeFiActions stack.
4137    /// This Sink is intended to be constructed from a Position object.
4138    ///
4139    access(all) struct PositionSink: DeFiActions.Sink {
4140
4141        /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of
4142        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
4143
4144        /// An authorized Capability on the Pool for which the related Position is in
4145        access(self) let pool: Capability<auth(EPosition) &Pool>
4146
4147        /// The ID of the position in the Pool
4148        access(self) let positionID: UInt64
4149
4150        /// The Type of Vault this Sink accepts
4151        access(self) let type: Type
4152
4153        /// Whether deposits through this Sink to the Position should push available value to the Position's
4154        /// drawDownSink
4155        access(self) let pushToDrawDownSink: Bool
4156
4157        init(
4158            id: UInt64,
4159            pool: Capability<auth(EPosition) &Pool>,
4160            type: Type,
4161            pushToDrawDownSink: Bool
4162        ) {
4163            self.uniqueID = nil
4164            self.positionID = id
4165            self.pool = pool
4166            self.type = type
4167            self.pushToDrawDownSink = pushToDrawDownSink
4168        }
4169
4170        /// Returns the Type of Vault this Sink accepts on deposits
4171        access(all) view fun getSinkType(): Type {
4172            return self.type
4173        }
4174
4175        /// Returns the minimum capacity this Sink can accept as deposits
4176        access(all) fun minimumCapacity(): UFix64 {
4177            return self.pool.check() ? UFix64.max : 0.0
4178        }
4179
4180        /// Deposits the funds from the provided Vault reference to the related Position
4181        access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
4182            if let pool = self.pool.borrow() {
4183                pool.depositAndPush(
4184                    pid: self.positionID,
4185                    from: <-from.withdraw(amount: from.balance),
4186                    pushToDrawDownSink: self.pushToDrawDownSink
4187                )
4188            }
4189        }
4190
4191        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
4192            return DeFiActions.ComponentInfo(
4193                type: self.getType(),
4194                id: self.id(),
4195                innerComponents: []
4196            )
4197        }
4198
4199        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
4200            return self.uniqueID
4201        }
4202
4203        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
4204            self.uniqueID = id
4205        }
4206    }
4207
4208    /// PositionSource
4209    ///
4210    /// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack.
4211    /// This Source is intended to be constructed from a Position object.
4212    ///
4213    access(all) struct PositionSource: DeFiActions.Source {
4214
4215        /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of
4216        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
4217
4218        /// An authorized Capability on the Pool for which the related Position is in
4219        access(self) let pool: Capability<auth(EPosition) &Pool>
4220
4221        /// The ID of the position in the Pool
4222        access(self) let positionID: UInt64
4223
4224        /// The Type of Vault this Sink provides
4225        access(self) let type: Type
4226
4227        /// Whether withdrawals through this Sink from the Position should pull value from the Position's topUpSource
4228        /// in the event the withdrawal puts the position under its target health
4229        access(self) let pullFromTopUpSource: Bool
4230
4231        init(
4232            id: UInt64,
4233            pool: Capability<auth(EPosition) &Pool>,
4234            type: Type,
4235            pullFromTopUpSource: Bool
4236        ) {
4237            self.uniqueID = nil
4238            self.positionID = id
4239            self.pool = pool
4240            self.type = type
4241            self.pullFromTopUpSource = pullFromTopUpSource
4242        }
4243
4244        /// Returns the Type of Vault this Source provides on withdrawals
4245        access(all) view fun getSourceType(): Type {
4246            return self.type
4247        }
4248
4249        /// Returns the minimum available this Source can provide on withdrawal
4250        access(all) fun minimumAvailable(): UFix64 {
4251            if !self.pool.check() {
4252                return 0.0
4253            }
4254
4255            let pool = self.pool.borrow()!
4256            return pool.availableBalance(
4257                pid: self.positionID,
4258                type: self.type,
4259                pullFromTopUpSource: self.pullFromTopUpSource
4260            )
4261        }
4262
4263        /// Withdraws up to the max amount as the sourceType Vault
4264        access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
4265            if !self.pool.check() {
4266                return <- DeFiActionsUtils.getEmptyVault(self.type)
4267            }
4268
4269            let pool = self.pool.borrow()!
4270            let available = pool.availableBalance(
4271                pid: self.positionID,
4272                type: self.type,
4273                pullFromTopUpSource: self.pullFromTopUpSource
4274            )
4275            let withdrawAmount = (available > maxAmount) ? maxAmount : available
4276            if withdrawAmount > 0.0 {
4277                return <- pool.withdrawAndPull(
4278                    pid: self.positionID,
4279                    type: self.type,
4280                    amount: withdrawAmount,
4281                    pullFromTopUpSource: self.pullFromTopUpSource
4282                )
4283            } else {
4284                // Create an empty vault - this is a limitation we need to handle properly
4285                return <- DeFiActionsUtils.getEmptyVault(self.type)
4286            }
4287        }
4288
4289        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
4290            return DeFiActions.ComponentInfo(
4291                type: self.getType(),
4292                id: self.id(),
4293                innerComponents: []
4294            )
4295        }
4296
4297        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
4298            return self.uniqueID
4299        }
4300
4301        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
4302            self.uniqueID = id
4303        }
4304    }
4305
4306    /// BalanceDirection
4307    ///
4308    /// The direction of a given balance
4309    access(all) enum BalanceDirection: UInt8 {
4310
4311        /// Denotes that a balance that is withdrawable from the protocol
4312        access(all) case Credit
4313
4314        /// Denotes that a balance that is due to the protocol
4315        access(all) case Debit
4316    }
4317
4318    /// PositionBalance
4319    ///
4320    /// A structure returned externally to report a position's balance for a particular token.
4321    /// This structure is NOT used internally.
4322    access(all) struct PositionBalance {
4323
4324        /// The token type for which the balance details relate to
4325        access(all) let vaultType: Type
4326
4327        /// Whether the balance is a Credit or Debit
4328        access(all) let direction: BalanceDirection
4329
4330        /// The balance of the token for the related Position
4331        access(all) let balance: UFix64
4332
4333        init(
4334            vaultType: Type,
4335            direction: BalanceDirection,
4336            balance: UFix64
4337        ) {
4338            self.vaultType = vaultType
4339            self.direction = direction
4340            self.balance = balance
4341        }
4342    }
4343
4344    /// PositionDetails
4345    ///
4346    /// A structure returned externally to report all of the details associated with a position.
4347    /// This structure is NOT used internally.
4348    access(all) struct PositionDetails {
4349
4350        /// Balance details about each Vault Type deposited to the related Position
4351        access(all) let balances: [PositionBalance]
4352
4353        /// The default token Type of the Pool in which the related position is held
4354        access(all) let poolDefaultToken: Type
4355
4356        /// The available balance of the Pool's default token Type
4357        access(all) let defaultTokenAvailableBalance: UFix64
4358
4359        /// The current health of the related position
4360        access(all) let health: UFix128
4361
4362        init(
4363            balances: [PositionBalance],
4364            poolDefaultToken: Type,
4365            defaultTokenAvailableBalance: UFix64,
4366            health: UFix128
4367        ) {
4368            self.balances = balances
4369            self.poolDefaultToken = poolDefaultToken
4370            self.defaultTokenAvailableBalance = defaultTokenAvailableBalance
4371            self.health = health
4372        }
4373    }
4374
4375    /* --- PUBLIC METHODS ---- */
4376
4377    /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold.
4378    /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points.
4379    access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool {
4380        let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice
4381        let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice
4382        let diffBps = UInt16(diffPct * 10_000.0)
4383        return diffBps <= maxDeviationBps
4384    }
4385
4386    /// Returns a health value computed from the provided effective collateral and debt values
4387    /// where health is a ratio of effective collateral over effective debt
4388    access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 {
4389        if effectiveDebt == 0.0 {
4390            // Handles X/0 (infinite) including 0/0 (safe empty position)
4391            return UFix128.max
4392        }
4393
4394        if effectiveCollateral == 0.0 {
4395            // 0/Y where Y > 0 is 0 health (unsafe)
4396            return 0.0
4397        }
4398
4399        if (effectiveDebt / effectiveCollateral) == 0.0 {
4400            // Negligible debt relative to collateral: treat as infinite
4401            return UFix128.max
4402        }
4403
4404        return effectiveCollateral / effectiveDebt
4405    }
4406
4407    // Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point
4408    // number with 18 decimal places). The input to this function will be just the relative annual interest rate
4409    // (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001).
4410    access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 {
4411        let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0
4412        assert(
4413            perSecondScaledValue < UFix128.max,
4414            message: "Per-second interest rate \(perSecondScaledValue) is too high"
4415        )
4416        return perSecondScaledValue + 1.0
4417    }
4418
4419    /// Returns the compounded interest index reflecting the passage of time
4420    /// The result is: newIndex = oldIndex * perSecondRate ^ seconds
4421    access(all) view fun compoundInterestIndex(
4422        oldIndex: UFix128,
4423        perSecondRate: UFix128,
4424        elapsedSeconds: UFix64
4425    ): UFix128 {
4426        // Exponentiation by squaring on UFix128 for performance and precision
4427        let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds)
4428        return oldIndex * pow
4429    }
4430
4431    /// Transforms the provided `scaledBalance` to a true balance (or actual balance)
4432    /// where the true balance is the scaledBalance + accrued interest
4433    /// and the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals)
4434    access(all) view fun scaledBalanceToTrueBalance(
4435        _ scaled: UFix128,
4436        interestIndex: UFix128
4437    ): UFix128 {
4438        return scaled * interestIndex
4439    }
4440
4441    /// Transforms the provided `trueBalance` to a scaled balance
4442    /// where the scaled balance is the amount a borrower has actually interacted with (via deposits or withdrawals)
4443    /// and the true balance is the amount with respect to accrued interest
4444    access(all) view fun trueBalanceToScaledBalance(
4445        _ trueBalance: UFix128,
4446        interestIndex: UFix128
4447    ): UFix128 {
4448        return trueBalance / interestIndex
4449    }
4450
4451    /* --- INTERNAL METHODS --- */
4452
4453    /// Returns a reference to the contract account's MOET Minter resource
4454    access(self) view fun _borrowMOETMinter(): &MOET.Minter {
4455        return self.account.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath)
4456            ?? panic("Could not borrow reference to internal MOET Minter resource")
4457    }
4458
4459    init() {
4460        self.PoolStoragePath = StoragePath(identifier: "flowALPv0Pool_\(self.account.address)")!
4461        self.PoolFactoryPath = StoragePath(identifier: "flowALPv0PoolFactory_\(self.account.address)")!
4462        self.PoolPublicPath = PublicPath(identifier: "flowALPv0Pool_\(self.account.address)")!
4463        self.PoolCapStoragePath = StoragePath(identifier: "flowALPv0PoolCap_\(self.account.address)")!
4464
4465        self.PositionStoragePath = StoragePath(identifier: "flowALPv0Position_\(self.account.address)")!
4466        self.PositionPublicPath = PublicPath(identifier: "flowALPv0Position_\(self.account.address)")!
4467
4468        // save PoolFactory in storage
4469        self.account.storage.save(
4470            <-create PoolFactory(),
4471            to: self.PoolFactoryPath
4472        )
4473        let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)!
4474    }
4475}
4476