Smart Contract

FlowALP

A.6b00ff876c299c61.FlowALP

Valid From

132,033,883

Deployed

1w ago
Feb 19, 2026, 08:58:55 AM UTC

Dependents

19 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 FlowALP {
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; promotions to 128-bit occur only for internal math that multiplies by
15    ///   indices/rates. This strikes a balance between precision and ergonomics while keeping on-chain math safe.
16
17    /// The canonical StoragePath where the primary FlowALP Pool is stored
18    access(all) let PoolStoragePath: StoragePath
19    /// The canonical StoragePath where the PoolFactory resource is stored
20    access(all) let PoolFactoryPath: StoragePath
21    /// The canonical PublicPath where the primary FlowALP Pool can be accessed publicly
22    access(all) let PoolPublicPath: PublicPath
23
24    access(all) let PoolCapStoragePath: StoragePath
25
26    /* --- EVENTS ---- */
27
28    access(all) event Opened(pid: UInt64, poolUUID: UInt64)
29    // Prefer Type in events for stronger typing; off-chain can stringify via .identifier
30    access(all) event Deposited(pid: UInt64, poolUUID: UInt64, vaultType: Type, amount: UFix64, depositedUUID: UInt64)
31    access(all) event Withdrawn(pid: UInt64, poolUUID: UInt64, vaultType: Type, amount: UFix64, withdrawnUUID: UInt64)
32    access(all) event Rebalanced(pid: UInt64, poolUUID: UInt64, atHealth: UFix128, amount: UFix64, fromUnder: Bool)
33    // Consolidated liquidation params update event including all updated values
34    access(all) event LiquidationParamsUpdated(
35        poolUUID: UInt64,
36        targetHF: UFix128,
37        warmupSec: UInt64,
38        protocolFeeBps: UInt16
39    )
40    access(all) event LiquidationsPaused(poolUUID: UInt64)
41    access(all) event LiquidationsUnpaused(poolUUID: UInt64, warmupEndsAt: UInt64)
42    access(all) event LiquidationExecuted(pid: UInt64, poolUUID: UInt64, debtType: String, repayAmount: UFix64, seizeType: String, seizeAmount: UFix64, newHF: UFix128)
43    access(all) event LiquidationExecutedViaDex(pid: UInt64, poolUUID: UInt64, seizeType: String, seized: UFix64, debtType: String, repaid: UFix64, slippageBps: UInt16, newHF: UFix128)
44    access(all) event PriceOracleUpdated(poolUUID: UInt64, newOracleType: String)
45
46    /* --- CONSTRUCTS & INTERNAL METHODS ---- */
47
48    access(all) entitlement EPosition
49    access(all) entitlement EGovernance
50    access(all) entitlement EImplementation
51    access(all) entitlement EParticipant
52
53    /* --- NUMERIC TYPES POLICY ---
54        - External/public APIs (Vault amounts, deposits/withdrawals, events) use UFix64.
55        - Internal accounting and risk math use UFix128: scaled/true balances, interest indices/rates,
56          health factor, and prices once converted.
57        Rationale:
58        - Interest indices and rates are modeled as 18-decimal fixed-point in FlowALPMath and stored as UFix128.
59        - Operating in the UFix128 domain minimizes rounding error in true↔scaled conversions and
60          health/price computations.
61        - We convert at boundaries via FlowALPMath.toUFix128/toUFix64.
62    */
63    /// InternalBalance
64    ///
65    /// A structure used internally to track a position's balance for a particular token
66    access(all) struct InternalBalance {
67        /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol)
68        access(all) var direction: BalanceDirection
69        /// Internally, position balances are tracked using a "scaled balance". The "scaled balance" is the
70        /// actual balance divided by the current interest index for the associated token. This means we don't
71        /// need to update the balance of a position as time passes, even as interest rates change. We only need
72        /// to update the scaled balance when the user deposits or withdraws funds. The interest index
73        /// is a number relatively close to 1.0, so the scaled balance will be roughly of the same order of
74        /// magnitude as the actual balance. We store the scaled balance as UFix128 to align with UFix128
75        /// interest indices and to reduce rounding during true↔scaled conversions.
76        access(all) var scaledBalance: UFix128
77
78        // Single initializer that can handle both cases
79        init(direction: BalanceDirection, scaledBalance: UFix128) {
80            self.direction = direction
81            self.scaledBalance = scaledBalance
82        }
83
84        /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values in the
85        /// provided TokenState. It's assumed the TokenState and InternalBalance relate to the same token Type, but
86        /// since neither struct have values defining the associated token, callers should be sure to make the arguments
87        /// do in fact relate to the same token Type.
88        /// amount is expressed in UFix128 (true token units) to operate in the internal UFix128 domain; public
89        /// deposit APIs accept UFix64 and are converted at the boundary.
90        access(contract) fun recordDeposit(amount: UFix128, tokenState: auth(EImplementation) &TokenState) {
91            if self.direction == BalanceDirection.Credit {
92                // Depositing into a credit position just increases the balance.
93
94                // To maximize precision, we could convert the scaled balance to a true balance, add the
95                // deposit amount, and then convert the result back to a scaled balance. However, this will
96                // only cause problems for very small deposits (fractions of a cent), so we save computational
97                // cycles by just scaling the deposit amount and adding it directly to the scaled balance.
98                let scaledDeposit = FlowALP.trueBalanceToScaledBalance(amount,
99                    interestIndex: tokenState.creditInterestIndex)
100
101                self.scaledBalance = self.scaledBalance + scaledDeposit
102
103                // Increase the total credit balance for the token
104                tokenState.increaseCreditBalance(by: amount)
105            } else {
106                // When depositing into a debit position, we first need to compute the true balance to see
107                // if this deposit will flip the position from debit to credit.
108                let trueBalance = FlowALP.scaledBalanceToTrueBalance(self.scaledBalance,
109                    interestIndex: tokenState.debitInterestIndex)
110
111                // Harmonize comparison with withdrawal: treat an exact match as "does not flip to credit"
112                if trueBalance >= amount {
113                    // The deposit isn't big enough to clear the debt, so we just decrement the debt.
114                    let updatedBalance = trueBalance - amount
115
116                    self.scaledBalance = FlowALP.trueBalanceToScaledBalance(updatedBalance,
117                        interestIndex: tokenState.debitInterestIndex)
118
119                    // Decrease the total debit balance for the token
120                    tokenState.decreaseDebitBalance(by: amount)
121                } else {
122                    // The deposit is enough to clear the debt, so we switch to a credit position.
123                    let updatedBalance = amount - trueBalance
124
125                    self.direction = BalanceDirection.Credit
126                    self.scaledBalance = FlowALP.trueBalanceToScaledBalance(updatedBalance,
127                        interestIndex: tokenState.creditInterestIndex)
128
129                    // Increase the credit balance AND decrease the debit balance
130                    tokenState.increaseCreditBalance(by: updatedBalance)
131                    tokenState.decreaseDebitBalance(by: trueBalance)
132                }
133            }
134        }
135
136        /// Records a withdrawal of the defined amount, updating the inner scaledBalance as well as relevant values in
137        /// the provided TokenState. It's assumed the TokenState and InternalBalance relate to the same token Type, but
138        /// since neither struct have values defining the associated token, callers should be sure to make the arguments
139        /// do in fact relate to the same token Type.
140        /// amount is expressed in UFix128 for the same rationale as deposits; public withdraw APIs are UFix64 and are
141        /// converted at the boundary.
142        access(all) fun recordWithdrawal(amount: UFix128, tokenState: &TokenState) {
143            if self.direction == BalanceDirection.Debit {
144                // Withdrawing from a debit position just increases the debt amount.
145
146                // To maximize precision, we could convert the scaled balance to a true balance, subtract the
147                // withdrawal amount, and then convert the result back to a scaled balance. However, this will
148                // only cause problems for very small withdrawals (fractions of a cent), so we save computational
149                // cycles by just scaling the withdrawal amount and subtracting it directly from the scaled balance.
150                let scaledWithdrawal = FlowALP.trueBalanceToScaledBalance(amount,
151                    interestIndex: tokenState.debitInterestIndex)
152
153                self.scaledBalance = self.scaledBalance + scaledWithdrawal
154
155                // Increase the total debit balance for the token
156                tokenState.increaseDebitBalance(by: amount)
157            } else {
158                // When withdrawing from a credit position, we first need to compute the true balance to see
159                // if this withdrawal will flip the position from credit to debit.
160                let trueBalance = FlowALP.scaledBalanceToTrueBalance(self.scaledBalance,
161                    interestIndex: tokenState.creditInterestIndex)
162
163                if trueBalance >= amount {
164                    // The withdrawal isn't big enough to push the position into debt, so we just decrement the
165                    // credit balance.
166                    let updatedBalance = trueBalance - amount
167
168                    self.scaledBalance = FlowALP.trueBalanceToScaledBalance(updatedBalance,
169                        interestIndex: tokenState.creditInterestIndex)
170
171                    // Decrease the total credit balance for the token
172                    tokenState.decreaseCreditBalance(by: amount)
173                } else {
174                    // The withdrawal is enough to push the position into debt, so we switch to a debit position.
175                    let updatedBalance = amount - trueBalance
176
177                    self.direction = BalanceDirection.Debit
178                    self.scaledBalance = FlowALP.trueBalanceToScaledBalance(updatedBalance,
179                        interestIndex: tokenState.debitInterestIndex)
180
181                    // Decrease the credit balance AND increase the debit balance
182                    tokenState.decreaseCreditBalance(by: trueBalance)
183                    tokenState.increaseDebitBalance(by: updatedBalance)
184                }
185            }
186        }
187    }
188
189    /// BalanceSheet
190    ///
191    /// An struct containing a position's overview in terms of its effective collateral and debt as well as its
192    /// current health
193    access(all) struct BalanceSheet {
194        /// A position's withdrawable value based on collateral deposits against the Pool's collateral and borrow factors
195        access(all) let effectiveCollateral: UFix128
196        /// A position's withdrawn value based on withdrawals against the Pool's collateral and borrow factors
197        access(all) let effectiveDebt: UFix128
198        /// The health of the related position
199        access(all) let health: UFix128
200
201        init(effectiveCollateral: UFix128, effectiveDebt: UFix128) {
202            self.effectiveCollateral = effectiveCollateral
203            self.effectiveDebt = effectiveDebt
204            self.health = FlowALP.healthComputation(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt)
205        }
206    }
207
208    /// Liquidation parameters view (global)
209    access(all) struct LiquidationParamsView {
210        access(all) let targetHF: UFix128
211        access(all) let paused: Bool
212        access(all) let warmupSec: UInt64
213        access(all) let lastUnpausedAt: UInt64?
214        access(all) let triggerHF: UFix128
215        access(all) let protocolFeeBps: UInt16
216        init(targetHF: UFix128, paused: Bool, warmupSec: UInt64, lastUnpausedAt: UInt64?, triggerHF: UFix128, protocolFeeBps: UInt16) {
217            self.targetHF = targetHF
218            self.paused = paused
219            self.warmupSec = warmupSec
220            self.lastUnpausedAt = lastUnpausedAt
221            self.triggerHF = triggerHF
222            self.protocolFeeBps = protocolFeeBps
223        }
224    }
225
226    /// Liquidation quote output
227    access(all) struct LiquidationQuote {
228        access(all) let requiredRepay: UFix64
229        access(all) let seizeType: Type
230        access(all) let seizeAmount: UFix64
231        access(all) let newHF: UFix128
232        init(requiredRepay: UFix64, seizeType: Type, seizeAmount: UFix64, newHF: UFix128) {
233            self.requiredRepay = requiredRepay
234            self.seizeType = seizeType
235            self.seizeAmount = seizeAmount
236            self.newHF = newHF
237        }
238    }
239
240    /// Entitlement mapping enabling authorized references on nested resources within InternalPosition
241    access(all) entitlement mapping ImplementationUpdates {
242        EImplementation -> Mutate
243        EImplementation -> FungibleToken.Withdraw
244    }
245
246    /// InternalPosition
247    ///
248    /// An internal resource used to track deposits, withdrawals, balances, and queued deposits to an open position.
249    access(all) resource InternalPosition {
250        /// The target health of the position
251        access(EImplementation) var targetHealth: UFix128
252        /// The minimum health of the position, below which a position is considered undercollateralized
253        access(EImplementation) var minHealth: UFix128
254        /// The maximum health of the position, above which a position is considered overcollateralized
255        access(EImplementation) var maxHealth: UFix128
256        /// The balances of deposited and withdrawn token types
257        access(mapping ImplementationUpdates) var balances: {Type: InternalBalance}
258        /// Funds that have been deposited but must be asynchronously added to the Pool's reserves and recorded
259        access(mapping ImplementationUpdates) var queuedDeposits: @{Type: {FungibleToken.Vault}}
260        /// A DeFiActions Sink that if non-nil will enable the Pool to push overflown value automatically when the
261        /// position exceeds its maximum health based on the value of deposited collateral versus withdrawals
262        access(mapping ImplementationUpdates) var drawDownSink: {DeFiActions.Sink}?
263        /// A DeFiActions Source that if non-nil will enable the Pool to pull underflown value automatically when the
264        /// position falls below its minimum health based on the value of deposited collateral versus withdrawals. If
265        /// this value is not set, liquidation may occur in the event of undercollateralization.
266        access(mapping ImplementationUpdates) var topUpSource: {DeFiActions.Source}?
267
268        init() {
269            self.balances = {}
270            self.queuedDeposits <- {}
271            self.targetHealth = FlowALPMath.toUFix128(1.3)
272            self.minHealth = FlowALPMath.toUFix128(1.1)
273            self.maxHealth = FlowALPMath.toUFix128(1.5)
274            self.drawDownSink = nil
275            self.topUpSource = nil
276        }
277        /// Sets the Position's target health
278        access(EImplementation) fun setTargetHealth(_ value: UFix128) {
279            self.targetHealth = value
280        }
281        /// Sets the Position's minimum health
282        access(EImplementation) fun setMinHealth(_ value: UFix128) {
283            self.minHealth = value
284        }
285        /// Sets the Position's maximum health
286        access(EImplementation) fun setMaxHealth(_ value: UFix128) {
287            self.maxHealth = value
288        }
289        /// Returns a value-copy of `balances` suitable for constructing a `PositionView`.
290        access(all) fun copyBalances(): {Type: InternalBalance} {
291            return self.balances
292        }
293        /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when
294        /// the position exceeds its maximum health. Note, if a non-nil value is provided, the Sink MUST accept MOET
295        /// deposits or the operation will revert.
296        access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) {
297            pre {
298                sink == nil || sink!.getSinkType() == Type<@MOET.Vault>():
299                "Invalid Sink provided - Sink must accept MOET"
300            }
301            self.drawDownSink = sink
302        }
303        /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when
304        /// the position falls below its minimum health which may result in liquidation.
305        access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) {
306            self.topUpSource = source
307        }
308    }
309
310    /// InterestCurve
311    ///
312    /// A simple interface to calculate interest rate
313    access(all) struct interface InterestCurve {
314        access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 {
315            post {
316                result <= FlowALPMath.one: "Interest rate can't exceed 100%"
317            }
318        }
319    }
320
321    /// SimpleInterestCurve
322    ///
323    /// A simple implementation of the InterestCurve interface.
324    access(all) struct SimpleInterestCurve: InterestCurve {
325        access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 {
326            return 0.0 as UFix128 // TODO: replace with proper curve
327        }
328    }
329
330    /// TokenState
331    ///
332    /// The TokenState struct tracks values related to a single token Type within the Pool.
333    access(all) struct TokenState {
334        /// The timestamp at which the TokenState was last updated
335        access(all) var lastUpdate: UFix64
336        /// The total credit balance of the related Token across the whole Pool in which this TokenState resides
337        access(all) var totalCreditBalance: UFix128
338        /// The total debit balance of the related Token across the whole Pool in which this TokenState resides
339        access(all) var totalDebitBalance: UFix128
340        /// The index of the credit interest for the related token. Interest indices are 18-decimal fixed-point values
341        /// (see FlowALPMath) and are stored as UFix128 to maintain precision when converting between scaled and true
342        /// balances and when compounding.
343        access(all) var creditInterestIndex: UFix128
344        /// The index of the debit interest for the related token. Same rationale as creditInterestIndex: UFix128 keeps
345        /// the internal interest math and conversions precise and consistent.
346        access(all) var debitInterestIndex: UFix128
347        /// The interest rate for credit of the associated token, stored as UFix128 to match index precision and avoid
348        /// cumulative rounding during compounding.
349        access(all) var currentCreditRate: UFix128
350        /// The interest rate for debit of the associated token. Also UFix128 for consistency with indices/rates math.
351        access(all) var currentDebitRate: UFix128
352        /// The interest curve implementation used to calculate interest rate
353        access(all) var interestCurve: {InterestCurve}
354        /// The insurance rate applied to total credit when computing credit interest (default 0.1%)
355        access(all) var insuranceRate: UFix64
356        /// Per-deposit limit fraction of capacity (default 0.05 i.e., 5%)
357        access(all) var depositLimitFraction: UFix64
358        /// The rate at which depositCapacity can increase over time
359        access(all) var depositRate: UFix64
360        /// The limit on deposits of the related token
361        access(all) var depositCapacity: UFix64
362        /// The upper bound on total deposits of the related token, limiting how much depositCapacity can reach
363        access(all) var depositCapacityCap: UFix64
364
365        init(interestCurve: {InterestCurve}, depositRate: UFix64, depositCapacityCap: UFix64) {
366            self.lastUpdate = getCurrentBlock().timestamp
367            self.totalCreditBalance = 0.0 as UFix128
368            self.totalDebitBalance = 0.0 as UFix128
369            self.creditInterestIndex = FlowALPMath.one
370            self.debitInterestIndex = FlowALPMath.one
371            self.currentCreditRate = FlowALPMath.one
372            self.currentDebitRate = FlowALPMath.one
373            self.interestCurve = interestCurve
374            self.insuranceRate = 0.001
375            self.depositLimitFraction = 0.05
376            self.depositRate = depositRate
377            self.depositCapacity = depositCapacityCap
378            self.depositCapacityCap = depositCapacityCap
379        }
380
381        /// Sets the insurance rate for this token state
382        access(EImplementation) fun setInsuranceRate(_ rate: UFix64) {
383            self.insuranceRate = rate
384        }
385        /// Sets the per-deposit limit fraction for this token state
386        access(EImplementation) fun setDepositLimitFraction(_ frac: UFix64) {
387            self.depositLimitFraction = frac
388        }
389
390        // Explicit UFix128 balance update helpers used by core accounting
391        access(all) fun increaseCreditBalance(by amount: UFix128) {
392            self.totalCreditBalance = self.totalCreditBalance + amount
393        }
394
395        access(all) fun decreaseCreditBalance(by amount: UFix128) {
396            if amount >= self.totalCreditBalance {
397                self.totalCreditBalance = 0.0 as UFix128
398            } else {
399                self.totalCreditBalance = self.totalCreditBalance - amount
400            }
401        }
402
403        access(all) fun increaseDebitBalance(by amount: UFix128) {
404            self.totalDebitBalance = self.totalDebitBalance + amount
405        }
406
407        access(all) fun decreaseDebitBalance(by amount: UFix128) {
408            if amount >= self.totalDebitBalance {
409                self.totalDebitBalance = 0.0 as UFix128
410            } else {
411                self.totalDebitBalance = self.totalDebitBalance - amount
412            }
413        }
414
415        /// Updates the totalCreditBalance by the provided amount
416        access(all) fun updateCreditBalance(amount: Int256) {
417            // temporary cast the credit balance to a signed value so we can add/subtract
418            let adjustedBalance = Int256(self.totalCreditBalance) + amount
419            // Do not silently clamp: underflow indicates a serious accounting error
420            assert(adjustedBalance >= 0, message: "totalCreditBalance underflow")
421            self.totalCreditBalance = UFix128(adjustedBalance)
422        }
423
424        access(all) fun updateDebitBalance(amount: Int256) {
425            // temporary cast the debit balance to a signed value so we can add/subtract
426            let adjustedBalance = Int256(self.totalDebitBalance) + amount
427            // Do not silently clamp: underflow indicates a serious accounting error
428            assert(adjustedBalance >= 0, message: "totalDebitBalance underflow")
429            self.totalDebitBalance = UFix128(adjustedBalance)
430        }
431
432        // Enhanced updateInterestIndices with deposit capacity update
433        access(all) fun updateInterestIndices() {
434            let currentTime: UFix64 = getCurrentBlock().timestamp
435            let dt: UFix64 = currentTime - self.lastUpdate
436
437            // No time elapsed or already at cap → nothing to do
438            if dt <= 0.0 {
439                return
440            }
441
442            // Update interest indices (dt > 0 ensures sensible compounding)
443            self.creditInterestIndex = FlowALP.compoundInterestIndex(
444                oldIndex: self.creditInterestIndex,
445                perSecondRate: self.currentCreditRate,
446                elapsedSeconds: dt
447            )
448            self.debitInterestIndex = FlowALP.compoundInterestIndex(
449                oldIndex: self.debitInterestIndex,
450                perSecondRate: self.currentDebitRate,
451                elapsedSeconds: dt
452            )
453
454            // Record the moment we accounted for
455            self.lastUpdate = currentTime
456
457            // Deposit capacity is fixed at the cap; growth logic is disabled.
458        }
459
460        // Deposit limit function
461        // Rationale: cap per-deposit size to a fraction of the time-based
462        // depositCapacity so a single large deposit cannot monopolize capacity.
463        // Excess is queued and drained in chunks (see asyncUpdatePosition),
464        // enabling fair throughput across many deposits in a block. The 5%
465        // fraction is conservative and can be tuned by protocol parameters.
466        access(all) fun depositLimit(): UFix64 {
467            return self.depositCapacity * self.depositLimitFraction
468        }
469
470        access(all) fun updateForTimeChange() {
471            self.updateInterestIndices()
472        }
473
474        access(all) fun updateInterestRates() {
475            // If there's no credit balance, we can't calculate a meaningful credit rate
476            // so we'll just set both rates to one (no interest) and return early
477            if self.totalCreditBalance == 0.0 as UFix128 {
478                self.currentCreditRate = FlowALPMath.one  // 1.0 in fixed point (no interest)
479                self.currentDebitRate = FlowALPMath.one   // 1.0 in fixed point (no interest)
480                return
481            }
482
483            let debitRate = self.interestCurve.interestRate(creditBalance: self.totalCreditBalance, debitBalance: self.totalDebitBalance)
484            let debitIncome = self.totalDebitBalance + debitRate
485
486            // Calculate insurance amount (0.1% of credit balance)
487            let insuranceRate: UFix128 = FlowALPMath.toUFix128(self.insuranceRate)
488            let insuranceAmount: UFix128 = self.totalCreditBalance * insuranceRate
489
490            // Calculate credit rate, ensuring we don't have underflows
491            var creditRate: UFix128 = 0.0 as UFix128
492            if debitIncome >= insuranceAmount {
493                creditRate = ((debitIncome - insuranceAmount) / self.totalCreditBalance) - FlowALPMath.one
494            } else {
495                // If debit income doesn't cover insurance, credit interest would be negative.
496                // Since negative rates aren't represented here, we pay 0% to depositors.
497                creditRate = 0.0 as UFix128
498            }
499
500            self.currentCreditRate = FlowALP.perSecondInterestRate(yearlyRate: creditRate)
501            self.currentDebitRate = FlowALP.perSecondInterestRate(yearlyRate: debitRate)
502        }
503    }
504
505    
506
507    /// Risk parameters for a token used in effective collateral/debt computations.
508    /// - collateralFactor: fraction applied to credit value to derive effective collateral
509    /// - borrowFactor: fraction dividing debt value to derive effective debt
510    /// - liquidationBonus: premium applied to liquidations to incentivize repayors
511    access(all) struct RiskParams {
512        access(all) let collateralFactor: UFix128
513        access(all) let borrowFactor: UFix128
514        access(all) let liquidationBonus: UFix128  // bonus expressed as fractional rate, e.g. 0.05 for 5%
515
516        init(cf: UFix128, bf: UFix128, lb: UFix128) {
517            self.collateralFactor = cf
518            self.borrowFactor = bf
519            self.liquidationBonus = lb
520        }
521    }
522
523    /// Immutable snapshot of token-level data required for pure math operations
524    access(all) struct TokenSnapshot {
525        access(all) let price: UFix128
526        access(all) let creditIndex: UFix128
527        access(all) let debitIndex: UFix128
528        access(all) let risk: RiskParams
529        init(price: UFix128, credit: UFix128, debit: UFix128, risk: RiskParams) {
530            self.price = price
531            self.creditIndex = credit
532            self.debitIndex = debit
533            self.risk = risk
534        }
535    }
536
537    /// Copy-only representation of a position used by pure math (no storage refs)
538    access(all) struct PositionView {
539        access(all) let balances: {Type: InternalBalance}
540        access(all) let snapshots: {Type: TokenSnapshot}
541        access(all) let defaultToken: Type
542        access(all) let minHealth: UFix128
543        access(all) let maxHealth: UFix128
544        init(balances: {Type: InternalBalance},
545             snapshots: {Type: TokenSnapshot},
546             def: Type,
547             min: UFix128,
548             max: UFix128) {
549            self.balances = balances
550            self.snapshots = snapshots
551            self.defaultToken = def
552            self.minHealth = min
553            self.maxHealth = max
554        }
555    }
556
557    // PURE HELPERS -------------------------------------------------------------
558
559    access(all) view fun effectiveCollateral(credit: UFix128, snap: TokenSnapshot): UFix128 {
560        return (credit * snap.price) * snap.risk.collateralFactor
561    }
562
563    access(all) view fun effectiveDebt(debit: UFix128, snap: TokenSnapshot): UFix128 {
564        return (debit * snap.price) / snap.risk.borrowFactor
565    }
566
567    /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0)
568    access(all) view fun healthFactor(view: PositionView): UFix128 {
569        var effectiveCollateralTotal: UFix128 = 0.0 as UFix128
570        var effectiveDebtTotal: UFix128 = 0.0 as UFix128
571        for tokenType in view.balances.keys {
572            let balance = view.balances[tokenType]!
573            let snap = view.snapshots[tokenType]!
574            if balance.direction == BalanceDirection.Credit {
575                let trueBalance = FlowALP.scaledBalanceToTrueBalance(
576                    balance.scaledBalance,
577                    interestIndex: snap.creditIndex
578                )
579                effectiveCollateralTotal = effectiveCollateralTotal + FlowALP.effectiveCollateral(credit: trueBalance, snap: snap)
580            } else {
581                let trueBalance = FlowALP.scaledBalanceToTrueBalance(
582                    balance.scaledBalance,
583                    interestIndex: snap.debitIndex
584                )
585                effectiveDebtTotal = effectiveDebtTotal + FlowALP.effectiveDebt(debit: trueBalance, snap: snap)
586            }
587        }
588        return FlowALP.healthComputation(
589            effectiveCollateral: effectiveCollateralTotal,
590            effectiveDebt: effectiveDebtTotal
591        )
592    }
593
594    /// Amount of `withdrawSnap` token that can be withdrawn while staying ≥ targetHealth
595    access(all) view fun maxWithdraw(
596        view: PositionView,
597        withdrawSnap: TokenSnapshot,
598        withdrawBal: InternalBalance?,
599        targetHealth: UFix128
600    ): UFix128 {
601        let preHealth = FlowALP.healthFactor(view: view)
602        if preHealth <= targetHealth {
603            return 0.0 as UFix128
604        }
605
606        var effectiveCollateralTotal: UFix128 = 0.0 as UFix128
607        var effectiveDebtTotal: UFix128 = 0.0 as UFix128
608        for tokenType in view.balances.keys {
609            let balance = view.balances[tokenType]!
610            let snap = view.snapshots[tokenType]!
611            if balance.direction == BalanceDirection.Credit {
612                let trueBalance = FlowALP.scaledBalanceToTrueBalance(
613                    balance.scaledBalance,
614                    interestIndex: snap.creditIndex
615                )
616                effectiveCollateralTotal = effectiveCollateralTotal + FlowALP.effectiveCollateral(credit: trueBalance, snap: snap)
617            } else {
618                let trueBalance = FlowALP.scaledBalanceToTrueBalance(
619                    balance.scaledBalance,
620                    interestIndex: snap.debitIndex
621                )
622                effectiveDebtTotal = effectiveDebtTotal + FlowALP.effectiveDebt(debit: trueBalance, snap: snap)
623            }
624        }
625
626        let collateralFactor = withdrawSnap.risk.collateralFactor
627        let borrowFactor = withdrawSnap.risk.borrowFactor
628
629        if withdrawBal == nil || withdrawBal!.direction == BalanceDirection.Debit {
630            // withdrawing increases debt
631            let numerator = effectiveCollateralTotal
632            let denominatorTarget = numerator / targetHealth
633            let deltaDebt = denominatorTarget > effectiveDebtTotal ? denominatorTarget - effectiveDebtTotal : FlowALPMath.zero
634            let tokens = (deltaDebt * borrowFactor) / withdrawSnap.price
635            return tokens
636        } else {
637            // withdrawing reduces collateral
638            let trueBalance = FlowALP.scaledBalanceToTrueBalance(
639                withdrawBal!.scaledBalance,
640                interestIndex: withdrawSnap.creditIndex
641            )
642            let maxPossible = trueBalance
643            let requiredCollateral = effectiveDebtTotal * targetHealth
644            if effectiveCollateralTotal <= requiredCollateral {
645                return 0.0 as UFix128
646            }
647            let deltaCollateralEffective = effectiveCollateralTotal - requiredCollateral
648            let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.price
649            return deltaTokens > maxPossible ? maxPossible : deltaTokens
650        }
651    }
652
653    
654
655    /// Pool
656    ///
657    /// A Pool is the primary logic for protocol operations. It contains the global state of all positions, credit and
658    /// debit balances for each supported token type, and reserves as they are deposited to positions.
659    access(all) resource Pool {
660        /// Enable or disable verbose contract logging for debugging.
661        access(self) var debugLogging: Bool
662        /// Global state for tracking each token
663        access(self) var globalLedger: {Type: TokenState}
664        /// Individual user positions
665        access(self) var positions: @{UInt64: InternalPosition}
666        /// The actual reserves of each token
667        access(self) var reserves: @{Type: {FungibleToken.Vault}}
668        /// Auto-incrementing position identifier counter
669        access(self) var nextPositionID: UInt64
670        /// The default token type used as the "unit of account" for the pool.
671        access(self) let defaultToken: Type
672        /// A price oracle that will return the price of each token in terms of the default token.
673        access(self) var priceOracle: {DeFiActions.PriceOracle}
674        /// Together with borrowFactor, collateralFactor determines borrowing limits for each token
675        /// When determining the withdrawable loan amount, the value of the token (provided by the PriceOracle) is
676        /// multiplied by the collateral factor. The total "effective collateral" for a position is the value of each
677        /// token deposited to the position multiplied by its collateral factor
678        access(self) var collateralFactor: {Type: UFix64}
679        /// Together with collateralFactor, borrowFactor determines borrowing limits for each token
680        /// The borrowFactor determines how much of a position's "effective collateral" can be borrowed against as a
681        /// percentage between 0.0 and 1.0
682        access(self) var borrowFactor: {Type: UFix64}
683        /// Per-token liquidation bonus fraction (e.g., 0.05 for 5%)
684        access(self) var liquidationBonus: {Type: UFix64}
685        /// The count of positions to update per asynchronous update
686        access(self) var positionsProcessedPerCallback: UInt64
687        /// Position update queue to be processed as an asynchronous update
688        access(EImplementation) var positionsNeedingUpdates: [UInt64]
689        /// A simple version number that is incremented whenever one or more interest indices are updated. This is used
690        /// to detect when the interest indices need to be updated in InternalPositions.
691        access(EImplementation) var version: UInt64
692        /// Liquidation target health and controls (global)
693        access(self) var liquidationTargetHF: UFix128   // e24 fixed-point, e.g., 1.05e24
694        access(self) var liquidationsPaused: Bool
695        access(self) var liquidationWarmupSec: UInt64
696        access(self) var lastUnpausedAt: UInt64?
697        access(self) var protocolLiquidationFeeBps: UInt16
698        /// Allowlist of permitted DeFiActions Swapper types for DEX liquidations
699        access(self) var allowedSwapperTypes: {Type: Bool}
700        /// Max allowed deviation in basis points between DEX-implied price and oracle price
701        access(self) var dexOracleDeviationBps: UInt16
702        /// Max slippage allowed in basis points for DEX liquidations
703        access(self) var dexMaxSlippageBps: UInt64
704        /// Max route hops allowed for DEX liquidations
705        access(self) var dexMaxRouteHops: UInt64
706
707        init(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}) {
708            pre {
709                priceOracle.unitOfAccount() == defaultToken: "Price oracle must return prices in terms of the default token"
710            }
711
712            self.version = 0
713            self.debugLogging = false
714            self.globalLedger = {defaultToken: TokenState(
715                interestCurve: SimpleInterestCurve(),
716                depositRate: 1_000_000.0,        // Default: no rate limiting for default token
717                depositCapacityCap: 1_000_000.0  // Default: high capacity cap
718            )}
719            self.positions <- {}
720            self.reserves <- {}
721            self.defaultToken = defaultToken
722            self.priceOracle = priceOracle
723            self.collateralFactor = {defaultToken: 1.0}
724            self.borrowFactor = {defaultToken: 1.0}
725            self.liquidationBonus = {defaultToken: 0.05}
726            self.nextPositionID = 0
727            self.positionsNeedingUpdates = []
728            self.positionsProcessedPerCallback = 100
729            self.liquidationTargetHF = FlowALPMath.toUFix128(1.05)
730            self.liquidationsPaused = false
731            self.liquidationWarmupSec = 300
732            self.lastUnpausedAt = nil
733            self.protocolLiquidationFeeBps = UInt16(0)
734            self.allowedSwapperTypes = {}
735            self.dexOracleDeviationBps = UInt16(300) // 3% default
736            self.dexMaxSlippageBps = 100
737            self.dexMaxRouteHops = 3
738
739            // The pool starts with an empty reserves map. Vaults will be created when tokens are first deposited.
740        }
741
742        access(self) fun _assertLiquidationsActive() {
743            assert(!self.liquidationsPaused, message: "Liquidations paused")
744            if self.lastUnpausedAt != nil {
745                let now = UInt64(getCurrentBlock().timestamp)
746                assert(now >= self.lastUnpausedAt! + self.liquidationWarmupSec, message: "Liquidations in warm-up period")
747            }
748        }
749
750        ///////////////
751        // GETTERS
752        ///////////////
753
754        /// Returns an array of the supported token Types
755        access(all) view fun getSupportedTokens(): [Type] {
756            return self.globalLedger.keys
757        }
758
759        /// Returns whether a given token Type is supported or not
760        access(all) view fun isTokenSupported(tokenType: Type): Bool {
761            return self.globalLedger[tokenType] != nil
762        }
763
764        /// Returns current liquidation parameters
765        access(all) fun getLiquidationParams(): FlowALP.LiquidationParamsView {
766            return FlowALP.LiquidationParamsView(
767                targetHF: self.liquidationTargetHF,
768                paused: self.liquidationsPaused,
769                warmupSec: self.liquidationWarmupSec,
770                lastUnpausedAt: self.lastUnpausedAt,
771                triggerHF: FlowALPMath.one, // 1.0e24
772                protocolFeeBps: self.protocolLiquidationFeeBps
773            )
774        }
775
776        /// Returns Oracle-DEX guards and allowlists for frontends/keepers
777        access(all) fun getDexLiquidationConfig(): {String: AnyStruct} {
778            let allowed: [String] = []
779            for t in self.allowedSwapperTypes.keys {
780                allowed.append(t.identifier)
781            }
782            return {
783                "dexOracleDeviationBps": self.dexOracleDeviationBps,
784                "allowedSwappers": allowed,
785                "dexMaxSlippageBps": self.dexMaxSlippageBps,
786                "dexMaxRouteHops": self.dexMaxRouteHops // informational; enforcement is left to swapper implementations
787            }
788        }
789
790        /// Returns true if the position is under the global liquidation trigger (health < 1.0)
791        access(all) fun isLiquidatable(pid: UInt64): Bool {
792            let health = self.positionHealth(pid: pid)
793            return health < FlowALPMath.one
794        }
795
796        /// Returns the current reserve balance for the specified token type.
797        access(all) view fun reserveBalance(type: Type): UFix64 {
798            let vaultRef = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)
799            if vaultRef == nil {
800                return 0.0
801            }
802            return vaultRef!.balance
803        }
804
805        /// Returns a position's balance available for withdrawal of a given Vault type.
806        /// Phase 0 refactor: compute via pure helpers using a PositionView and TokenSnapshot for the base path.
807        /// When pullFromTopUpSource is true and a topUpSource exists, preserve deposit-assisted semantics.
808        access(all) fun availableBalance(pid: UInt64, type: Type, pullFromTopUpSource: Bool): UFix64 {
809            if self.debugLogging { log("    [CONTRACT] availableBalance(pid: \(pid), type: \(type.contractName!), pullFromTopUpSource: \(pullFromTopUpSource))") }
810            let position = self._borrowPosition(pid: pid)
811
812            if pullFromTopUpSource && position.topUpSource != nil {
813                let topUpSource = position.topUpSource!
814                let sourceType = topUpSource.getSourceType()
815                let sourceAmount = topUpSource.minimumAvailable()
816                if self.debugLogging { log("    [CONTRACT] Calling to fundsAvailableAboveTargetHealthAfterDepositing with sourceAmount \(sourceAmount) and targetHealth \(position.minHealth)") }
817
818                return self.fundsAvailableAboveTargetHealthAfterDepositing(
819                    pid: pid,
820                    withdrawType: type,
821                    targetHealth: position.minHealth,
822                    depositType: sourceType,
823                    depositAmount: sourceAmount
824                )
825            }
826
827            let view = self.buildPositionView(pid: pid)
828
829            // Build a TokenSnapshot for the requested withdraw type (may not exist in view.snapshots)
830            let tokenState = self._borrowUpdatedTokenState(type: type)
831            let snap = FlowALP.TokenSnapshot(
832                price: FlowALPMath.toUFix128(self.priceOracle.price(ofToken: type)!),
833                credit: tokenState.creditInterestIndex,
834                debit: tokenState.debitInterestIndex,
835                risk: FlowALP.RiskParams(
836                    cf: FlowALPMath.toUFix128(self.collateralFactor[type]!),
837                    bf: FlowALPMath.toUFix128(self.borrowFactor[type]!),
838                    lb: FlowALPMath.toUFix128(self.liquidationBonus[type]!)
839                )
840            )
841
842            let withdrawBal = view.balances[type]
843            let uintMax = FlowALP.maxWithdraw(
844                view: view,
845                withdrawSnap: snap,
846                withdrawBal: withdrawBal,
847                targetHealth: view.minHealth
848            )
849            return FlowALPMath.toUFix64Round(uintMax)
850        }
851
852        /// Returns the health of the given position, which is the ratio of the position's effective collateral to its
853        /// debt as denominated in the Pool's default token. "Effective collateral" means the value of each credit balance
854        /// times the liquidation threshold for that token. i.e. the maximum borrowable amount
855        access(all) fun positionHealth(pid: UInt64): UFix128 {
856            let position = self._borrowPosition(pid: pid)
857
858            // Get the position's collateral and debt values in terms of the default token.
859            var effectiveCollateral: UFix128 = 0.0 as UFix128
860            var effectiveDebt: UFix128 = 0.0 as UFix128
861
862            for type in position.balances.keys {
863                let balance = position.balances[type]!
864                let tokenState = self._borrowUpdatedTokenState(type: type)
865
866                let collateralFactor = FlowALPMath.toUFix128(self.collateralFactor[type]!)
867                let borrowFactor = FlowALPMath.toUFix128(self.borrowFactor[type]!)
868                let price = FlowALPMath.toUFix128(self.priceOracle.price(ofToken: type)!)
869                if balance.direction == BalanceDirection.Credit {
870                    let trueBalance = FlowALP.scaledBalanceToTrueBalance(balance.scaledBalance,
871                        interestIndex: tokenState.creditInterestIndex)
872
873                let value = price * trueBalance
874                let effectiveCollateralValue = value * collateralFactor
875                    effectiveCollateral = effectiveCollateral + effectiveCollateralValue
876                } else {
877                    let trueBalance = FlowALP.scaledBalanceToTrueBalance(balance.scaledBalance,
878                        interestIndex: tokenState.debitInterestIndex)
879
880                    let value = price * trueBalance
881                    let effectiveDebtValue = value / borrowFactor
882                    effectiveDebt = effectiveDebt + effectiveDebtValue
883                }
884            }
885
886            // Calculate the health as the ratio of collateral to debt.
887            return FlowALP.healthComputation(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt)
888        }
889
890        /// Returns the quantity of funds of a specified token which would need to be deposited to bring the position to
891        /// the provided target health. This function will return 0.0 if the position is already at or over that health
892        /// value.
893        access(all) fun fundsRequiredForTargetHealth(pid: UInt64, type: Type, targetHealth: UFix128): UFix64 {
894            return self.fundsRequiredForTargetHealthAfterWithdrawing(
895                pid: pid,
896                depositType: type,
897                targetHealth: targetHealth,
898                withdrawType: self.defaultToken,
899                withdrawAmount: 0.0
900            )
901        }
902
903        /// Returns the details of a given position as a PositionDetails external struct
904        access(all) fun getPositionDetails(pid: UInt64): PositionDetails {
905            if self.debugLogging { log("    [CONTRACT] getPositionDetails(pid: \(pid))") }
906            let position = self._borrowPosition(pid: pid)
907            var balances: [PositionBalance] = []
908
909            for type in position.balances.keys {
910                let balance = position.balances[type]!
911                let tokenState = self._borrowUpdatedTokenState(type: type)
912                let trueBalance = balance.direction == BalanceDirection.Credit
913                    ? FlowALP.scaledBalanceToTrueBalance(balance.scaledBalance, interestIndex: tokenState.creditInterestIndex)
914                    : FlowALP.scaledBalanceToTrueBalance(balance.scaledBalance, interestIndex: tokenState.debitInterestIndex)
915
916                balances.append(PositionBalance(
917                    vaultType: type,
918                    direction: balance.direction,
919                    balance: FlowALPMath.toUFix64Round(trueBalance)
920                ))
921            }
922
923            let health = self.positionHealth(pid: pid)
924            let defaultTokenAvailable = self.availableBalance(pid: pid, type: self.defaultToken, pullFromTopUpSource: false)
925
926            return PositionDetails(
927                balances: balances,
928                poolDefaultToken: self.defaultToken,
929                defaultTokenAvailableBalance: defaultTokenAvailable,
930                health: health
931            )
932        }
933
934        /// Quote liquidation required repay and seize amounts to bring HF to liquidationTargetHF using a single seizeType
935        access(all) fun quoteLiquidation(pid: UInt64, debtType: Type, seizeType: Type): FlowALP.LiquidationQuote {
936            pre {
937                self.globalLedger[debtType] != nil: "Invalid debt type \(debtType.identifier)"
938                self.globalLedger[seizeType] != nil: "Invalid seize type \(seizeType.identifier)"
939            }
940            let view = self.buildPositionView(pid: pid)
941            let health = FlowALP.healthFactor(view: view)
942            if health >= FlowALPMath.one {
943                return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: health)
944            }
945            // Build snapshots
946            let debtState = self._borrowUpdatedTokenState(type: debtType)
947            let seizeState = self._borrowUpdatedTokenState(type: seizeType)
948            // Resolve per-token liquidation bonus (default 5%) for debtType
949            var lbDebtUFix: UFix64 = 0.05
950            let lbDebtOpt = self.liquidationBonus[debtType]
951            if lbDebtOpt != nil {
952                lbDebtUFix = lbDebtOpt!
953            }
954            let debtSnap = FlowALP.TokenSnapshot(
955                price: FlowALPMath.toUFix128(self.priceOracle.price(ofToken: debtType)!),
956                credit: debtState.creditInterestIndex,
957                debit: debtState.debitInterestIndex,
958                risk: FlowALP.RiskParams(
959                    cf: FlowALPMath.toUFix128(self.collateralFactor[debtType]!),
960                    bf: FlowALPMath.toUFix128(self.borrowFactor[debtType]!),
961                    lb: FlowALPMath.toUFix128(lbDebtUFix)
962                )
963            )
964            // Resolve per-token liquidation bonus (default 5%) for seizeType
965            var lbSeizeUFix: UFix64 = 0.05
966            let lbSeizeOpt = self.liquidationBonus[seizeType]
967            if lbSeizeOpt != nil {
968                lbSeizeUFix = lbSeizeOpt!
969            }
970            let seizeSnap = FlowALP.TokenSnapshot(
971                price: FlowALPMath.toUFix128(self.priceOracle.price(ofToken: seizeType)!),
972                credit: seizeState.creditInterestIndex,
973                debit: seizeState.debitInterestIndex,
974                risk: FlowALP.RiskParams(
975                    cf: FlowALPMath.toUFix128(self.collateralFactor[seizeType]!),
976                    bf: FlowALPMath.toUFix128(self.borrowFactor[seizeType]!),
977                    lb: FlowALPMath.toUFix128(lbSeizeUFix)
978                )
979            )
980
981            // Recompute effective totals and capture available true collateral for seizeType
982            var effColl: UFix128 = 0.0 as UFix128
983            var effDebt: UFix128 = 0.0 as UFix128
984            var trueCollateralSeize: UFix128 = 0.0 as UFix128
985            var trueDebt: UFix128 = 0.0 as UFix128
986            for t in view.balances.keys {
987                let b = view.balances[t]!
988                let st = self._borrowUpdatedTokenState(type: t)
989                // Resolve per-token liquidation bonus (default 5%) for token t
990            var lbTUFix: UFix64 = 0.05
991                let lbTOpt = self.liquidationBonus[t]
992                if lbTOpt != nil {
993                    lbTUFix = lbTOpt!
994                }
995                let snap = FlowALP.TokenSnapshot(
996                    price: FlowALPMath.toUFix128(self.priceOracle.price(ofToken: t)!),
997                    credit: st.creditInterestIndex,
998                    debit: st.debitInterestIndex,
999                    risk: FlowALP.RiskParams(
1000                        cf: FlowALPMath.toUFix128(self.collateralFactor[t]!),
1001                        bf: FlowALPMath.toUFix128(self.borrowFactor[t]!),
1002                        lb: FlowALPMath.toUFix128(lbTUFix)
1003                    )
1004                )
1005                if b.direction == BalanceDirection.Credit {
1006                    let trueBal = FlowALP.scaledBalanceToTrueBalance(b.scaledBalance, interestIndex: snap.creditIndex)
1007                    if t == seizeType {
1008                        trueCollateralSeize = trueBal
1009                    }
1010                    effColl = effColl + FlowALP.effectiveCollateral(credit: trueBal, snap: snap)
1011                } else {
1012                    let trueBal = FlowALP.scaledBalanceToTrueBalance(b.scaledBalance, interestIndex: snap.debitIndex)
1013                    if t == debtType {
1014                        trueDebt = trueBal
1015                    }
1016                    effDebt = effDebt + FlowALP.effectiveDebt(debit: trueBal, snap: snap)
1017                }
1018            }
1019
1020            // Compute required effective collateral increase to reach targetHF
1021            let target = self.liquidationTargetHF
1022            if effDebt == 0.0 as UFix128 { // no debt
1023                return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: UFix128.max)
1024            }
1025            let requiredEffColl = effDebt * target
1026            if effColl >= requiredEffColl {
1027                return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: health)
1028            }
1029            let deltaEffColl = requiredEffColl - effColl
1030
1031            // Paying debt reduces effectiveDebt instead of increasing collateral. Solve for repay needed in debt token terms:
1032            // effDebtNew = effDebt - (repayTrue * debtSnap.price / debtSnap.risk.borrowFactor)
1033            // target = effColl / effDebtNew  => effDebtNew = effColl / target
1034            // So reductionNeeded = effDebt - effColl/target
1035            let effDebtNew = FlowALPMath.div(effColl, target)
1036            if effDebt <= effDebtNew {
1037                return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: target)
1038            }
1039            // Use simultaneous solve below; the approximate path is omitted
1040
1041            // New simultaneous solve for repayTrue (let R = repayTrue, S = seizeTrue):
1042            // Target HF = (effColl - S * Pc * CF) / (effDebt - R * Pd / BF)
1043            // S = (R * Pd / BF) * (1 + LB) / (Pc * CF)
1044            // Solve for R such that HF = target
1045            let Pd = debtSnap.price
1046            let Pc = seizeSnap.price
1047            let BF = debtSnap.risk.borrowFactor
1048            let CF = seizeSnap.risk.collateralFactor
1049            let LB = seizeSnap.risk.liquidationBonus
1050
1051            // Reuse previously computed effective collateral and debt
1052
1053            if effDebt == 0.0 as UFix128 || effColl / effDebt >= target {
1054                return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: effColl / effDebt)
1055            }
1056
1057            // Derived formula with positive denominator: u = (t * effDebt - effColl) / (t - (1 + LB) * CF)
1058            let num = effDebt * target - effColl
1059            let denomFactor = target - ((FlowALPMath.one + LB) * CF)
1060            if denomFactor <= FlowALPMath.zero {
1061                // Impossible target, return 0
1062                return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: health)
1063            }
1064            var repayTrueU128 = FlowALPMath.div(num * BF, Pd * denomFactor)
1065            if repayTrueU128 > trueDebt {
1066                repayTrueU128 = trueDebt
1067            }
1068            let u = FlowALPMath.div(repayTrueU128 * Pd, BF)
1069            var seizeTrueU128 = FlowALPMath.div(u * (FlowALPMath.one + LB), Pc)
1070            if seizeTrueU128 > trueCollateralSeize {
1071                seizeTrueU128 = trueCollateralSeize
1072                let uAllowed = FlowALPMath.div(seizeTrueU128 * Pc, (FlowALPMath.one + LB))
1073                repayTrueU128 = FlowALPMath.div(uAllowed * BF, Pd)
1074                if repayTrueU128 > trueDebt {
1075                    repayTrueU128 = trueDebt
1076                }
1077            }
1078            let repayExact = FlowALPMath.toUFix64RoundUp(repayTrueU128)
1079            let seizeExact = FlowALPMath.toUFix64RoundUp(seizeTrueU128)
1080            let repayEff = FlowALPMath.div(repayTrueU128 * Pd, BF)
1081            let seizeEff = seizeTrueU128 * (Pc * CF)
1082            let newEffColl = effColl > seizeEff ? effColl - seizeEff : FlowALPMath.zero
1083            let newEffDebt = effDebt > repayEff ? effDebt - repayEff : FlowALPMath.zero
1084            let newHF = newEffDebt == FlowALPMath.zero ? UFix128.max : FlowALPMath.div(newEffColl * FlowALPMath.one, newEffDebt)
1085
1086            // Prevent liquidation if it would worsen HF (deep insolvency case).
1087            // Enhanced fallback: search for the repay/seize pair (under protocol pricing relation
1088            // and available-collateral/debt caps) that maximizes HF. We discretize the search to keep costs bounded.
1089            if newHF < health {
1090                // Compute the maximum repay allowed by available seize collateral (Rcap), preserving R<->S pricing relation.
1091                // uAllowed = seizeTrue * Pc / (1 + LB)
1092            let uAllowedMax = FlowALPMath.div(trueCollateralSeize * Pc, (FlowALPMath.one + LB))
1093            var repayCapBySeize = FlowALPMath.div(uAllowedMax * BF, Pd)
1094                if repayCapBySeize > trueDebt { repayCapBySeize = trueDebt }
1095
1096                var bestHF: UFix128 = health
1097                var bestRepayTrue: UFix128 = 0.0 as UFix128
1098                var bestSeizeTrue: UFix128 = 0.0 as UFix128
1099
1100                // If nothing can be repaid or seized, abort with no quote
1101                if repayCapBySeize == FlowALPMath.zero || trueCollateralSeize == FlowALPMath.zero {
1102                    return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: health)
1103                }
1104
1105                // Discrete bounded search over repay in [1..repayCapBySeize]
1106                // Use up to 16 steps to balance precision and cost
1107                let stepsU: UFix128 = FlowALPMath.toUFix128(16.0)
1108                var step: UFix128 = repayCapBySeize / stepsU
1109                if step == FlowALPMath.zero { step = FlowALPMath.one }
1110
1111                var r: UFix128 = step
1112                while r <= repayCapBySeize {
1113                    // Compute S for this R under pricing relation, capped by available collateral
1114                    let uForR = FlowALPMath.div(r * Pd, BF)
1115                    var sForR = FlowALPMath.div(uForR * (FlowALPMath.one + LB), Pc)
1116                    if sForR > trueCollateralSeize { sForR = trueCollateralSeize }
1117
1118                    // Compute resulting HF
1119                    let repayEffC = FlowALPMath.div(r * Pd, BF)
1120                    let seizeEffC = sForR * (Pc * CF)
1121                    let newEffCollC = effColl > seizeEffC ? effColl - seizeEffC : FlowALPMath.zero
1122                    let newEffDebtC = effDebt > repayEffC ? effDebt - repayEffC : FlowALPMath.zero
1123                    let newHFC = newEffDebtC == FlowALPMath.zero ? UFix128.max : FlowALPMath.div(newEffCollC * FlowALPMath.one, newEffDebtC)
1124
1125                    if newHFC > bestHF {
1126                        bestHF = newHFC
1127                        bestRepayTrue = r
1128                        bestSeizeTrue = sForR
1129                    }
1130
1131                    // Advance; ensure we always reach the cap
1132                    let next = r + step
1133                    if next > repayCapBySeize { break }
1134                    r = next
1135                }
1136
1137                // Also evaluate at the cap explicitly (in case step didn't land exactly)
1138                let rCap = repayCapBySeize
1139                let uForR2 = FlowALPMath.div(rCap * Pd, BF)
1140                var sForR2 = FlowALPMath.div(uForR2 * (FlowALPMath.one + LB), Pc)
1141                if sForR2 > trueCollateralSeize { sForR2 = trueCollateralSeize }
1142                let repayEffC2 = FlowALPMath.div(rCap * Pd, BF)
1143                let seizeEffC2 = sForR2 * (Pc * CF)
1144                let newEffCollC2 = effColl > seizeEffC2 ? effColl - seizeEffC2 : FlowALPMath.zero
1145                let newEffDebtC2 = effDebt > repayEffC2 ? effDebt - repayEffC2 : FlowALPMath.zero
1146                let newHFC2 = newEffDebtC2 == FlowALPMath.zero ? UFix128.max : FlowALPMath.div(newEffCollC2 * FlowALPMath.one, newEffDebtC2)
1147                if newHFC2 > bestHF {
1148                    bestHF = newHFC2
1149                    bestRepayTrue = rCap
1150                    bestSeizeTrue = sForR2
1151                }
1152
1153                if bestHF > health && bestRepayTrue > FlowALPMath.zero && bestSeizeTrue > FlowALPMath.zero {
1154                    let repayExactBest = FlowALPMath.toUFix64RoundUp(bestRepayTrue)
1155                    let seizeExactBest = FlowALPMath.toUFix64RoundUp(bestSeizeTrue)
1156                    log("[LIQ][QUOTE][FALLBACK][SEARCH] repayExact=\(repayExactBest) seizeExact=\(seizeExactBest)")
1157                    return FlowALP.LiquidationQuote(requiredRepay: repayExactBest, seizeType: seizeType, seizeAmount: seizeExactBest, newHF: bestHF)
1158                }
1159
1160                // No improving pair found
1161                return FlowALP.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: health)
1162            }
1163
1164            log("[LIQ][QUOTE] repayExact=\(repayExact) seizeExact=\(seizeExact) trueCollateralSeize=\(FlowALPMath.toUFix64Round(trueCollateralSeize))")
1165            return FlowALP.LiquidationQuote(requiredRepay: repayExact, seizeType: seizeType, seizeAmount: seizeExact, newHF: newHF)
1166        }
1167
1168        /// Returns the quantity of funds of a specified token which would need to be deposited in order to bring the
1169        /// position to the target health assuming we also withdraw a specified amount of another token. This function
1170        /// will return 0.0 if the position would already be at or over the target health value after the proposed
1171        /// withdrawal.
1172        access(all) fun fundsRequiredForTargetHealthAfterWithdrawing(
1173            pid: UInt64,
1174            depositType: Type,
1175            targetHealth: UFix128,
1176            withdrawType: Type,
1177            withdrawAmount: UFix64
1178        ): UFix64 {
1179            if self.debugLogging { log("    [CONTRACT] fundsRequiredForTargetHealthAfterWithdrawing(pid: \(pid), depositType: \(depositType.contractName!), targetHealth: \(targetHealth), withdrawType: \(withdrawType.contractName!), withdrawAmount: \(withdrawAmount))") }
1180            if depositType == withdrawType && withdrawAmount > 0.0 {
1181                // If the deposit and withdrawal types are the same, we compute the required deposit assuming
1182                // no withdrawal (which is less work) and increase that by the withdraw amount at the end
1183                return self.fundsRequiredForTargetHealth(pid: pid, type: depositType, targetHealth: targetHealth) + withdrawAmount
1184            }
1185
1186            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1187            let position = self._borrowPosition(pid: pid)
1188
1189            let adjusted = self.computeAdjustedBalancesAfterWithdrawal(
1190                balanceSheet: balanceSheet,
1191                position: position,
1192                withdrawType: withdrawType,
1193                withdrawAmount: withdrawAmount
1194            )
1195
1196            return self.computeRequiredDepositForHealth(
1197                position: position,
1198                depositType: depositType,
1199                withdrawType: withdrawType,
1200                effectiveCollateral: adjusted.effectiveCollateral,
1201                effectiveDebt: adjusted.effectiveDebt,
1202                targetHealth: targetHealth
1203            )
1204        }
1205
1206        /// Permissionless liquidation: keeper repays exactly the required amount to reach target HF and receives seized collateral
1207        access(all) fun liquidateRepayForSeize(
1208            pid: UInt64,
1209            debtType: Type,
1210            maxRepayAmount: UFix64,
1211            seizeType: Type,
1212            minSeizeAmount: UFix64,
1213            from: @{FungibleToken.Vault}
1214        ): @LiquidationResult {
1215            pre {
1216                self.globalLedger[debtType] != nil: "Invalid debt type \(debtType.identifier)"
1217                self.globalLedger[seizeType] != nil: "Invalid seize type \(seizeType.identifier)"
1218            }
1219            // Pause/warm-up checks
1220            self._assertLiquidationsActive()
1221
1222            // Quote required repay and seize
1223            let quote = self.quoteLiquidation(pid: pid, debtType: debtType, seizeType: seizeType)
1224            assert(quote.requiredRepay > 0.0, message: "Position not liquidatable or already healthy")
1225            assert(maxRepayAmount >= quote.requiredRepay, message: "Insufficient max repay")
1226            assert(quote.seizeAmount >= minSeizeAmount, message: "Seize amount below minimum")
1227
1228            // Ensure internal reserves exist for seizeType and debtType
1229            if self.reserves[seizeType] == nil {
1230                self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType)
1231            }
1232            if self.reserves[debtType] == nil {
1233                self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType)
1234            }
1235
1236            // Move repay tokens into reserves (repay vault must exactly match requiredRepay)
1237            assert(from.getType() == debtType, message: "Vault type mismatch for repay")
1238            assert(from.balance >= quote.requiredRepay, message: "Repay vault balance must be at least requiredRepay")
1239            let toUse <- from.withdraw(amount: quote.requiredRepay)
1240            let debtReserveRef = (&self.reserves[debtType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1241            debtReserveRef.deposit(from: <-toUse)
1242
1243            // Reduce borrower's debt position by repayAmount
1244            let position = self._borrowPosition(pid: pid)
1245            let debtState = self._borrowUpdatedTokenState(type: debtType)
1246            let repayUint = FlowALPMath.toUFix128(quote.requiredRepay)
1247            if position.balances[debtType] == nil {
1248                position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0 as UFix128)
1249            }
1250            position.balances[debtType]!.recordDeposit(amount: repayUint, tokenState: debtState)
1251
1252            // Withdraw seized collateral from position and send to liquidator
1253            let seizeState = self._borrowUpdatedTokenState(type: seizeType)
1254            let seizeUint = FlowALPMath.toUFix128(quote.seizeAmount)
1255            if position.balances[seizeType] == nil {
1256                position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
1257            }
1258            position.balances[seizeType]!.recordWithdrawal(amount: seizeUint, tokenState: seizeState)
1259            let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1260            let payout <- seizeReserveRef.withdraw(amount: quote.seizeAmount)
1261
1262            let actualNewHF = self.positionHealth(pid: pid)
1263            // Ensure realized HF is not materially below quoted HF (allow tiny rounding tolerance)
1264            let expectedHF = quote.newHF
1265            let hfTolerance: UFix128 = FlowALPMath.toUFix128(0.00001)
1266            assert(actualNewHF + hfTolerance >= expectedHF, message: "Post-liquidation HF below expected")
1267
1268            emit LiquidationExecuted(pid: pid, poolUUID: self.uuid, debtType: debtType.identifier, repayAmount: quote.requiredRepay, seizeType: seizeType.identifier, seizeAmount: quote.seizeAmount, newHF: actualNewHF)
1269
1270            return <- create LiquidationResult(seized: <-payout, remainder: <-from)
1271        }
1272
1273        /// Liquidation via DEX: seize collateral, swap via allowlisted Swapper to debt token, repay debt
1274        access(all) fun liquidateViaDex(
1275            pid: UInt64,
1276            debtType: Type,
1277            seizeType: Type,
1278            maxSeizeAmount: UFix64,
1279            minRepayAmount: UFix64,
1280            swapper: {DeFiActions.Swapper},
1281            quote: {DeFiActions.Quote}?
1282        ) {
1283            pre {
1284                self.globalLedger[debtType] != nil: "Invalid debt type \(debtType.identifier)"
1285                self.globalLedger[seizeType] != nil: "Invalid seize type \(seizeType.identifier)"
1286                !self.liquidationsPaused: "Liquidations paused"
1287            }
1288            self._assertLiquidationsActive()
1289
1290            // Ensure reserve vaults exist for both tokens
1291            if self.reserves[seizeType] == nil { self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType) }
1292            if self.reserves[debtType] == nil { self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) }
1293
1294            // Validate position is liquidatable
1295            let health = self.positionHealth(pid: pid)
1296            assert(health < FlowALPMath.one, message: "Position not liquidatable")
1297            assert(self.isLiquidatable(pid: pid), message: "Position \(pid) is not liquidatable")
1298
1299            // Internal quote to determine required seize (capped by max)
1300            let internalQuote = self.quoteLiquidation(pid: pid, debtType: debtType, seizeType: seizeType)
1301            var requiredSeize = internalQuote.seizeAmount
1302            if requiredSeize > maxSeizeAmount { requiredSeize = maxSeizeAmount }
1303            assert(requiredSeize > 0.0, message: "Nothing to seize")
1304
1305            // Allowlist/type checks
1306            assert(self.allowedSwapperTypes[swapper.getType()] == true, message: "Swapper not allowlisted")
1307            assert(swapper.inType() == seizeType, message: "Swapper must accept seizeType \(seizeType.identifier)")
1308            assert(swapper.outType() == debtType, message: "Swapper must output debtType \(debtType.identifier)")
1309
1310            // Oracle vs DEX price deviation guard
1311            let Pc = self.priceOracle.price(ofToken: seizeType)!
1312            let Pd = self.priceOracle.price(ofToken: debtType)!
1313            let dexQuote = quote != nil ? quote! : swapper.quoteOut(forProvided: requiredSeize, reverse: false)
1314            let dexOut = dexQuote.outAmount
1315            let impliedPrice = dexOut / requiredSeize
1316            let oraclePrice = Pd / Pc
1317            let deviation = impliedPrice > oraclePrice ? impliedPrice - oraclePrice : oraclePrice - impliedPrice
1318            let deviationBps = UInt16((deviation / oraclePrice) * 10000.0)
1319            assert(deviationBps <= self.dexOracleDeviationBps, message: "DEX price deviates too high")
1320
1321            // Seize collateral and swap
1322            let seized <- self.internalSeize(pid: pid, tokenType: seizeType, amount: requiredSeize)
1323            let outDebt <- swapper.swap(quote: dexQuote, inVault: <-seized)
1324            assert(outDebt.getType() == debtType, message: "Swapper returned wrong out type")
1325
1326            // Slippage guard if quote provided
1327            var slipBps: UInt16 = 0
1328            // Slippage vs expected from oracle prices
1329            let expectedOutFromOracle = requiredSeize * (Pd / Pc)
1330            if expectedOutFromOracle > 0.0 {
1331                let diff: UFix64 = outDebt.balance > expectedOutFromOracle ? outDebt.balance - expectedOutFromOracle : expectedOutFromOracle - outDebt.balance
1332                let frac: UFix64 = diff / expectedOutFromOracle
1333                let bpsU: UFix64 = frac * 10000.0
1334                slipBps = UInt16(bpsU)
1335                assert(UInt64(slipBps) <= self.dexMaxSlippageBps, message: "Swap slippage too high")
1336            }
1337
1338            // Repay debt using swap output
1339            let repaid = self.internalRepay(pid: pid, from: <-outDebt)
1340            assert(repaid >= minRepayAmount, message: "Insufficient repay after swap - required \(minRepayAmount) but repaid \(repaid)")
1341            // Optional safety: ensure improved health meets target
1342            let postHF = self.positionHealth(pid: pid)
1343            assert(postHF >= self.liquidationTargetHF, message: "Post-liquidation HF below target")
1344
1345            emit LiquidationExecutedViaDex(
1346                pid: pid,
1347                poolUUID: self.uuid,
1348                seizeType: seizeType.identifier,
1349                seized: requiredSeize,
1350                debtType: debtType.identifier,
1351                repaid: repaid,
1352                slippageBps: slipBps,
1353                newHF: self.positionHealth(pid: pid)
1354            )
1355        }
1356
1357        // Internal helpers for DEX liquidation path (resource-scoped)
1358        access(self) fun internalSeize(pid: UInt64, tokenType: Type, amount: UFix64): @{FungibleToken.Vault} {
1359            let position = self._borrowPosition(pid: pid)
1360            let tokenState = self._borrowUpdatedTokenState(type: tokenType)
1361            let seizeUint = FlowALPMath.toUFix128(amount)
1362            if position.balances[tokenType] == nil {
1363                position.balances[tokenType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
1364            }
1365            position.balances[tokenType]!.recordWithdrawal(amount: seizeUint, tokenState: tokenState)
1366            if self.reserves[tokenType] == nil {
1367                self.reserves[tokenType] <-! DeFiActionsUtils.getEmptyVault(tokenType)
1368            }
1369            let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1370            return <- reserveRef.withdraw(amount: amount)
1371        }
1372
1373        access(self) fun internalRepay(pid: UInt64, from: @{FungibleToken.Vault}): UFix64 {
1374            let debtType = from.getType()
1375            if self.reserves[debtType] == nil {
1376                self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType)
1377            }
1378            let toDeposit <- from
1379            let amount = toDeposit.balance
1380            let reserveRef = (&self.reserves[debtType] as &{FungibleToken.Vault}?)!
1381            reserveRef.deposit(from: <-toDeposit)
1382            let position = self._borrowPosition(pid: pid)
1383            let debtState = self._borrowUpdatedTokenState(type: debtType)
1384            let repayUint = FlowALPMath.toUFix128(amount)
1385            if position.balances[debtType] == nil {
1386                position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0 as UFix128)
1387            }
1388            position.balances[debtType]!.recordDeposit(amount: repayUint, tokenState: debtState)
1389            return amount
1390        }
1391
1392        access(self) fun computeAdjustedBalancesAfterWithdrawal(
1393            balanceSheet: BalanceSheet,
1394            position: &InternalPosition,
1395            withdrawType: Type,
1396            withdrawAmount: UFix64
1397        ): BalanceSheet {
1398            var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral
1399            var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt
1400
1401            if withdrawAmount == 0.0 {
1402                return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal)
1403            }
1404            if self.debugLogging {
1405                log("    [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)")
1406                log("    [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)")
1407            }
1408
1409            let withdrawAmountU = FlowALPMath.toUFix128(withdrawAmount)
1410            let withdrawPrice2 = FlowALPMath.toUFix128(self.priceOracle.price(ofToken: withdrawType)!)
1411            let withdrawBorrowFactor2 = FlowALPMath.toUFix128(self.borrowFactor[withdrawType]!)
1412
1413            let maybeBalance = position.balances[withdrawType]
1414                if maybeBalance == nil || maybeBalance!.direction == BalanceDirection.Debit {
1415                    // If the position doesn't have any collateral for the withdrawn token, we can just compute how much
1416                    // additional effective debt the withdrawal will create.
1417                    effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt +
1418                        FlowALPMath.div(withdrawAmountU * withdrawPrice2, withdrawBorrowFactor2)
1419                } else {
1420                    let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType)
1421
1422                    // The user has a collateral position in the given token, we need to figure out if this withdrawal
1423                    // will flip over into debt, or just draw down the collateral.
1424                    let collateralBalance = maybeBalance!.scaledBalance
1425                    let trueCollateral = FlowALP.scaledBalanceToTrueBalance(collateralBalance,
1426                        interestIndex: withdrawTokenState.creditInterestIndex
1427                    )
1428                    let collateralFactor = FlowALPMath.toUFix128(self.collateralFactor[withdrawType]!)
1429                    if trueCollateral >= withdrawAmountU {
1430                        // This withdrawal will draw down collateral, but won't create debt, we just need to account
1431                        // for the collateral decrease.
1432                        effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral -
1433                            (withdrawAmountU * withdrawPrice2) * collateralFactor
1434                    } else {
1435                        // The withdrawal will wipe out all of the collateral, and create some debt.
1436                        effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt +
1437                            FlowALPMath.div((withdrawAmountU - trueCollateral) * withdrawPrice2, withdrawBorrowFactor2)
1438                        effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral -
1439                            (trueCollateral * withdrawPrice2) * collateralFactor
1440                    }
1441                }
1442
1443            return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal)
1444        }
1445
1446
1447        access(self) fun computeRequiredDepositForHealth(
1448            position: &InternalPosition,
1449            depositType: Type,
1450            withdrawType: Type,
1451            effectiveCollateral: UFix128,
1452            effectiveDebt: UFix128,
1453            targetHealth: UFix128
1454        ): UFix64 {
1455            var effectiveCollateralAfterWithdrawal = effectiveCollateral
1456            var effectiveDebtAfterWithdrawal = effectiveDebt
1457
1458            if self.debugLogging {
1459                log("    [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)")
1460                log("    [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)")
1461            }
1462
1463            // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!)
1464            // Now we can figure out how many of the given token would need to be deposited to bring the position
1465            // to the target health value.
1466            var healthAfterWithdrawal = FlowALP.healthComputation(
1467                effectiveCollateral: effectiveCollateralAfterWithdrawal,
1468                effectiveDebt: effectiveDebtAfterWithdrawal
1469            )
1470            if self.debugLogging { log("    [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") }
1471
1472            if healthAfterWithdrawal >= targetHealth {
1473                // The position is already at or above the target health, so we don't need to deposit anything.
1474                return 0.0
1475            }
1476
1477            // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep
1478            // track of the number of tokens that went towards paying off debt.
1479            var debtTokenCount: UFix128 = FlowALPMath.zero
1480            let depositPrice = FlowALPMath.toUFix128(self.priceOracle.price(ofToken: depositType)!)
1481            let depositBorrowFactor = FlowALPMath.toUFix128(self.borrowFactor[depositType]!)
1482            let withdrawBorrowFactor = FlowALPMath.toUFix128(self.borrowFactor[withdrawType]!)
1483            let maybeBalance = position.balances[depositType]
1484            if maybeBalance?.direction == BalanceDirection.Debit {
1485                // The user has a debt position in the given token, we start by looking at the health impact of paying off
1486                // the entire debt.
1487                let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
1488                let debtBalance = maybeBalance!.scaledBalance
1489                let trueDebt = FlowALP.scaledBalanceToTrueBalance(debtBalance,
1490                    interestIndex: depositTokenState.debitInterestIndex
1491                )
1492                let debtEffectiveValue = FlowALPMath.div(depositPrice * trueDebt, depositBorrowFactor)
1493
1494                // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal,
1495                // it means we can pay off all debt
1496            var effectiveDebtAfterPayment: UFix128 = FlowALPMath.zero
1497                if debtEffectiveValue <= effectiveDebtAfterWithdrawal {
1498                    effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue
1499                }
1500
1501                // Check what the new health would be if we paid off all of this debt
1502                let potentialHealth = FlowALP.healthComputation(
1503                    effectiveCollateral: effectiveCollateralAfterWithdrawal,
1504                    effectiveDebt: effectiveDebtAfterPayment
1505                )
1506
1507                // Does paying off all of the debt reach the target health? Then we're done.
1508                if potentialHealth >= targetHealth {
1509                    // We can reach the target health by paying off some or all of the debt. We can easily
1510                    // compute how many units of the token would be needed to reach the target health.
1511                    let healthChange = targetHealth - healthAfterWithdrawal
1512                    let requiredEffectiveDebt = effectiveDebtAfterWithdrawal - FlowALPMath.div(
1513                            effectiveCollateralAfterWithdrawal,
1514                            targetHealth
1515                        )
1516
1517                    // The amount of the token to pay back, in units of the token.
1518                    let paybackAmount = FlowALPMath.div(requiredEffectiveDebt * depositBorrowFactor, depositPrice)
1519
1520                    if self.debugLogging { log("    [CONTRACT] paybackAmount: \(paybackAmount)") }
1521
1522                    return FlowALPMath.toUFix64RoundUp(paybackAmount)
1523                } else {
1524                    // We can pay off the entire debt, but we still need to deposit more to reach the target health.
1525                    // We have logic below that can determine the collateral deposition required to reach the target health
1526                    // from this new health position. Rather than copy that logic here, we fall through into it. But first
1527                    // we have to record the amount of tokens that went towards debt payback and adjust the effective
1528                    // debt to reflect that it has been paid off.
1529                    debtTokenCount = FlowALPMath.div(trueDebt, depositPrice)
1530                    // Ensure we don't underflow
1531                    if debtEffectiveValue <= effectiveDebtAfterWithdrawal {
1532                        effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue
1533                    } else {
1534                        effectiveDebtAfterWithdrawal = 0.0 as UFix128
1535                    }
1536                    healthAfterWithdrawal = potentialHealth
1537                }
1538            }
1539
1540            // At this point, we're either dealing with a position that didn't have a debt position in the deposit
1541            // token, or we've accounted for the debt payoff and adjusted the effective debt above.
1542            // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the
1543            // target health. We can rearrange the health equation to solve for the required collateral:
1544
1545            // We need to increase the effective collateral from its current value to the required value, so we
1546            // multiply the required health change by the effective debt, and turn that into a token amount.
1547            let healthChangeU = targetHealth - healthAfterWithdrawal
1548            // TODO: apply the same logic as below to the early return blocks above
1549            let depositCollateralFactor = FlowALPMath.toUFix128(self.collateralFactor[depositType]!)
1550            var requiredEffectiveCollateral = healthChangeU * effectiveDebtAfterWithdrawal
1551            requiredEffectiveCollateral = FlowALPMath.div(requiredEffectiveCollateral, depositCollateralFactor)
1552
1553            // The amount of the token to deposit, in units of the token.
1554            let collateralTokenCount = FlowALPMath.div(requiredEffectiveCollateral, depositPrice)
1555            if self.debugLogging {
1556                log("    [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)")
1557                log("    [CONTRACT] collateralTokenCount: \(collateralTokenCount)")
1558                log("    [CONTRACT] debtTokenCount: \(debtTokenCount)")
1559                log("    [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)")
1560            }
1561
1562            // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt.
1563            return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount)
1564        }
1565
1566        /// Returns the quantity of the specified token that could be withdrawn while still keeping the position's
1567        /// health at or above the provided target.
1568        access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix128): UFix64 {
1569            return self.fundsAvailableAboveTargetHealthAfterDepositing(
1570                pid: pid,
1571                withdrawType: type,
1572                targetHealth: targetHealth,
1573                depositType: self.defaultToken,
1574                depositAmount: 0.0
1575            )
1576        }
1577
1578        /// Returns the quantity of the specified token that could be withdrawn while still keeping the position's health
1579        /// at or above the provided target, assuming we also deposit a specified amount of another token.
1580        access(all) fun fundsAvailableAboveTargetHealthAfterDepositing(
1581            pid: UInt64,
1582            withdrawType: Type,
1583            targetHealth: UFix128,
1584            depositType: Type,
1585            depositAmount: UFix64
1586        ): UFix64 {
1587            if self.debugLogging { log("    [CONTRACT] fundsAvailableAboveTargetHealthAfterDepositing(pid: \(pid), withdrawType: \(withdrawType.contractName!), targetHealth: \(targetHealth), depositType: \(depositType.contractName!), depositAmount: \(depositAmount))") }
1588            if depositType == withdrawType && depositAmount > 0.0 {
1589                // If the deposit and withdrawal types are the same, we compute the available funds assuming
1590                // no deposit (which is less work) and increase that by the deposit amount at the end
1591                return self.fundsAvailableAboveTargetHealth(pid: pid, type: withdrawType, targetHealth: targetHealth) + depositAmount
1592            }
1593
1594            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1595            let position = self._borrowPosition(pid: pid)
1596
1597            let adjusted = self.computeAdjustedBalancesAfterDeposit(
1598                balanceSheet: balanceSheet,
1599                position: position,
1600                depositType: depositType,
1601                depositAmount: depositAmount
1602            )
1603
1604            return self.computeAvailableWithdrawal(
1605                position: position,
1606                withdrawType: withdrawType,
1607                effectiveCollateral: adjusted.effectiveCollateral,
1608                effectiveDebt: adjusted.effectiveDebt,
1609                targetHealth: targetHealth
1610            )
1611        }
1612
1613        // Helper function to compute balances after deposit
1614        access(self) fun computeAdjustedBalancesAfterDeposit(
1615            balanceSheet: BalanceSheet,
1616            position: &InternalPosition,
1617            depositType: Type,
1618            depositAmount: UFix64
1619        ): BalanceSheet {
1620            var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral
1621            var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt
1622
1623            if self.debugLogging {
1624                log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
1625                log("    [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)")
1626            }
1627            if depositAmount == 0.0 {
1628                return BalanceSheet(effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit)
1629            }
1630
1631            let depositAmountCasted = FlowALPMath.toUFix128(depositAmount)
1632            let depositPriceCasted = FlowALPMath.toUFix128(self.priceOracle.price(ofToken: depositType)!)
1633            let depositBorrowFactorCasted = FlowALPMath.toUFix128(self.borrowFactor[depositType]!)
1634            let depositCollateralFactorCasted = FlowALPMath.toUFix128(self.collateralFactor[depositType]!)
1635            let maybeBalance = position.balances[depositType]
1636                if maybeBalance == nil || maybeBalance!.direction == BalanceDirection.Credit {
1637                    // If there's no debt for the deposit token, we can just compute how much additional effective collateral the deposit will create.
1638                    effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral +
1639                        (depositAmountCasted * depositPriceCasted) * depositCollateralFactorCasted
1640                } else {
1641                    let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
1642
1643                    // The user has a debt position in the given token, we need to figure out if this deposit
1644                    // will result in net collateral, or just bring down the debt.
1645                    let debtBalance = maybeBalance!.scaledBalance
1646                    let trueDebt = FlowALP.scaledBalanceToTrueBalance(debtBalance,
1647                        interestIndex: depositTokenState.debitInterestIndex
1648                    )
1649                    if self.debugLogging { log("    [CONTRACT] trueDebt: \(trueDebt)") }
1650
1651                    if trueDebt >= depositAmountCasted {
1652                        // This deposit will pay down some debt, but won't result in net collateral, we
1653                        // just need to account for the debt decrease.
1654                        // TODO - validate if this should deal with withdrawType or depositType
1655                        effectiveDebtAfterDeposit = balanceSheet.effectiveDebt -
1656                            FlowALPMath.div(depositAmountCasted * depositPriceCasted, depositBorrowFactorCasted)
1657                    } else {
1658                        // The deposit will wipe out all of the debt, and create some collateral.
1659                        // TODO - validate if this should deal with withdrawType or depositType
1660                        effectiveDebtAfterDeposit = balanceSheet.effectiveDebt -
1661                            FlowALPMath.div(trueDebt * depositPriceCasted, depositBorrowFactorCasted)
1662                        effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral +
1663                            (depositAmountCasted - trueDebt) * depositPriceCasted * depositCollateralFactorCasted
1664                    }
1665                }
1666
1667            if self.debugLogging {
1668                log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
1669                log("    [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)")
1670            }
1671
1672            // We now have new effective collateral and debt values that reflect the proposed deposit (if any!)
1673            // Now we can figure out how many of the withdrawal token are available while keeping the position
1674            // at or above the target health value.
1675            return BalanceSheet(effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit)
1676        }
1677
1678        // Helper function to compute available withdrawal
1679        access(self) fun computeAvailableWithdrawal(
1680            position: &InternalPosition,
1681            withdrawType: Type,
1682            effectiveCollateral: UFix128,
1683            effectiveDebt: UFix128,
1684            targetHealth: UFix128 
1685        ): UFix64 {
1686            var effectiveCollateralAfterDeposit = effectiveCollateral
1687            var effectiveDebtAfterDeposit = effectiveDebt
1688
1689            var healthAfterDeposit = FlowALP.healthComputation(
1690                effectiveCollateral: effectiveCollateralAfterDeposit,
1691                effectiveDebt: effectiveDebtAfterDeposit
1692            )
1693            if self.debugLogging { log("    [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") }
1694
1695            if healthAfterDeposit <= targetHealth {
1696                // The position is already at or below the provided target health, so we can't withdraw anything.
1697                return 0.0
1698            }
1699
1700            // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep
1701            // track of the number of tokens that are available from collateral
1702            var collateralTokenCount: UFix128 = FlowALPMath.zero
1703
1704            let withdrawPrice = FlowALPMath.toUFix128(self.priceOracle.price(ofToken: withdrawType)!)
1705            let withdrawCollateralFactor = FlowALPMath.toUFix128(self.collateralFactor[withdrawType]!)
1706            let withdrawBorrowFactor = FlowALPMath.toUFix128(self.borrowFactor[withdrawType]!)
1707
1708            let maybeBalance = position.balances[withdrawType]
1709            if maybeBalance?.direction == BalanceDirection.Credit {
1710                // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all
1711                // of that collateral
1712                let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType)
1713                let creditBalance = maybeBalance!.scaledBalance
1714                let trueCredit = FlowALP.scaledBalanceToTrueBalance(creditBalance,
1715                    interestIndex: withdrawTokenState.creditInterestIndex
1716                )
1717                let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor
1718
1719                // Check what the new health would be if we took out all of this collateral
1720                let potentialHealth = FlowALP.healthComputation(
1721                    effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract?
1722                    effectiveDebt: effectiveDebtAfterDeposit
1723                )
1724
1725
1726                // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only.
1727                if potentialHealth <= targetHealth {
1728                    // We will hit the health target before using up all of the withdraw token credit. We can easily
1729                    // compute how many units of the token would bring the position down to the target health.
1730                // We will hit the health target before using up all available withdraw credit.
1731
1732            let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit)
1733                    if self.debugLogging { log("    [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") }
1734
1735                    // The amount of the token we can take using that amount of health
1736            let availableTokenCount = FlowALPMath.div(FlowALPMath.div(availableEffectiveValue, withdrawCollateralFactor), withdrawPrice)
1737                    if self.debugLogging { log("    [CONTRACT] availableTokenCount: \(availableTokenCount)") }
1738
1739                    return FlowALPMath.toUFix64RoundDown(availableTokenCount)
1740                } else {
1741                    // We can flip this credit position into a debit position, before hitting the target health.
1742                    // We have logic below that can determine health changes for debit positions. We've copied it here
1743                    // with an added handling for the case where the health after deposit is an edgecase
1744                    collateralTokenCount = trueCredit
1745                    effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue
1746                    if self.debugLogging {
1747                        log("    [CONTRACT] collateralTokenCount: \(collateralTokenCount)")
1748                        log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
1749                    }
1750
1751                    // We can calculate the available debt increase that would bring us to the target health
1752                    var availableDebtIncrease = FlowALPMath.div(effectiveCollateralAfterDeposit, targetHealth) - effectiveDebtAfterDeposit
1753                    let availableTokens = FlowALPMath.div(availableDebtIncrease * withdrawBorrowFactor, withdrawPrice)
1754                    if self.debugLogging {
1755                        log("    [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)")
1756                        log("    [CONTRACT] availableTokens: \(availableTokens)")
1757                        log("    [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)")
1758                    }
1759                    return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount)
1760                }
1761            }
1762
1763            // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw
1764            // token, or we've accounted for the credit balance and adjusted the effective collateral above.
1765
1766            // We can calculate the available debt increase that would bring us to the target health
1767            var availableDebtIncrease = FlowALPMath.div(effectiveCollateralAfterDeposit, targetHealth) - effectiveDebtAfterDeposit
1768            let availableTokens = FlowALPMath.div(availableDebtIncrease * withdrawBorrowFactor, withdrawPrice)
1769            if self.debugLogging {
1770                log("    [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)")
1771                log("    [CONTRACT] availableTokens: \(availableTokens)")
1772                log("    [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)")
1773            }
1774            return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount)
1775        }
1776
1777        /// Returns the position's health if the given amount of the specified token were deposited
1778        access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix128 {
1779            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1780            let position = self._borrowPosition(pid: pid)
1781            let tokenState = self._borrowUpdatedTokenState(type: type)
1782
1783            var effectiveCollateralIncrease: UFix128 = FlowALPMath.zero
1784            var effectiveDebtDecrease: UFix128 = FlowALPMath.zero
1785
1786            let amountU = FlowALPMath.toUFix128(amount)
1787            let price = FlowALPMath.toUFix128(self.priceOracle.price(ofToken: type)!)
1788            let collateralFactor = FlowALPMath.toUFix128(self.collateralFactor[type]!)
1789            let borrowFactor = FlowALPMath.toUFix128(self.borrowFactor[type]!)
1790            if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Credit {
1791                // Since the user has no debt in the given token, we can just compute how much
1792                // additional collateral this deposit will create.
1793                effectiveCollateralIncrease = (amountU * price) * collateralFactor
1794            } else {
1795                // The user has a debit position in the given token, we need to figure out if this deposit
1796                // will only pay off some of the debt, or if it will also create new collateral.
1797                let debtBalance = position.balances[type]!.scaledBalance
1798                let trueDebt = FlowALP.scaledBalanceToTrueBalance(debtBalance,
1799                    interestIndex: tokenState.debitInterestIndex
1800                )
1801
1802                if trueDebt >= amountU {
1803                    // This deposit will wipe out some or all of the debt, but won't create new collateral, we
1804                    // just need to account for the debt decrease.
1805                    effectiveDebtDecrease = FlowALPMath.div(amountU * price, borrowFactor)
1806                } else {
1807                    // This deposit will wipe out all of the debt, and create new collateral.
1808                    effectiveCollateralIncrease = ((amountU - trueDebt) * price) * collateralFactor
1809                }
1810            }
1811
1812            return FlowALP.healthComputation(
1813                effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease,
1814                effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease
1815            )
1816        }
1817
1818        // Returns health value of this position if the given amount of the specified token were withdrawn without
1819        // using the top up source.
1820        // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates
1821        // that the proposed withdrawal would fail (unless a top up source is available and used).
1822        access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix128 {
1823            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1824            let position = self._borrowPosition(pid: pid)
1825            let tokenState = self._borrowUpdatedTokenState(type: type)
1826
1827            var effectiveCollateralDecrease: UFix128 = FlowALPMath.zero
1828            var effectiveDebtIncrease: UFix128 = FlowALPMath.zero
1829
1830            let amountU = FlowALPMath.toUFix128(amount)
1831            let price = FlowALPMath.toUFix128(self.priceOracle.price(ofToken: type)!)
1832            let collateralFactor = FlowALPMath.toUFix128(self.collateralFactor[type]!)
1833            let borrowFactor = FlowALPMath.toUFix128(self.borrowFactor[type]!)
1834            if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Debit {
1835                // The user has no credit position in the given token, we can just compute how much
1836                // additional effective debt this withdrawal will create.
1837                effectiveDebtIncrease = FlowALPMath.div(amountU * price, borrowFactor)
1838            } else {
1839                // The user has a credit position in the given token, we need to figure out if this withdrawal
1840                // will only draw down some of the collateral, or if it will also create new debt.
1841                let creditBalance = position.balances[type]!.scaledBalance
1842                let trueCredit = FlowALP.scaledBalanceToTrueBalance(creditBalance,
1843                    interestIndex: tokenState.creditInterestIndex
1844                )
1845
1846                if trueCredit >= amountU {
1847                    // This withdrawal will draw down some collateral, but won't create new debt, we
1848                    // just need to account for the collateral decrease.
1849                    // effectiveCollateralDecrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]!
1850                    effectiveCollateralDecrease = (amountU * price) * collateralFactor
1851                } else {
1852                    // The withdrawal will wipe out all of the collateral, and create new debt.
1853                    effectiveDebtIncrease = FlowALPMath.div((amountU - trueCredit) * price, borrowFactor)
1854                    effectiveCollateralDecrease = (trueCredit * price) * collateralFactor
1855                }
1856            }
1857
1858            return FlowALP.healthComputation(
1859                effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease,
1860                effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease
1861            )
1862        }
1863
1864        ///////////////////////////
1865        // POSITION MANAGEMENT
1866        ///////////////////////////
1867
1868        /// Creates a lending position against the provided collateral funds, depositing the loaned amount to the
1869        /// given Sink. If a Source is provided, the position will be configured to pull loan repayment when the loan
1870        /// becomes undercollateralized, preferring repayment to outright liquidation.
1871        access(EParticipant) fun createPosition(
1872            funds: @{FungibleToken.Vault},
1873            issuanceSink: {DeFiActions.Sink},
1874            repaymentSource: {DeFiActions.Source}?,
1875            pushToDrawDownSink: Bool
1876        ): UInt64 {
1877            pre {
1878                self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier) - not supported by this Pool"
1879            }
1880            // construct a new InternalPosition, assigning it the current position ID
1881            let id = self.nextPositionID
1882            self.nextPositionID = self.nextPositionID + 1
1883            self.positions[id] <-! create InternalPosition()
1884
1885            emit Opened(pid: id, poolUUID: self.uuid)
1886
1887            // assign issuance & repayment connectors within the InternalPosition
1888            let iPos = self._borrowPosition(pid: id)
1889            let fundsType = funds.getType()
1890            iPos.setDrawDownSink(issuanceSink)
1891            if repaymentSource != nil {
1892                iPos.setTopUpSource(repaymentSource)
1893            }
1894
1895            // deposit the initial funds & return the position ID
1896            self.depositAndPush(
1897                pid: id,
1898                from: <-funds,
1899                pushToDrawDownSink: pushToDrawDownSink
1900            )
1901            return id
1902        }
1903
1904        /// Allows anyone to deposit funds into any position. If the provided Vault is not supported by the Pool, the
1905        /// operation reverts.
1906        access(EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) {
1907            self.depositAndPush(pid: pid, from: <-from, pushToDrawDownSink: false)
1908        }
1909
1910        /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option. If
1911        /// `pushToDrawDownSink` is true, excess value putting the position above its max health is pushed to the
1912        /// position's configured `drawDownSink`.
1913        access(EPosition) fun depositAndPush(pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) {
1914            pre {
1915                self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool"
1916                self.globalLedger[from.getType()] != nil: "Invalid token type \(from.getType().identifier) - not supported by this Pool"
1917            }
1918            if self.debugLogging { log("    [CONTRACT] depositAndPush(pid: \(pid), pushToDrawDownSink: \(pushToDrawDownSink))") }
1919
1920            if from.balance == 0.0 {
1921                Burner.burn(<-from)
1922                return
1923            }
1924
1925            // Get a reference to the user's position and global token state for the affected token.
1926            let type = from.getType()
1927            let position = self._borrowPosition(pid: pid)
1928            let tokenState = self._borrowUpdatedTokenState(type: type)
1929            let amount = from.balance
1930            let depositedUUID = from.uuid
1931
1932            // Time-based state is handled by the tokenState() helper function
1933
1934            // Deposit rate limiting: prevent a single large deposit from monopolizing capacity.
1935            // Excess is queued to be processed asynchronously (see asyncUpdatePosition).
1936            let depositAmount = from.balance
1937            let uintDepositAmount = FlowALPMath.toUFix128(depositAmount)
1938            let depositLimit = tokenState.depositLimit()
1939
1940            if depositAmount > depositLimit {
1941                // The deposit is too big, so we need to queue the excess
1942                let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit)
1943
1944                if position.queuedDeposits[type] == nil {
1945                    position.queuedDeposits[type] <-! queuedDeposit
1946                } else {
1947                    position.queuedDeposits[type]!.deposit(from: <-queuedDeposit)
1948                }
1949            }
1950
1951            // If this position doesn't currently have an entry for this token, create one.
1952            if position.balances[type] == nil {
1953                position.balances[type] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
1954            }
1955
1956            // Create vault if it doesn't exist yet
1957            if self.reserves[type] == nil {
1958                self.reserves[type] <-! from.createEmptyVault()
1959            }
1960            let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1961
1962            // Reflect the deposit in the position's balance
1963            position.balances[type]!.recordDeposit(amount: uintDepositAmount, tokenState: tokenState)
1964
1965            // Add the money to the reserves
1966            reserveVault.deposit(from: <-from)
1967
1968            // Rebalancing and queue management
1969            if pushToDrawDownSink {
1970                self.rebalancePosition(pid: pid, force: true)
1971            }
1972
1973            self._queuePositionForUpdateIfNecessary(pid: pid)
1974            emit Deposited(pid: pid, poolUUID: self.uuid, vaultType: type, amount: amount, depositedUUID: depositedUUID)
1975        }
1976
1977        /// Withdraws the requested funds from the specified position. Callers should be careful that the withdrawal
1978        /// does not put their position under its target health, especially if the position doesn't have a configured
1979        /// `topUpSource` from which to repay borrowed funds in the event of undercollaterlization.
1980        access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} {
1981            // Call the enhanced function with pullFromTopUpSource = false for backward compatibility
1982            return <- self.withdrawAndPull(pid: pid, type: type, amount: amount, pullFromTopUpSource: false)
1983        }
1984
1985        /// Withdraws the requested funds from the specified position with the configurable `pullFromTopUpSource`
1986        /// option. If `pullFromTopUpSource` is true, deficient value putting the position below its min health is
1987        /// pulled from the position's configured `topUpSource`.
1988        access(EPosition) fun withdrawAndPull(
1989            pid: UInt64,
1990            type: Type,
1991            amount: UFix64,
1992            pullFromTopUpSource: Bool
1993        ): @{FungibleToken.Vault} {
1994            pre {
1995                self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool"
1996                self.globalLedger[type] != nil: "Invalid token type \(type.identifier) - not supported by this Pool"
1997            }
1998            if self.debugLogging { log("    [CONTRACT] withdrawAndPull(pid: \(pid), type: \(type.identifier), amount: \(amount), pullFromTopUpSource: \(pullFromTopUpSource))") }
1999            if amount == 0.0 {
2000                return <- DeFiActionsUtils.getEmptyVault(type)
2001            }
2002
2003            // Get a reference to the user's position and global token state for the affected token.
2004            let position = self._borrowPosition(pid: pid)
2005            let tokenState = self._borrowUpdatedTokenState(type: type)
2006
2007            // Global interest indices are updated via tokenState() helper
2008
2009            // Preflight to see if the funds are available
2010            let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}?
2011            let topUpType = topUpSource?.getSourceType() ?? self.defaultToken
2012
2013            let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
2014                pid: pid,
2015                depositType: topUpType,
2016                targetHealth: position.minHealth,
2017                withdrawType: type,
2018                withdrawAmount: amount
2019            )
2020
2021            var canWithdraw = false
2022            var usedTopUp = false
2023
2024            if requiredDeposit == 0.0 {
2025                // We can service this withdrawal without any top up
2026                canWithdraw = true
2027            } else {
2028                // We need more funds to service this withdrawal, see if they are available from the top up source
2029                if pullFromTopUpSource && topUpSource != nil {
2030                    // If we have to rebalance, let's try to rebalance to the target health, not just the minimum
2031                    let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
2032                        pid: pid,
2033                        depositType: topUpType,
2034                        targetHealth: position.targetHealth,
2035                        withdrawType: type,
2036                        withdrawAmount: amount
2037                    )
2038
2039                    let pulledVault <- topUpSource!.withdrawAvailable(maxAmount: idealDeposit)
2040                    let pulledAmount = pulledVault.balance
2041
2042                    // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here.
2043                    // The top up source may not have enough funds get us to the target health, but could have
2044                    // enough to keep us over the minimum.
2045                    if pulledAmount >= requiredDeposit {
2046                        // We can service this withdrawal if we deposit funds from our top up source
2047                        self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false)
2048                        usedTopUp = pulledAmount > 0.0
2049                        canWithdraw = true
2050                    } else {
2051                        // We can't get the funds required to service this withdrawal, so we need to redeposit what we got
2052                        self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false)
2053                        usedTopUp = pulledAmount > 0.0
2054                    }
2055                }
2056            }
2057
2058            if !canWithdraw {
2059                // Log detailed information about the failed withdrawal (only if debugging enabled)
2060                if self.debugLogging {
2061                    let availableBalance = self.availableBalance(pid: pid, type: type, pullFromTopUpSource: false)
2062                    log("    [CONTRACT] WITHDRAWAL FAILED:")
2063                    log("    [CONTRACT] Position ID: \(pid)")
2064                    log("    [CONTRACT] Token type: \(type.identifier)")
2065                    log("    [CONTRACT] Requested amount: \(amount)")
2066                    log("    [CONTRACT] Available balance (without topUp): \(availableBalance)")
2067                    log("    [CONTRACT] Required deposit for minHealth: \(requiredDeposit)")
2068                    log("    [CONTRACT] Pull from topUpSource: \(pullFromTopUpSource)")
2069                }
2070
2071                // We can't service this withdrawal, so we just abort
2072                panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal")
2073            }
2074
2075            // If this position doesn't currently have an entry for this token, create one.
2076            if position.balances[type] == nil {
2077                position.balances[type] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
2078            }
2079
2080            let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
2081
2082            // Reflect the withdrawal in the position's balance
2083            let uintAmount = FlowALPMath.toUFix128(amount)
2084            position.balances[type]!.recordWithdrawal(amount: uintAmount, tokenState: tokenState)
2085            // Ensure that this withdrawal doesn't cause the position to be overdrawn.
2086            // Skip the assertion only when a top-up was used in this call and the immediate
2087            // post-withdrawal health is 0 (transitional state before top-up effects fully reflect).
2088            let postHealth = self.positionHealth(pid: pid)
2089            if !(usedTopUp && postHealth == 0.0 as UFix128) {
2090                assert(position.minHealth <= postHealth, message: "Position is overdrawn")
2091            }
2092
2093            // Queue for update if necessary
2094            self._queuePositionForUpdateIfNecessary(pid: pid)
2095
2096            let withdrawn <- reserveVault.withdraw(amount: amount)
2097
2098            emit Withdrawn(pid: pid, poolUUID: self.uuid, vaultType: type, amount: withdrawn.balance, withdrawnUUID: withdrawn.uuid)
2099
2100            return <- withdrawn
2101        }
2102
2103        /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when
2104        /// the position exceeds its maximum health. Note, if a non-nil value is provided, the Sink MUST accept the
2105        /// Pool's default deposits or the operation will revert.
2106        access(EPosition) fun provideDrawDownSink(pid: UInt64, sink: {DeFiActions.Sink}?) {
2107            let position = self._borrowPosition(pid: pid)
2108            position.setDrawDownSink(sink)
2109        }
2110
2111        /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when
2112        /// the position falls below its minimum health which may result in liquidation.
2113        access(EPosition) fun provideTopUpSource(pid: UInt64, source: {DeFiActions.Source}?) {
2114            let position = self._borrowPosition(pid: pid)
2115            position.setTopUpSource(source)
2116        }
2117
2118        // ---- Position health accessors (called via Position using EPosition capability) ----
2119        access(EPosition) view fun readTargetHealth(pid: UInt64): UFix128 {
2120            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2121            return pos.targetHealth
2122        }
2123        access(EPosition) view fun readMinHealth(pid: UInt64): UFix128 {
2124            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2125            return pos.minHealth
2126        }
2127        access(EPosition) view fun readMaxHealth(pid: UInt64): UFix128 {
2128            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2129            return pos.maxHealth
2130        }
2131        access(EPosition) fun writeTargetHealth(pid: UInt64, targetHealth: UFix128) {
2132            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2133            assert(targetHealth >= pos.minHealth, message: "targetHealth must be ≥ minHealth")
2134            assert(targetHealth <= pos.maxHealth, message: "targetHealth must be ≤ maxHealth")
2135            pos.setTargetHealth(targetHealth)
2136        }
2137        access(EPosition) fun writeMinHealth(pid: UInt64, minHealth: UFix128) {
2138            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2139            assert(minHealth <= pos.targetHealth, message: "minHealth must be ≤ targetHealth")
2140            pos.setMinHealth(minHealth)
2141        }
2142        access(EPosition) fun writeMaxHealth(pid: UInt64, maxHealth: UFix128) {
2143            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2144            assert(maxHealth >= pos.targetHealth, message: "maxHealth must be ≥ targetHealth")
2145            pos.setMaxHealth(maxHealth)
2146        }
2147
2148        ///////////////////////
2149        // POOL MANAGEMENT
2150        ///////////////////////
2151
2152        /// Updates liquidation-related parameters (any nil values are ignored)
2153        access(EGovernance) fun setLiquidationParams(
2154            targetHF: UFix128?,
2155            warmupSec: UInt64?,
2156            protocolFeeBps: UInt16?
2157        ) {
2158            var newTarget: UFix128 = self.liquidationTargetHF
2159            var newWarmup: UInt64 = self.liquidationWarmupSec
2160            var newProtocolFee: UInt16 = self.protocolLiquidationFeeBps
2161            if targetHF != nil {
2162                assert(targetHF! > FlowALPMath.one, message: "targetHF must be > 1.0")
2163                self.liquidationTargetHF = targetHF!
2164                newTarget = targetHF!
2165            }
2166            if warmupSec != nil {
2167                self.liquidationWarmupSec = warmupSec!
2168                newWarmup = warmupSec!
2169            }
2170            if protocolFeeBps != nil {
2171                self.protocolLiquidationFeeBps = protocolFeeBps!
2172                newProtocolFee = protocolFeeBps!
2173            }
2174            emit LiquidationParamsUpdated(poolUUID: self.uuid, targetHF: newTarget, warmupSec: newWarmup, protocolFeeBps: newProtocolFee)
2175        }
2176
2177        /// Governance: set DEX oracle deviation guard and toggle allowlisted swapper types
2178        access(EGovernance) fun setDexLiquidationConfig(
2179            dexOracleDeviationBps: UInt16?,
2180            allowSwappers: [Type]?,
2181            disallowSwappers: [Type]?,
2182            dexMaxSlippageBps: UInt64?,
2183            dexMaxRouteHops: UInt64?
2184        ) {
2185            if dexOracleDeviationBps != nil { self.dexOracleDeviationBps = dexOracleDeviationBps! }
2186            if allowSwappers != nil {
2187                for t in allowSwappers! {
2188                    self.allowedSwapperTypes[t] = true
2189                }
2190            }
2191            if disallowSwappers != nil {
2192                for t in disallowSwappers! {
2193                    self.allowedSwapperTypes.remove(key: t)
2194                }
2195            }
2196            if dexMaxSlippageBps != nil { self.dexMaxSlippageBps = dexMaxSlippageBps! }
2197            if dexMaxRouteHops != nil { self.dexMaxRouteHops = dexMaxRouteHops! }
2198        }
2199
2200        /// Pauses or unpauses liquidations; when unpausing, starts a warm-up window
2201        access(EGovernance) fun pauseLiquidations(flag: Bool) {
2202            if flag {
2203                self.liquidationsPaused = true
2204                emit LiquidationsPaused(poolUUID: self.uuid)
2205            } else {
2206                self.liquidationsPaused = false
2207                let now = UInt64(getCurrentBlock().timestamp)
2208                self.lastUnpausedAt = now
2209                emit LiquidationsUnpaused(poolUUID: self.uuid, warmupEndsAt: now + self.liquidationWarmupSec)
2210            }
2211        }
2212
2213        /// Adds a new token type to the pool with the given parameters defining borrowing limits on collateral,
2214        /// interest accumulation, deposit rate limiting, and deposit size capacity
2215        access(EGovernance) fun addSupportedToken(
2216            tokenType: Type,
2217            collateralFactor: UFix64,
2218            borrowFactor: UFix64,
2219            interestCurve: {InterestCurve},
2220            depositRate: UFix64,
2221            depositCapacityCap: UFix64
2222        ) {
2223            pre {
2224                self.globalLedger[tokenType] == nil: "Token type already supported"
2225                tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()):
2226                "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation"
2227                collateralFactor > 0.0 && collateralFactor <= 1.0: "Collateral factor must be between 0 and 1"
2228                borrowFactor > 0.0 && borrowFactor <= 1.0: "Borrow factor must be between 0 and 1"
2229                depositRate > 0.0: "Deposit rate must be positive"
2230                depositCapacityCap > 0.0: "Deposit capacity cap must be positive"
2231                DeFiActionsUtils.definingContractIsFungibleToken(tokenType):
2232                "Invalid token contract definition for tokenType \(tokenType.identifier) - defining contract is not FungibleToken conformant"
2233            }
2234
2235            // Add token to global ledger with its interest curve and deposit parameters
2236            self.globalLedger[tokenType] = TokenState(
2237                interestCurve: interestCurve,
2238                depositRate: depositRate,
2239                depositCapacityCap: depositCapacityCap
2240            )
2241
2242            // Set collateral factor (what percentage of value can be used as collateral)
2243            self.collateralFactor[tokenType] = collateralFactor
2244
2245            // Set borrow factor (risk adjustment for borrowed amounts)
2246            self.borrowFactor[tokenType] = borrowFactor
2247
2248            // Default liquidation bonus per token = 5%
2249            self.liquidationBonus[tokenType] = 0.05
2250        }
2251
2252        // Removed: addSupportedTokenWithLiquidationBonus — callers should use addSupportedToken then setTokenLiquidationBonus if needed
2253
2254        /// Sets per-token liquidation bonus fraction (0.0 to 1.0). E.g., 0.05 means +5% seize bonus.
2255        access(EGovernance) fun setTokenLiquidationBonus(tokenType: Type, bonus: UFix64) {
2256            pre {
2257                self.globalLedger[tokenType] != nil: "Unsupported token type"
2258                bonus >= 0.0 && bonus <= 1.0: "Liquidation bonus must be between 0 and 1"
2259            }
2260            self.liquidationBonus[tokenType] = bonus
2261        }
2262
2263        /// Updates the insurance rate for a given token (fraction in [0,1])
2264        access(EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) {
2265            pre {
2266                self.globalLedger[tokenType] != nil: "Unsupported token type"
2267                insuranceRate >= 0.0 && insuranceRate <= 1.0: "insuranceRate must be between 0 and 1"
2268            }
2269            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
2270                ?? panic("Invariant: token state missing")
2271            tsRef.setInsuranceRate(insuranceRate)
2272        }
2273
2274        /// Updates the per-deposit limit fraction for a given token (fraction in [0,1])
2275        access(EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) {
2276            pre {
2277                self.globalLedger[tokenType] != nil: "Unsupported token type"
2278                fraction > 0.0 && fraction <= 1.0: "fraction must be in (0,1]"
2279            }
2280            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
2281                ?? panic("Invariant: token state missing")
2282            tsRef.setDepositLimitFraction(fraction)
2283        }
2284
2285        /// Enables or disables verbose logging inside the Pool for testing and diagnostics
2286        access(EGovernance) fun setDebugLogging(_ enabled: Bool) {
2287            self.debugLogging = enabled
2288        }
2289
2290        /// Rebalances the position to the target health value. If `force` is `true`, the position will be rebalanced
2291        /// even if it is currently healthy. Otherwise, this function will do nothing if the position is within the
2292        /// min/max health bounds.
2293        access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) {
2294            if self.debugLogging { log("    [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") }
2295            let position = self._borrowPosition(pid: pid)
2296            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
2297
2298            if !force && (position.minHealth <= balanceSheet.health  && balanceSheet.health <= position.maxHealth) {
2299                // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do!
2300                return
2301            }
2302
2303            if balanceSheet.health < position.targetHealth {
2304                // The position is undercollateralized, see if the source can get more collateral to bring it up to the target health.
2305                if position.topUpSource != nil {
2306                    let topUpSource = position.topUpSource! as auth(FungibleToken.Withdraw) &{DeFiActions.Source}
2307                    let idealDeposit = self.fundsRequiredForTargetHealth(
2308                        pid: pid,
2309                        type: topUpSource.getSourceType(),
2310                        targetHealth: position.targetHealth
2311                    )
2312                    if self.debugLogging { log("    [CONTRACT] idealDeposit: \(idealDeposit)") }
2313
2314                    let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit)
2315
2316                    emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: pulledVault.balance, fromUnder: true)
2317
2318                    self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false)
2319                }
2320            } else if balanceSheet.health > position.targetHealth {
2321                // The position is overcollateralized, we'll withdraw funds to match the target health and offer it to the sink.
2322                if position.drawDownSink != nil {
2323                    let drawDownSink = position.drawDownSink!
2324                    let sinkType = drawDownSink.getSinkType()
2325                    let idealWithdrawal = self.fundsAvailableAboveTargetHealth(
2326                        pid: pid,
2327                        type: sinkType,
2328                        targetHealth: position.targetHealth
2329                    )
2330                    if self.debugLogging { log("    [CONTRACT] idealWithdrawal: \(idealWithdrawal)") }
2331
2332                    // Compute how many tokens of the sink's type are available to hit our target health.
2333                    let sinkCapacity = drawDownSink.minimumCapacity()
2334                    let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal
2335
2336                    if sinkAmount > 0.0 && sinkType == self.defaultToken { // second conditional included for sake of tracer bullet
2337                        // BUG: Calling through to withdrawAndPull results in an insufficient funds from the position's
2338                        //      topUpSource. These funds should come from the protocol or reserves, not from the user's
2339                        //      funds. To unblock here, we just mint MOET when a position is overcollateralized
2340                        // let sinkVault <- self.withdrawAndPull(
2341                        //     pid: pid,
2342                        //     type: sinkType,
2343                        //     amount: sinkAmount,
2344                        //     pullFromTopUpSource: false
2345                        // )
2346
2347                        let tokenState = self._borrowUpdatedTokenState(type: self.defaultToken)
2348                        if position.balances[self.defaultToken] == nil {
2349                            position.balances[self.defaultToken] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
2350                        }
2351                        // record the withdrawal and mint the tokens
2352                        let uintSinkAmount = FlowALPMath.toUFix128(sinkAmount)
2353                        position.balances[self.defaultToken]!.recordWithdrawal(amount: uintSinkAmount, tokenState: tokenState)
2354                        let sinkVault <- FlowALP._borrowMOETMinter().mintTokens(amount: sinkAmount)
2355
2356                        emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: sinkVault.balance, fromUnder: false)
2357
2358                        // Push what we can into the sink, and redeposit the rest
2359                        drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
2360                        if sinkVault.balance > 0.0 {
2361                            self.depositAndPush(pid: pid, from: <-sinkVault, pushToDrawDownSink: false)
2362                        } else {
2363                            Burner.burn(<-sinkVault)
2364                        }
2365                    }
2366                }
2367            }
2368        }
2369
2370        /// Executes asynchronous updates on positions that have been queued up to the lesser of the queue length or
2371        /// the configured positionsProcessedPerCallback value
2372        access(EImplementation) fun asyncUpdate() {
2373            // TODO: In the production version, this function should only process some positions (limited by positionsProcessedPerCallback) AND
2374            // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or
2375            // sink aborts) won't prevent other positions from being updated.
2376            var processed: UInt64 = 0
2377            while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback {
2378                let pid = self.positionsNeedingUpdates.removeFirst()
2379                self.asyncUpdatePosition(pid: pid)
2380                self._queuePositionForUpdateIfNecessary(pid: pid)
2381                processed = processed + 1
2382            }
2383        }
2384
2385        /// Executes an asynchronous update on the specified position
2386        access(EImplementation) fun asyncUpdatePosition(pid: UInt64) {
2387            let position = self._borrowPosition(pid: pid)
2388
2389            // First check queued deposits, their addition could affect the rebalance we attempt later
2390            for depositType in position.queuedDeposits.keys {
2391                let queuedVault <- position.queuedDeposits.remove(key: depositType)!
2392                let queuedAmount = queuedVault.balance
2393                let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
2394                let maxDeposit = depositTokenState.depositLimit()
2395
2396                if maxDeposit >= queuedAmount {
2397                    // We can deposit all of the queued deposit, so just do it and remove it from the queue
2398                    self.depositAndPush(pid: pid, from: <-queuedVault, pushToDrawDownSink: false)
2399                } else {
2400                    // We can only deposit part of the queued deposit, so do that and leave the rest in the queue
2401                    // for the next time we run.
2402                    let depositVault <- queuedVault.withdraw(amount: maxDeposit)
2403                    self.depositAndPush(pid: pid, from: <-depositVault, pushToDrawDownSink: false)
2404
2405                    // We need to update the queued vault to reflect the amount we used up
2406                    position.queuedDeposits[depositType] <-! queuedVault
2407                }
2408            }
2409
2410            // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance
2411            // the position if necessary.
2412            self.rebalancePosition(pid: pid, force: false)
2413        }
2414
2415        ////////////////
2416        // INTERNAL
2417        ////////////////
2418
2419        /// Queues a position for asynchronous updates if the position has been marked as requiring an update
2420        access(self) fun _queuePositionForUpdateIfNecessary(pid: UInt64) {
2421            if self.positionsNeedingUpdates.contains(pid) {
2422                // If this position is already queued for an update, no need to check anything else
2423                return
2424            } else {
2425                // If this position is not already queued for an update, we need to check if it needs one
2426                let position = self._borrowPosition(pid: pid)
2427
2428                if position.queuedDeposits.length > 0 {
2429                    // This position has deposits that need to be processed, so we need to queue it for an update
2430                    self.positionsNeedingUpdates.append(pid)
2431                    return
2432                }
2433
2434                let positionHealth = self.positionHealth(pid: pid)
2435
2436                if positionHealth < position.minHealth || positionHealth > position.maxHealth {
2437                    // This position is outside the configured health bounds, we queue it for an update
2438                    self.positionsNeedingUpdates.append(pid)
2439                    return
2440                }
2441            }
2442        }
2443
2444        /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health
2445        access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet {
2446            let position = self._borrowPosition(pid: pid)
2447            let priceOracle = &self.priceOracle as &{DeFiActions.PriceOracle}
2448
2449            // Get the position's collateral and debt values in terms of the default token.
2450            var effectiveCollateral: UFix128 = 0.0 as UFix128
2451            var effectiveDebt: UFix128 = 0.0 as UFix128
2452
2453            for type in position.balances.keys {
2454                let balance = position.balances[type]!
2455                let tokenState = self._borrowUpdatedTokenState(type: type)
2456                if balance.direction == BalanceDirection.Credit {
2457                    let trueBalance = FlowALP.scaledBalanceToTrueBalance(balance.scaledBalance,
2458                        interestIndex: tokenState.creditInterestIndex)
2459
2460                    let convertedPrice = FlowALPMath.toUFix128(priceOracle.price(ofToken: type)!)
2461                    let value = convertedPrice * trueBalance
2462
2463                    let convertedCollateralFactor = FlowALPMath.toUFix128(self.collateralFactor[type]!)
2464                    effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor)
2465                } else {
2466                    let trueBalance = FlowALP.scaledBalanceToTrueBalance(balance.scaledBalance,
2467                        interestIndex: tokenState.debitInterestIndex)
2468
2469                    let convertedPrice = FlowALPMath.toUFix128(priceOracle.price(ofToken: type)!)
2470                    let value = convertedPrice * trueBalance
2471
2472                    let convertedBorrowFactor = FlowALPMath.toUFix128(self.borrowFactor[type]!)
2473                    effectiveDebt = effectiveDebt + FlowALPMath.div(value, convertedBorrowFactor)
2474                }
2475            }
2476
2477            return BalanceSheet(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt)
2478        }
2479
2480        /// A convenience function that returns a reference to a particular token state, making sure it's up-to-date for
2481        /// the passage of time. This should always be used when accessing a token state to avoid missing interest
2482        /// updates (duplicate calls to updateForTimeChange() are a nop within a single block).
2483        access(self) fun _borrowUpdatedTokenState(type: Type): auth(EImplementation) &TokenState {
2484            let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState
2485            state.updateForTimeChange()
2486            return state
2487        }
2488
2489        /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist
2490        access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition {
2491            return &self.positions[pid] as auth(EImplementation) &InternalPosition?
2492                ?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool")
2493        }
2494
2495        /// Build a PositionView for the given position ID
2496        access(all) fun buildPositionView(pid: UInt64): FlowALP.PositionView {
2497            let position = self._borrowPosition(pid: pid)
2498            let snaps: {Type: FlowALP.TokenSnapshot} = {}
2499            let balancesCopy: {Type: FlowALP.InternalBalance} = position.copyBalances()
2500            for t in position.balances.keys {
2501                let tokenState = self._borrowUpdatedTokenState(type: t)
2502                snaps[t] = FlowALP.TokenSnapshot(
2503                    price: FlowALPMath.toUFix128(self.priceOracle.price(ofToken: t)!),
2504                    credit: tokenState.creditInterestIndex,
2505                    debit: tokenState.debitInterestIndex,
2506                    risk: FlowALP.RiskParams(
2507                        cf: FlowALPMath.toUFix128(self.collateralFactor[t]!),
2508                        bf: FlowALPMath.toUFix128(self.borrowFactor[t]!),
2509                        lb: FlowALPMath.toUFix128(self.liquidationBonus[t]!)
2510                    )
2511                )
2512            }
2513            return FlowALP.PositionView(
2514                balances: balancesCopy,
2515                snapshots: snaps,
2516                def: self.defaultToken,
2517                min: position.minHealth,
2518                max: position.maxHealth
2519            )
2520        }
2521        access(EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) {
2522            pre {
2523                newOracle.unitOfAccount() == self.defaultToken:
2524                    "Price oracle must return prices in terms of the pool's default token"
2525            }
2526            self.priceOracle = newOracle
2527            self.positionsNeedingUpdates = self.positions.keys
2528
2529            emit PriceOracleUpdated(poolUUID: self.uuid, newOracleType: newOracle.getType().identifier)
2530        }
2531        access(all) fun getDefaultToken(): Type {
2532            return self.defaultToken
2533        }
2534    }
2535
2536    /// PoolFactory
2537    ///
2538    /// Resource enabling the contract account to create the contract's Pool. This pattern is used in place of contract
2539    /// methods to ensure limited access to pool creation. While this could be done in contract's init, doing so here
2540    /// will allow for the setting of the Pool's PriceOracle without the introduction of a concrete PriceOracle defining
2541    /// contract which would include an external contract dependency.
2542    ///
2543    access(all) resource PoolFactory {
2544        /// Creates the contract-managed Pool and saves it to the canonical path, reverting if one is already stored
2545        access(all) fun createPool(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}) {
2546            pre {
2547                FlowALP.account.storage.type(at: FlowALP.PoolStoragePath) == nil:
2548                "Storage collision - Pool has already been created & saved to \(FlowALP.PoolStoragePath)"
2549            }
2550            let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle)
2551            FlowALP.account.storage.save(<-pool, to: FlowALP.PoolStoragePath)
2552            let cap = FlowALP.account.capabilities.storage.issue<&Pool>(FlowALP.PoolStoragePath)
2553            FlowALP.account.capabilities.unpublish(FlowALP.PoolPublicPath)
2554            FlowALP.account.capabilities.publish(cap, at: FlowALP.PoolPublicPath)
2555        }
2556    }
2557
2558    /// Position
2559    ///
2560    /// A Position is an external object representing ownership of value deposited to the protocol. From a Position, an
2561    /// actor can deposit and withdraw funds as well as construct DeFiActions components enabling value flows in and out
2562    /// of the Position from within the context of DeFiActions stacks.
2563    ///
2564    // TODO: Consider making this a resource given how critical it is to accessing a loan
2565    access(all) struct Position {
2566        /// The unique ID of the Position used to track deposits and withdrawals to the Pool
2567        access(self) let id: UInt64
2568        /// An authorized Capability to which the Position was opened
2569        access(self) let pool: Capability<auth(EPosition, EParticipant) &Pool>
2570
2571        init(id: UInt64, pool: Capability<auth(EPosition, EParticipant) &Pool>) {
2572            pre {
2573                pool.check(): "Invalid Pool Capability provided - cannot construct Position"
2574            }
2575            self.id = id
2576            self.pool = pool
2577        }
2578
2579        /// Returns the balances (both positive and negative) for all tokens in this position.
2580        access(all) fun getBalances(): [PositionBalance] {
2581            let pool = self.pool.borrow()!
2582            return pool.getPositionDetails(pid: self.id).balances
2583        }
2584        /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the
2585        /// calculation will be made assuming the position is topped up if the withdrawal amount puts the Position
2586        /// below its min health. If pullFromTopUpSource is false, the calculation will return the balance currently
2587        /// available without topping up the position.
2588        access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 {
2589            let pool = self.pool.borrow()!
2590            return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource)
2591        }
2592        /// Returns the current health of the position
2593        access(all) fun getHealth(): UFix128 {
2594            let pool = self.pool.borrow()!
2595            return pool.positionHealth(pid: self.id)
2596        }
2597        /// Returns the Position's target health (unitless ratio ≥ 1.0)
2598        access(all) fun getTargetHealth(): UFix64 {
2599            let pool = self.pool.borrow()!
2600            let uint = pool.readTargetHealth(pid: self.id)
2601            return FlowALPMath.toUFix64Round(uint)
2602        }
2603        /// Sets the target health of the Position
2604        access(all) fun setTargetHealth(targetHealth: UFix64) {
2605            let pool = self.pool.borrow()!
2606            let uint = FlowALPMath.toUFix128(targetHealth)
2607            pool.writeTargetHealth(pid: self.id, targetHealth: uint)
2608        }
2609        /// Returns the minimum health of the Position
2610        access(all) fun getMinHealth(): UFix64 {
2611            let pool = self.pool.borrow()!
2612            let uint = pool.readMinHealth(pid: self.id)
2613            return FlowALPMath.toUFix64Round(uint)
2614        }
2615        /// Sets the minimum health of the Position
2616        access(all) fun setMinHealth(minHealth: UFix64) {
2617            let pool = self.pool.borrow()!
2618            let uint = FlowALPMath.toUFix128(minHealth)
2619            pool.writeMinHealth(pid: self.id, minHealth: uint)
2620        }
2621        /// Returns the maximum health of the Position
2622        access(all) fun getMaxHealth(): UFix64 {
2623            let pool = self.pool.borrow()!
2624            let uint = pool.readMaxHealth(pid: self.id)
2625            return FlowALPMath.toUFix64Round(uint)
2626        }
2627        /// Sets the maximum health of the position
2628        access(all) fun setMaxHealth(maxHealth: UFix64) {
2629            let pool = self.pool.borrow()!
2630            let uint = FlowALPMath.toUFix128(maxHealth)
2631            pool.writeMaxHealth(pid: self.id, maxHealth: uint)
2632        }
2633        /// Returns the maximum amount of the given token type that could be deposited into this position
2634        access(all) fun getDepositCapacity(type: Type): UFix64 {
2635            // There's no limit on deposits from the position's perspective
2636            return UFix64.max
2637        }
2638        /// Deposits funds to the Position without pushing to the drawDownSink if the deposit puts the Position above
2639        /// its maximum health
2640        access(EParticipant) fun deposit(from: @{FungibleToken.Vault}) {
2641            let pool = self.pool.borrow()!
2642            pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: false)
2643        }
2644        /// Deposits funds to the Position enabling the caller to configure whether excess value should be pushed to the
2645        /// drawDownSink if the deposit puts the Position above its maximum health
2646        access(EParticipant) fun depositAndPush(from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) {
2647            let pool = self.pool.borrow()!
2648            pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: pushToDrawDownSink)
2649        }
2650        /// Withdraws funds from the Position without pulling from the topUpSource if the deposit puts the Position below
2651        /// its minimum health
2652        access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} {
2653            return <- self.withdrawAndPull(type: type, amount: amount, pullFromTopUpSource: false)
2654        }
2655        /// Withdraws funds from the Position enabling the caller to configure whether insufficient value should be
2656        /// pulled from the topUpSource if the deposit puts the Position below its minimum health
2657        access(FungibleToken.Withdraw) fun withdrawAndPull(type: Type, amount: UFix64, pullFromTopUpSource: Bool): @{FungibleToken.Vault} {
2658            let pool = self.pool.borrow()!
2659            return <- pool.withdrawAndPull(pid: self.id, type: type, amount: amount, pullFromTopUpSource: pullFromTopUpSource)
2660        }
2661        /// Returns a new Sink for the given token type that will accept deposits of that token and update the
2662        /// position's collateral and/or debt accordingly. Note that calling this method multiple times will create
2663        /// multiple sinks, each of which will continue to work regardless of how many other sinks have been created.
2664        access(all) fun createSink(type: Type): {DeFiActions.Sink} {
2665            // create enhanced sink with pushToDrawDownSink option
2666            return self.createSinkWithOptions(type: type, pushToDrawDownSink: false)
2667        }
2668        /// Returns a new Sink for the given token type and pushToDrawDownSink opetion that will accept deposits of that
2669        /// token and update the position's collateral and/or debt accordingly. Note that calling this method multiple
2670        /// times will create multiple sinks, each of which will continue to work regardless of how many other sinks
2671        /// have been created.
2672        access(all) fun createSinkWithOptions(type: Type, pushToDrawDownSink: Bool): {DeFiActions.Sink} {
2673            let pool = self.pool.borrow()!
2674            return PositionSink(id: self.id, pool: self.pool, type: type, pushToDrawDownSink: pushToDrawDownSink)
2675        }
2676        /// Returns a new Source for the given token type that will service withdrawals of that token and update the
2677        /// position's collateral and/or debt accordingly. Note that calling this method multiple times will create
2678        /// multiple sources, each of which will continue to work regardless of how many other sources have been created.
2679        access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source} {
2680            // Create enhanced source with pullFromTopUpSource = true
2681            return self.createSourceWithOptions(type: type, pullFromTopUpSource: false)
2682        }
2683        /// Returns a new Source for the given token type and pullFromTopUpSource option that will service withdrawals
2684        /// of that token and update the position's collateral and/or debt accordingly. Note that calling this method
2685        /// multiple times will create multiple sources, each of which will continue to work regardless of how many
2686        /// other sources have been created.
2687        access(FungibleToken.Withdraw) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DeFiActions.Source} {
2688            let pool = self.pool.borrow()!
2689            return PositionSource(id: self.id, pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource)
2690        }
2691        /// Provides a sink to the Position that will have tokens proactively pushed into it when the position has
2692        /// excess collateral. (Remember that sinks do NOT have to accept all tokens provided to them; the sink can
2693        /// choose to accept only some (or none) of the tokens provided, leaving the position overcollateralized).
2694        ///
2695        /// Each position can have only one sink, and the sink must accept the default token type configured for the
2696        /// pool. Providing a new sink will replace the existing sink. Pass nil to configure the position to not push
2697        /// tokens when the Position exceeds its maximum health.
2698        access(FungibleToken.Withdraw) fun provideSink(sink: {DeFiActions.Sink}?) {
2699            let pool = self.pool.borrow()!
2700            pool.provideDrawDownSink(pid: self.id, sink: sink)
2701        }
2702        /// Provides a source to the Position that will have tokens proactively pulled from it when the position has
2703        /// insufficient collateral. If the source can cover the position's debt, the position will not be liquidated.
2704        ///
2705        /// Each position can have only one source, and the source must accept the default token type configured for the
2706        /// pool. Providing a new source will replace the existing source. Pass nil to configure the position to not
2707        /// pull tokens.
2708        access(EParticipant) fun provideSource(source: {DeFiActions.Source}?) {
2709            let pool = self.pool.borrow()!
2710            pool.provideTopUpSource(pid: self.id, source: source)
2711        }
2712    }
2713
2714    /// PositionSink
2715    ///
2716    /// A DeFiActions connector enabling deposits to a Position from within a DeFiActions stack. This Sink is intended to
2717    /// be constructed from a Position object.
2718    access(all) struct PositionSink: DeFiActions.Sink {
2719        /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of
2720        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
2721        /// An authorized Capability on the Pool for which the related Position is in
2722        access(self) let pool: Capability<auth(EPosition) &Pool>
2723        /// The ID of the position in the Pool
2724        access(self) let positionID: UInt64
2725        /// The Type of Vault this Sink accepts
2726        access(self) let type: Type
2727        /// Whether deposits through this Sink to the Position should push available value to the Position's
2728        /// drawDownSink
2729        access(self) let pushToDrawDownSink: Bool
2730
2731        init(id: UInt64, pool: Capability<auth(EPosition) &Pool>, type: Type, pushToDrawDownSink: Bool) {
2732            self.uniqueID = nil
2733            self.positionID = id
2734            self.pool = pool
2735            self.type = type
2736            self.pushToDrawDownSink = pushToDrawDownSink
2737        }
2738
2739        /// Returns the Type of Vault this Sink accepts on deposits
2740        access(all) view fun getSinkType(): Type {
2741            return self.type
2742        }
2743        /// Returns the minimum capacity this Sink can accept as deposits
2744        access(all) fun minimumCapacity(): UFix64 {
2745            return self.pool.check() ? UFix64.max : 0.0
2746        }
2747        /// Deposits the funds from the provided Vault reference to the related Position
2748        access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
2749            if let pool = self.pool.borrow() {
2750                pool.depositAndPush(
2751                    pid: self.positionID,
2752                    from: <-from.withdraw(amount: from.balance),
2753                    pushToDrawDownSink: self.pushToDrawDownSink
2754                )
2755            }
2756        }
2757        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
2758            return DeFiActions.ComponentInfo(
2759                type: self.getType(),
2760                id: self.id(),
2761                innerComponents: []
2762            )
2763        }
2764        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
2765            return self.uniqueID
2766        }
2767        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
2768            self.uniqueID = id
2769        }
2770    }
2771
2772    /// PositionSource
2773    ///
2774    /// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack. This Source is
2775    /// intended to be constructed from a Position object.
2776    ///
2777    access(all) struct PositionSource: DeFiActions.Source {
2778        /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of
2779        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
2780        /// An authorized Capability on the Pool for which the related Position is in
2781        access(self) let pool: Capability<auth(EPosition) &Pool>
2782        /// The ID of the position in the Pool
2783        access(self) let positionID: UInt64
2784        /// The Type of Vault this Sink provides
2785        access(self) let type: Type
2786        /// Whether withdrawals through this Sink from the Position should pull value from the Position's topUpSource
2787        /// in the event the withdrawal puts the position under its target health
2788        access(self) let pullFromTopUpSource: Bool
2789
2790        init(id: UInt64, pool: Capability<auth(EPosition) &Pool>, type: Type, pullFromTopUpSource: Bool) {
2791            self.uniqueID = nil
2792            self.positionID = id
2793            self.pool = pool
2794            self.type = type
2795            self.pullFromTopUpSource = pullFromTopUpSource
2796        }
2797
2798        /// Returns the Type of Vault this Source provides on withdrawals
2799        access(all) view fun getSourceType(): Type {
2800            return self.type
2801        }
2802        /// Returns the minimum availble this Source can provide on withdrawal
2803        access(all) fun minimumAvailable(): UFix64 {
2804            if !self.pool.check() {
2805                return 0.0
2806            }
2807            let pool = self.pool.borrow()!
2808            return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource)
2809        }
2810        /// Withdraws up to the max amount as the sourceType Vault
2811        access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
2812            if !self.pool.check() {
2813                return <- DeFiActionsUtils.getEmptyVault(self.type)
2814            }
2815            let pool = self.pool.borrow()!
2816            let available = pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource)
2817            let withdrawAmount = (available > maxAmount) ? maxAmount : available
2818            if withdrawAmount > 0.0 {
2819                return <- pool.withdrawAndPull(pid: self.positionID, type: self.type, amount: withdrawAmount, pullFromTopUpSource: self.pullFromTopUpSource)
2820            } else {
2821                // Create an empty vault - this is a limitation we need to handle properly
2822                return <- DeFiActionsUtils.getEmptyVault(self.type)
2823            }
2824        }
2825        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
2826            return DeFiActions.ComponentInfo(
2827                type: self.getType(),
2828                id: self.id(),
2829                innerComponents: []
2830            )
2831        }
2832        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
2833            return self.uniqueID
2834        }
2835        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
2836            self.uniqueID = id
2837        }
2838    }
2839
2840    /// BalanceDirection
2841    ///
2842    /// The direction of a given balance
2843    access(all) enum BalanceDirection: UInt8 {
2844        /// Denotes that a balance that is withdrawable from the protocol
2845        access(all) case Credit
2846        /// Denotes that a balance that is due to the protocol
2847        access(all) case Debit
2848    }
2849
2850    /// PositionBalance
2851    ///
2852    /// A structure returned externally to report a position's balance for a particular token.
2853    /// This structure is NOT used internally.
2854    access(all) struct PositionBalance {
2855        /// The token type for which the balance details relate to
2856        access(all) let vaultType: Type
2857        /// Whether the balance is a Credit or Debit
2858        access(all) let direction: BalanceDirection
2859        /// The balance of the token for the related Position
2860        access(all) let balance: UFix64
2861
2862        init(vaultType: Type, direction: BalanceDirection, balance: UFix64) {
2863            self.vaultType = vaultType
2864            self.direction = direction
2865            self.balance = balance
2866        }
2867    }
2868
2869    /// PositionDetails
2870    ///
2871    /// A structure returned externally to report all of the details associated with a position.
2872    /// This structure is NOT used internally.
2873    access(all) struct PositionDetails {
2874        /// Balance details about each Vault Type deposited to the related Position
2875        access(all) let balances: [PositionBalance]
2876        /// The default token Type of the Pool in which the related position is held
2877        access(all) let poolDefaultToken: Type
2878        /// The available balance of the Pool's default token Type
2879        access(all) let defaultTokenAvailableBalance: UFix64
2880        /// The current health of the related position
2881        access(all) let health: UFix128
2882
2883        init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix128) {
2884            self.balances = balances
2885            self.poolDefaultToken = poolDefaultToken
2886            self.defaultTokenAvailableBalance = defaultTokenAvailableBalance
2887            self.health = health
2888        }
2889    }
2890
2891    /* --- PUBLIC METHODS ---- */
2892    /// Returns a health value computed from the provided effective collateral and debt values where health is a ratio
2893    /// of effective collateral over effective debt
2894    access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 {
2895        if effectiveDebt == 0.0 as UFix128 {
2896            // Handles X/0 (infinite) including 0/0 (safe empty position)
2897            return UFix128.max
2898        } else if effectiveCollateral == 0.0 as UFix128 {
2899            // 0/Y where Y > 0 is 0 health (unsafe)
2900            return 0.0 as UFix128
2901        } else if FlowALPMath.div(effectiveDebt, effectiveCollateral) == 0.0 as UFix128 {
2902            // Negligible debt relative to collateral: treat as infinite
2903            return UFix128.max
2904        } else {
2905            return FlowALPMath.div(effectiveCollateral, effectiveDebt)
2906        }
2907    }
2908
2909    // Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point
2910    // number with 18 decimal places). The input to this function will be just the relative annual interest rate
2911    // (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001).
2912    access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 {
2913        let secondsInYear: UFix128 = 31_536_000.0 as UFix128
2914        let perSecondScaledValue = FlowALPMath.div(yearlyRate, secondsInYear)
2915        assert(perSecondScaledValue < UFix128.max, message: "Per-second interest rate \(perSecondScaledValue) is too high")
2916        return perSecondScaledValue + FlowALPMath.one
2917    }
2918
2919    /// Returns the compounded interest index reflecting the passage of time
2920    /// The result is: newIndex = oldIndex * perSecondRate ^ seconds
2921    access(all) view fun compoundInterestIndex(oldIndex: UFix128, perSecondRate: UFix128, elapsedSeconds: UFix64): UFix128 {
2922        // Exponentiation by squaring on UFix128 for performance and precision
2923        let pow = FlowALPMath.powUFix128(perSecondRate, elapsedSeconds)
2924        return oldIndex * pow
2925    }
2926
2927    /// Transforms the provided `scaledBalance` to a true balance (or actual balance) where the true balance is the
2928    /// scaledBalance + accrued interest and the scaled balance is the amount a borrower has actually interacted with
2929    /// (via deposits or withdrawals)
2930    access(all) view fun scaledBalanceToTrueBalance(_ scaled: UFix128, interestIndex: UFix128): UFix128 {
2931        // The interest index is a fixed point number with 18 decimal places. To maintain precision,
2932        // we multiply the scaled balance by the interest index and then divide by 10^18 to get the
2933        // true balance with proper decimal alignment.
2934        return FlowALPMath.div(scaled * interestIndex, FlowALPMath.one)
2935    }
2936
2937    /// Transforms the provided `trueBalance` to a scaled balance where the scaled balance is the amount a borrower has
2938    /// actually interacted with (via deposits or withdrawals) and the true balance is the amount with respect to
2939    /// accrued interest
2940    access(all) view fun trueBalanceToScaledBalance(_ trueBalance: UFix128, interestIndex: UFix128): UFix128 {
2941        // The interest index is a fixed point number with 18 decimal places. To maintain precision,
2942        // we multiply the true balance by 10^18 and then divide by the interest index to get the
2943        // scaled balance with proper decimal alignment.
2944        return FlowALPMath.div(trueBalance * FlowALPMath.one, interestIndex)
2945    }
2946
2947    /* --- INTERNAL METHODS --- */
2948
2949    /// Returns a reference to the contract account's MOET Minter resource
2950    access(self) view fun _borrowMOETMinter(): &MOET.Minter {
2951        return self.account.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath)
2952            ?? panic("Could not borrow reference to internal MOET Minter resource")
2953    }
2954
2955    init() {
2956        self.PoolStoragePath = StoragePath(identifier: "flowALPPool_\(self.account.address)")!
2957        self.PoolFactoryPath = StoragePath(identifier: "flowALPPoolFactory_\(self.account.address)")!
2958        self.PoolPublicPath = PublicPath(identifier: "flowALPPool_\(self.account.address)")!
2959        self.PoolCapStoragePath = StoragePath(identifier: "flowALPPoolCap_\(self.account.address)")!
2960
2961        // save PoolFactory in storage
2962        self.account.storage.save(
2963            <-create PoolFactory(),
2964            to: self.PoolFactoryPath
2965        )
2966        let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)!
2967    }
2968
2969    access(all) resource LiquidationResult: Burner.Burnable {
2970        access(all) var seized: @{FungibleToken.Vault}?
2971        access(all) var remainder: @{FungibleToken.Vault}?
2972
2973        init(seized: @{FungibleToken.Vault}, remainder: @{FungibleToken.Vault}) {
2974            self.seized <- seized
2975            self.remainder <- remainder
2976        }
2977
2978        access(all) fun takeSeized(): @{FungibleToken.Vault} {
2979            let s <- self.seized <- nil
2980            return <- s!
2981        }
2982
2983        access(all) fun takeRemainder(): @{FungibleToken.Vault} {
2984            let r <- self.remainder <- nil
2985            return <- r!
2986        }
2987
2988        access(contract) fun burnCallback() {
2989            let s <- self.seized <- nil
2990            let r <- self.remainder <- nil
2991            if s != nil {
2992                Burner.burn(<-s)
2993            } else {
2994                destroy s
2995            }
2996            if r != nil {
2997                Burner.burn(<-r)
2998            } else {
2999                destroy r
3000            }
3001        }
3002    }
3003
3004    // (contract-level helpers removed; resource-scoped versions live in Pool)
3005}
3006