Smart Contract

FlowCreditMarket

A.6b00ff876c299c61.FlowCreditMarket

Valid From

135,065,409

Deployed

2w ago
Feb 11, 2026, 06:47:02 PM UTC

Dependents

5 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 FlowCreditMarketMath from 0x6b00ff876c299c61
9
10access(all) contract FlowCreditMarket {
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 FlowCreditMarket 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 FlowCreditMarket 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 FlowCreditMarketMath 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 FlowCreditMarketMath.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarketMath.toUFix128(1.3)
272            self.minHealth = FlowCreditMarketMath.toUFix128(1.1)
273            self.maxHealth = FlowCreditMarketMath.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 <= FlowCreditMarketMath.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 FlowCreditMarketMath) 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 = FlowCreditMarketMath.one
370            self.debitInterestIndex = FlowCreditMarketMath.one
371            self.currentCreditRate = FlowCreditMarketMath.one
372            self.currentDebitRate = FlowCreditMarketMath.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 = FlowCreditMarket.compoundInterestIndex(
444                oldIndex: self.creditInterestIndex,
445                perSecondRate: self.currentCreditRate,
446                elapsedSeconds: dt
447            )
448            self.debitInterestIndex = FlowCreditMarket.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 = FlowCreditMarketMath.one  // 1.0 in fixed point (no interest)
479                self.currentDebitRate = FlowCreditMarketMath.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 = FlowCreditMarketMath.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)
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 = FlowCreditMarket.perSecondInterestRate(yearlyRate: creditRate)
501            self.currentDebitRate = FlowCreditMarket.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 = FlowCreditMarket.scaledBalanceToTrueBalance(
576                    balance.scaledBalance,
577                    interestIndex: snap.creditIndex
578                )
579                effectiveCollateralTotal = effectiveCollateralTotal + FlowCreditMarket.effectiveCollateral(credit: trueBalance, snap: snap)
580            } else {
581                let trueBalance = FlowCreditMarket.scaledBalanceToTrueBalance(
582                    balance.scaledBalance,
583                    interestIndex: snap.debitIndex
584                )
585                effectiveDebtTotal = effectiveDebtTotal + FlowCreditMarket.effectiveDebt(debit: trueBalance, snap: snap)
586            }
587        }
588        return FlowCreditMarket.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 = FlowCreditMarket.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 = FlowCreditMarket.scaledBalanceToTrueBalance(
613                    balance.scaledBalance,
614                    interestIndex: snap.creditIndex
615                )
616                effectiveCollateralTotal = effectiveCollateralTotal + FlowCreditMarket.effectiveCollateral(credit: trueBalance, snap: snap)
617            } else {
618                let trueBalance = FlowCreditMarket.scaledBalanceToTrueBalance(
619                    balance.scaledBalance,
620                    interestIndex: snap.debitIndex
621                )
622                effectiveDebtTotal = effectiveDebtTotal + FlowCreditMarket.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 : FlowCreditMarketMath.zero
634            let tokens = (deltaDebt * borrowFactor) / withdrawSnap.price
635            return tokens
636        } else {
637            // withdrawing reduces collateral
638            let trueBalance = FlowCreditMarket.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 = FlowCreditMarketMath.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(): FlowCreditMarket.LiquidationParamsView {
766            return FlowCreditMarket.LiquidationParamsView(
767                targetHF: self.liquidationTargetHF,
768                paused: self.liquidationsPaused,
769                warmupSec: self.liquidationWarmupSec,
770                lastUnpausedAt: self.lastUnpausedAt,
771                triggerHF: FlowCreditMarketMath.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 < FlowCreditMarketMath.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 = FlowCreditMarket.TokenSnapshot(
832                price: FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: type)!),
833                credit: tokenState.creditInterestIndex,
834                debit: tokenState.debitInterestIndex,
835                risk: FlowCreditMarket.RiskParams(
836                    cf: FlowCreditMarketMath.toUFix128(self.collateralFactor[type]!),
837                    bf: FlowCreditMarketMath.toUFix128(self.borrowFactor[type]!),
838                    lb: FlowCreditMarketMath.toUFix128(self.liquidationBonus[type]!)
839                )
840            )
841
842            let withdrawBal = view.balances[type]
843            let uintMax = FlowCreditMarket.maxWithdraw(
844                view: view,
845                withdrawSnap: snap,
846                withdrawBal: withdrawBal,
847                targetHealth: view.minHealth
848            )
849            return FlowCreditMarketMath.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 = FlowCreditMarketMath.toUFix128(self.collateralFactor[type]!)
867                let borrowFactor = FlowCreditMarketMath.toUFix128(self.borrowFactor[type]!)
868                let price = FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: type)!)
869                if balance.direction == BalanceDirection.Credit {
870                    let trueBalance = FlowCreditMarket.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 = FlowCreditMarket.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 FlowCreditMarket.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                    ? FlowCreditMarket.scaledBalanceToTrueBalance(balance.scaledBalance, interestIndex: tokenState.creditInterestIndex)
914                    : FlowCreditMarket.scaledBalanceToTrueBalance(balance.scaledBalance, interestIndex: tokenState.debitInterestIndex)
915
916                balances.append(PositionBalance(
917                    vaultType: type,
918                    direction: balance.direction,
919                    balance: FlowCreditMarketMath.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): FlowCreditMarket.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 = FlowCreditMarket.healthFactor(view: view)
942            if health >= FlowCreditMarketMath.one {
943                return FlowCreditMarket.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 = FlowCreditMarket.TokenSnapshot(
955                price: FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: debtType)!),
956                credit: debtState.creditInterestIndex,
957                debit: debtState.debitInterestIndex,
958                risk: FlowCreditMarket.RiskParams(
959                    cf: FlowCreditMarketMath.toUFix128(self.collateralFactor[debtType]!),
960                    bf: FlowCreditMarketMath.toUFix128(self.borrowFactor[debtType]!),
961                    lb: FlowCreditMarketMath.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 = FlowCreditMarket.TokenSnapshot(
971                price: FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: seizeType)!),
972                credit: seizeState.creditInterestIndex,
973                debit: seizeState.debitInterestIndex,
974                risk: FlowCreditMarket.RiskParams(
975                    cf: FlowCreditMarketMath.toUFix128(self.collateralFactor[seizeType]!),
976                    bf: FlowCreditMarketMath.toUFix128(self.borrowFactor[seizeType]!),
977                    lb: FlowCreditMarketMath.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 = FlowCreditMarket.TokenSnapshot(
996                    price: FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: t)!),
997                    credit: st.creditInterestIndex,
998                    debit: st.debitInterestIndex,
999                    risk: FlowCreditMarket.RiskParams(
1000                        cf: FlowCreditMarketMath.toUFix128(self.collateralFactor[t]!),
1001                        bf: FlowCreditMarketMath.toUFix128(self.borrowFactor[t]!),
1002                        lb: FlowCreditMarketMath.toUFix128(lbTUFix)
1003                    )
1004                )
1005                if b.direction == BalanceDirection.Credit {
1006                    let trueBal = FlowCreditMarket.scaledBalanceToTrueBalance(b.scaledBalance, interestIndex: snap.creditIndex)
1007                    if t == seizeType {
1008                        trueCollateralSeize = trueBal
1009                    }
1010                    effColl = effColl + FlowCreditMarket.effectiveCollateral(credit: trueBal, snap: snap)
1011                } else {
1012                    let trueBal = FlowCreditMarket.scaledBalanceToTrueBalance(b.scaledBalance, interestIndex: snap.debitIndex)
1013                    if t == debtType {
1014                        trueDebt = trueBal
1015                    }
1016                    effDebt = effDebt + FlowCreditMarket.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 FlowCreditMarket.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: UFix128.max)
1024            }
1025            let requiredEffColl = effDebt * target
1026            if effColl >= requiredEffColl {
1027                return FlowCreditMarket.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 = FlowCreditMarketMath.div(effColl, target)
1036            if effDebt <= effDebtNew {
1037                return FlowCreditMarket.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 FlowCreditMarket.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 - ((FlowCreditMarketMath.one + LB) * CF)
1060            if denomFactor <= FlowCreditMarketMath.zero {
1061                // Impossible target, return 0
1062                return FlowCreditMarket.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: health)
1063            }
1064            var repayTrueU128 = FlowCreditMarketMath.div(num * BF, Pd * denomFactor)
1065            if repayTrueU128 > trueDebt {
1066                repayTrueU128 = trueDebt
1067            }
1068            let u = FlowCreditMarketMath.div(repayTrueU128 * Pd, BF)
1069            var seizeTrueU128 = FlowCreditMarketMath.div(u * (FlowCreditMarketMath.one + LB), Pc)
1070            if seizeTrueU128 > trueCollateralSeize {
1071                seizeTrueU128 = trueCollateralSeize
1072                let uAllowed = FlowCreditMarketMath.div(seizeTrueU128 * Pc, (FlowCreditMarketMath.one + LB))
1073                repayTrueU128 = FlowCreditMarketMath.div(uAllowed * BF, Pd)
1074                if repayTrueU128 > trueDebt {
1075                    repayTrueU128 = trueDebt
1076                }
1077            }
1078            let repayExact = FlowCreditMarketMath.toUFix64RoundUp(repayTrueU128)
1079            let seizeExact = FlowCreditMarketMath.toUFix64RoundUp(seizeTrueU128)
1080            let repayEff = FlowCreditMarketMath.div(repayTrueU128 * Pd, BF)
1081            let seizeEff = seizeTrueU128 * (Pc * CF)
1082            let newEffColl = effColl > seizeEff ? effColl - seizeEff : FlowCreditMarketMath.zero
1083            let newEffDebt = effDebt > repayEff ? effDebt - repayEff : FlowCreditMarketMath.zero
1084            let newHF = newEffDebt == FlowCreditMarketMath.zero ? UFix128.max : FlowCreditMarketMath.div(newEffColl * FlowCreditMarketMath.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 = FlowCreditMarketMath.div(trueCollateralSeize * Pc, (FlowCreditMarketMath.one + LB))
1093            var repayCapBySeize = FlowCreditMarketMath.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 == FlowCreditMarketMath.zero || trueCollateralSeize == FlowCreditMarketMath.zero {
1102                    return FlowCreditMarket.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 = FlowCreditMarketMath.toUFix128(16.0)
1108                var step: UFix128 = repayCapBySeize / stepsU
1109                if step == FlowCreditMarketMath.zero { step = FlowCreditMarketMath.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 = FlowCreditMarketMath.div(r * Pd, BF)
1115                    var sForR = FlowCreditMarketMath.div(uForR * (FlowCreditMarketMath.one + LB), Pc)
1116                    if sForR > trueCollateralSeize { sForR = trueCollateralSeize }
1117
1118                    // Compute resulting HF
1119                    let repayEffC = FlowCreditMarketMath.div(r * Pd, BF)
1120                    let seizeEffC = sForR * (Pc * CF)
1121                    let newEffCollC = effColl > seizeEffC ? effColl - seizeEffC : FlowCreditMarketMath.zero
1122                    let newEffDebtC = effDebt > repayEffC ? effDebt - repayEffC : FlowCreditMarketMath.zero
1123                    let newHFC = newEffDebtC == FlowCreditMarketMath.zero ? UFix128.max : FlowCreditMarketMath.div(newEffCollC * FlowCreditMarketMath.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 = FlowCreditMarketMath.div(rCap * Pd, BF)
1140                var sForR2 = FlowCreditMarketMath.div(uForR2 * (FlowCreditMarketMath.one + LB), Pc)
1141                if sForR2 > trueCollateralSeize { sForR2 = trueCollateralSeize }
1142                let repayEffC2 = FlowCreditMarketMath.div(rCap * Pd, BF)
1143                let seizeEffC2 = sForR2 * (Pc * CF)
1144                let newEffCollC2 = effColl > seizeEffC2 ? effColl - seizeEffC2 : FlowCreditMarketMath.zero
1145                let newEffDebtC2 = effDebt > repayEffC2 ? effDebt - repayEffC2 : FlowCreditMarketMath.zero
1146                let newHFC2 = newEffDebtC2 == FlowCreditMarketMath.zero ? UFix128.max : FlowCreditMarketMath.div(newEffCollC2 * FlowCreditMarketMath.one, newEffDebtC2)
1147                if newHFC2 > bestHF {
1148                    bestHF = newHFC2
1149                    bestRepayTrue = rCap
1150                    bestSeizeTrue = sForR2
1151                }
1152
1153                if bestHF > health && bestRepayTrue > FlowCreditMarketMath.zero && bestSeizeTrue > FlowCreditMarketMath.zero {
1154                    let repayExactBest = FlowCreditMarketMath.toUFix64RoundUp(bestRepayTrue)
1155                    let seizeExactBest = FlowCreditMarketMath.toUFix64RoundUp(bestSeizeTrue)
1156                    log("[LIQ][QUOTE][FALLBACK][SEARCH] repayExact=\(repayExactBest) seizeExact=\(seizeExactBest)")
1157                    return FlowCreditMarket.LiquidationQuote(requiredRepay: repayExactBest, seizeType: seizeType, seizeAmount: seizeExactBest, newHF: bestHF)
1158                }
1159
1160                // No improving pair found
1161                return FlowCreditMarket.LiquidationQuote(requiredRepay: 0.0, seizeType: seizeType, seizeAmount: 0.0, newHF: health)
1162            }
1163
1164            log("[LIQ][QUOTE] repayExact=\(repayExact) seizeExact=\(seizeExact) trueCollateralSeize=\(FlowCreditMarketMath.toUFix64Round(trueCollateralSeize))")
1165            return FlowCreditMarket.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
1181            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1182            let position = self._borrowPosition(pid: pid)
1183
1184            let adjusted = self.computeAdjustedBalancesAfterWithdrawal(
1185                balanceSheet: balanceSheet,
1186                position: position,
1187                withdrawType: withdrawType,
1188                withdrawAmount: withdrawAmount
1189            )
1190
1191            return self.computeRequiredDepositForHealth(
1192                position: position,
1193                depositType: depositType,
1194                withdrawType: withdrawType,
1195                effectiveCollateral: adjusted.effectiveCollateral,
1196                effectiveDebt: adjusted.effectiveDebt,
1197                targetHealth: targetHealth
1198            )
1199        }
1200
1201        /// Permissionless liquidation: keeper repays exactly the required amount to reach target HF and receives seized collateral
1202        access(all) fun liquidateRepayForSeize(
1203            pid: UInt64,
1204            debtType: Type,
1205            maxRepayAmount: UFix64,
1206            seizeType: Type,
1207            minSeizeAmount: UFix64,
1208            from: @{FungibleToken.Vault}
1209        ): @LiquidationResult {
1210            pre {
1211                self.globalLedger[debtType] != nil: "Invalid debt type \(debtType.identifier)"
1212                self.globalLedger[seizeType] != nil: "Invalid seize type \(seizeType.identifier)"
1213            }
1214            // Pause/warm-up checks
1215            self._assertLiquidationsActive()
1216
1217            // Quote required repay and seize
1218            let quote = self.quoteLiquidation(pid: pid, debtType: debtType, seizeType: seizeType)
1219            assert(quote.requiredRepay > 0.0, message: "Position not liquidatable or already healthy")
1220            assert(maxRepayAmount >= quote.requiredRepay, message: "Insufficient max repay")
1221            assert(quote.seizeAmount >= minSeizeAmount, message: "Seize amount below minimum")
1222
1223            // Ensure internal reserves exist for seizeType and debtType
1224            if self.reserves[seizeType] == nil {
1225                self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType)
1226            }
1227            if self.reserves[debtType] == nil {
1228                self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType)
1229            }
1230
1231            // Move repay tokens into reserves (repay vault must exactly match requiredRepay)
1232            assert(from.getType() == debtType, message: "Vault type mismatch for repay")
1233            assert(from.balance >= quote.requiredRepay, message: "Repay vault balance must be at least requiredRepay")
1234            let toUse <- from.withdraw(amount: quote.requiredRepay)
1235            let debtReserveRef = (&self.reserves[debtType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1236            debtReserveRef.deposit(from: <-toUse)
1237
1238            // Reduce borrower's debt position by repayAmount
1239            let position = self._borrowPosition(pid: pid)
1240            let debtState = self._borrowUpdatedTokenState(type: debtType)
1241            let repayUint = FlowCreditMarketMath.toUFix128(quote.requiredRepay)
1242            if position.balances[debtType] == nil {
1243                position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0 as UFix128)
1244            }
1245            position.balances[debtType]!.recordDeposit(amount: repayUint, tokenState: debtState)
1246
1247            // Withdraw seized collateral from position and send to liquidator
1248            let seizeState = self._borrowUpdatedTokenState(type: seizeType)
1249            let seizeUint = FlowCreditMarketMath.toUFix128(quote.seizeAmount)
1250            if position.balances[seizeType] == nil {
1251                position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
1252            }
1253            position.balances[seizeType]!.recordWithdrawal(amount: seizeUint, tokenState: seizeState)
1254            let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1255            let payout <- seizeReserveRef.withdraw(amount: quote.seizeAmount)
1256
1257            let actualNewHF = self.positionHealth(pid: pid)
1258            // Ensure realized HF is not materially below quoted HF (allow tiny rounding tolerance)
1259            let expectedHF = quote.newHF
1260            let hfTolerance: UFix128 = FlowCreditMarketMath.toUFix128(0.00001)
1261            assert(actualNewHF + hfTolerance >= expectedHF, message: "Post-liquidation HF below expected")
1262
1263            emit LiquidationExecuted(pid: pid, poolUUID: self.uuid, debtType: debtType.identifier, repayAmount: quote.requiredRepay, seizeType: seizeType.identifier, seizeAmount: quote.seizeAmount, newHF: actualNewHF)
1264
1265            return <- create LiquidationResult(seized: <-payout, remainder: <-from)
1266        }
1267
1268        /// Liquidation via DEX: seize collateral, swap via allowlisted Swapper to debt token, repay debt
1269        access(all) fun liquidateViaDex(
1270            pid: UInt64,
1271            debtType: Type,
1272            seizeType: Type,
1273            maxSeizeAmount: UFix64,
1274            minRepayAmount: UFix64,
1275            swapper: {DeFiActions.Swapper},
1276            quote: {DeFiActions.Quote}?
1277        ) {
1278            pre {
1279                self.globalLedger[debtType] != nil: "Invalid debt type \(debtType.identifier)"
1280                self.globalLedger[seizeType] != nil: "Invalid seize type \(seizeType.identifier)"
1281                !self.liquidationsPaused: "Liquidations paused"
1282            }
1283            self._assertLiquidationsActive()
1284
1285            // Ensure reserve vaults exist for both tokens
1286            if self.reserves[seizeType] == nil { self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType) }
1287            if self.reserves[debtType] == nil { self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) }
1288
1289            // Validate position is liquidatable
1290            let health = self.positionHealth(pid: pid)
1291            assert(health < FlowCreditMarketMath.one, message: "Position not liquidatable")
1292            assert(self.isLiquidatable(pid: pid), message: "Position \(pid) is not liquidatable")
1293
1294            // Internal quote to determine required seize (capped by max)
1295            let internalQuote = self.quoteLiquidation(pid: pid, debtType: debtType, seizeType: seizeType)
1296            var requiredSeize = internalQuote.seizeAmount
1297            if requiredSeize > maxSeizeAmount { requiredSeize = maxSeizeAmount }
1298            assert(requiredSeize > 0.0, message: "Nothing to seize")
1299
1300            // Allowlist/type checks
1301            assert(self.allowedSwapperTypes[swapper.getType()] == true, message: "Swapper not allowlisted")
1302            assert(swapper.inType() == seizeType, message: "Swapper must accept seizeType \(seizeType.identifier)")
1303            assert(swapper.outType() == debtType, message: "Swapper must output debtType \(debtType.identifier)")
1304
1305            // Oracle vs DEX price deviation guard
1306            let Pc = self.priceOracle.price(ofToken: seizeType)!
1307            let Pd = self.priceOracle.price(ofToken: debtType)!
1308            let dexQuote = quote != nil ? quote! : swapper.quoteOut(forProvided: requiredSeize, reverse: false)
1309            let dexOut = dexQuote.outAmount
1310            let impliedPrice = dexOut / requiredSeize
1311            let oraclePrice = Pd / Pc
1312            let deviation = impliedPrice > oraclePrice ? impliedPrice - oraclePrice : oraclePrice - impliedPrice
1313            let deviationBps = UInt16((deviation / oraclePrice) * 10000.0)
1314            assert(deviationBps <= self.dexOracleDeviationBps, message: "DEX price deviates too high")
1315
1316            // Seize collateral and swap
1317            let seized <- self.internalSeize(pid: pid, tokenType: seizeType, amount: requiredSeize)
1318            let outDebt <- swapper.swap(quote: dexQuote, inVault: <-seized)
1319            assert(outDebt.getType() == debtType, message: "Swapper returned wrong out type")
1320
1321            // Slippage guard if quote provided
1322            var slipBps: UInt16 = 0
1323            // Slippage vs expected from oracle prices
1324            let expectedOutFromOracle = requiredSeize * (Pd / Pc)
1325            if expectedOutFromOracle > 0.0 {
1326                let diff: UFix64 = outDebt.balance > expectedOutFromOracle ? outDebt.balance - expectedOutFromOracle : expectedOutFromOracle - outDebt.balance
1327                let frac: UFix64 = diff / expectedOutFromOracle
1328                let bpsU: UFix64 = frac * 10000.0
1329                slipBps = UInt16(bpsU)
1330                assert(UInt64(slipBps) <= self.dexMaxSlippageBps, message: "Swap slippage too high")
1331            }
1332
1333            // Repay debt using swap output
1334            let repaid = self.internalRepay(pid: pid, from: <-outDebt)
1335            assert(repaid >= minRepayAmount, message: "Insufficient repay after swap - required \(minRepayAmount) but repaid \(repaid)")
1336            // Optional safety: ensure improved health meets target
1337            let postHF = self.positionHealth(pid: pid)
1338            assert(postHF >= self.liquidationTargetHF, message: "Post-liquidation HF below target")
1339
1340            emit LiquidationExecutedViaDex(
1341                pid: pid,
1342                poolUUID: self.uuid,
1343                seizeType: seizeType.identifier,
1344                seized: requiredSeize,
1345                debtType: debtType.identifier,
1346                repaid: repaid,
1347                slippageBps: slipBps,
1348                newHF: self.positionHealth(pid: pid)
1349            )
1350        }
1351
1352        // Internal helpers for DEX liquidation path (resource-scoped)
1353        access(self) fun internalSeize(pid: UInt64, tokenType: Type, amount: UFix64): @{FungibleToken.Vault} {
1354            let position = self._borrowPosition(pid: pid)
1355            let tokenState = self._borrowUpdatedTokenState(type: tokenType)
1356            let seizeUint = FlowCreditMarketMath.toUFix128(amount)
1357            if position.balances[tokenType] == nil {
1358                position.balances[tokenType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
1359            }
1360            position.balances[tokenType]!.recordWithdrawal(amount: seizeUint, tokenState: tokenState)
1361            if self.reserves[tokenType] == nil {
1362                self.reserves[tokenType] <-! DeFiActionsUtils.getEmptyVault(tokenType)
1363            }
1364            let reserveRef = (&self.reserves[tokenType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1365            return <- reserveRef.withdraw(amount: amount)
1366        }
1367
1368        access(self) fun internalRepay(pid: UInt64, from: @{FungibleToken.Vault}): UFix64 {
1369            let debtType = from.getType()
1370            if self.reserves[debtType] == nil {
1371                self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType)
1372            }
1373            let toDeposit <- from
1374            let amount = toDeposit.balance
1375            let reserveRef = (&self.reserves[debtType] as &{FungibleToken.Vault}?)!
1376            reserveRef.deposit(from: <-toDeposit)
1377            let position = self._borrowPosition(pid: pid)
1378            let debtState = self._borrowUpdatedTokenState(type: debtType)
1379            let repayUint = FlowCreditMarketMath.toUFix128(amount)
1380            if position.balances[debtType] == nil {
1381                position.balances[debtType] = InternalBalance(direction: BalanceDirection.Debit, scaledBalance: 0.0 as UFix128)
1382            }
1383            position.balances[debtType]!.recordDeposit(amount: repayUint, tokenState: debtState)
1384            return amount
1385        }
1386
1387        access(self) fun computeAdjustedBalancesAfterWithdrawal(
1388            balanceSheet: BalanceSheet,
1389            position: &InternalPosition,
1390            withdrawType: Type,
1391            withdrawAmount: UFix64
1392        ): BalanceSheet {
1393            var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral
1394            var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt
1395
1396            if withdrawAmount == 0.0 {
1397                return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal)
1398            }
1399            if self.debugLogging {
1400                log("    [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)")
1401                log("    [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)")
1402            }
1403
1404            let withdrawAmountU = FlowCreditMarketMath.toUFix128(withdrawAmount)
1405            let withdrawPrice2 = FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: withdrawType)!)
1406            let withdrawBorrowFactor2 = FlowCreditMarketMath.toUFix128(self.borrowFactor[withdrawType]!)
1407
1408            let maybeBalance = position.balances[withdrawType]
1409                if maybeBalance == nil || maybeBalance!.direction == BalanceDirection.Debit {
1410                    // If the position doesn't have any collateral for the withdrawn token, we can just compute how much
1411                    // additional effective debt the withdrawal will create.
1412                    effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt +
1413                        FlowCreditMarketMath.div(withdrawAmountU * withdrawPrice2, withdrawBorrowFactor2)
1414                } else {
1415                    let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType)
1416
1417                    // The user has a collateral position in the given token, we need to figure out if this withdrawal
1418                    // will flip over into debt, or just draw down the collateral.
1419                    let collateralBalance = maybeBalance!.scaledBalance
1420                    let trueCollateral = FlowCreditMarket.scaledBalanceToTrueBalance(collateralBalance,
1421                        interestIndex: withdrawTokenState.creditInterestIndex
1422                    )
1423                    let collateralFactor = FlowCreditMarketMath.toUFix128(self.collateralFactor[withdrawType]!)
1424                    if trueCollateral >= withdrawAmountU {
1425                        // This withdrawal will draw down collateral, but won't create debt, we just need to account
1426                        // for the collateral decrease.
1427                        effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral -
1428                            (withdrawAmountU * withdrawPrice2) * collateralFactor
1429                    } else {
1430                        // The withdrawal will wipe out all of the collateral, and create some debt.
1431                        effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt +
1432                            FlowCreditMarketMath.div((withdrawAmountU - trueCollateral) * withdrawPrice2, withdrawBorrowFactor2)
1433                        effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral -
1434                            (trueCollateral * withdrawPrice2) * collateralFactor
1435                    }
1436                }
1437
1438            return BalanceSheet(effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterWithdrawal)
1439        }
1440
1441
1442        access(self) fun computeRequiredDepositForHealth(
1443            position: &InternalPosition,
1444            depositType: Type,
1445            withdrawType: Type,
1446            effectiveCollateral: UFix128,
1447            effectiveDebt: UFix128,
1448            targetHealth: UFix128
1449        ): UFix64 {
1450            var effectiveCollateralAfterWithdrawal = effectiveCollateral
1451            var effectiveDebtAfterWithdrawal = effectiveDebt
1452
1453            if self.debugLogging {
1454                log("    [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)")
1455                log("    [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)")
1456            }
1457
1458            // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!)
1459            // Now we can figure out how many of the given token would need to be deposited to bring the position
1460            // to the target health value.
1461            var healthAfterWithdrawal = FlowCreditMarket.healthComputation(
1462                effectiveCollateral: effectiveCollateralAfterWithdrawal,
1463                effectiveDebt: effectiveDebtAfterWithdrawal
1464            )
1465            if self.debugLogging { log("    [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") }
1466
1467            if healthAfterWithdrawal >= targetHealth {
1468                // The position is already at or above the target health, so we don't need to deposit anything.
1469                return 0.0
1470            }
1471
1472            // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep
1473            // track of the number of tokens that went towards paying off debt.
1474            var debtTokenCount: UFix128 = FlowCreditMarketMath.zero
1475            let depositPrice = FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: depositType)!)
1476            let depositBorrowFactor = FlowCreditMarketMath.toUFix128(self.borrowFactor[depositType]!)
1477            let withdrawBorrowFactor = FlowCreditMarketMath.toUFix128(self.borrowFactor[withdrawType]!)
1478            let maybeBalance = position.balances[depositType]
1479            if maybeBalance?.direction == BalanceDirection.Debit {
1480                // The user has a debt position in the given token, we start by looking at the health impact of paying off
1481                // the entire debt.
1482                let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
1483                let debtBalance = maybeBalance!.scaledBalance
1484                let trueDebtTokenCount = FlowCreditMarket.scaledBalanceToTrueBalance(debtBalance,
1485                    interestIndex: depositTokenState.debitInterestIndex
1486                )
1487                let debtEffectiveValue = FlowCreditMarketMath.div(depositPrice * trueDebtTokenCount, depositBorrowFactor)
1488
1489                // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal,
1490                // it means we can pay off all debt
1491            var effectiveDebtAfterPayment: UFix128 = FlowCreditMarketMath.zero
1492                if debtEffectiveValue <= effectiveDebtAfterWithdrawal {
1493                    effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue
1494                }
1495
1496                // Check what the new health would be if we paid off all of this debt
1497                let potentialHealth = FlowCreditMarket.healthComputation(
1498                    effectiveCollateral: effectiveCollateralAfterWithdrawal,
1499                    effectiveDebt: effectiveDebtAfterPayment
1500                )
1501
1502                // Does paying off all of the debt reach the target health? Then we're done.
1503                if potentialHealth >= targetHealth {
1504                    // We can reach the target health by paying off some or all of the debt. We can easily
1505                    // compute how many units of the token would be needed to reach the target health.
1506                    let healthChange = targetHealth - healthAfterWithdrawal
1507                    let requiredEffectiveDebt = effectiveDebtAfterWithdrawal - FlowCreditMarketMath.div(
1508                            effectiveCollateralAfterWithdrawal,
1509                            targetHealth
1510                        )
1511
1512                    // The amount of the token to pay back, in units of the token.
1513                    let paybackAmount = FlowCreditMarketMath.div(requiredEffectiveDebt * depositBorrowFactor, depositPrice)
1514
1515                    if self.debugLogging { log("    [CONTRACT] paybackAmount: \(paybackAmount)") }
1516
1517                    return FlowCreditMarketMath.toUFix64RoundUp(paybackAmount)
1518                } else {
1519                    // We can pay off the entire debt, but we still need to deposit more to reach the target health.
1520                    // We have logic below that can determine the collateral deposition required to reach the target health
1521                    // from this new health position. Rather than copy that logic here, we fall through into it. But first
1522                    // we have to record the amount of tokens that went towards debt payback and adjust the effective
1523                    // debt to reflect that it has been paid off.
1524                    debtTokenCount = trueDebtTokenCount
1525                    // Ensure we don't underflow
1526                    if debtEffectiveValue <= effectiveDebtAfterWithdrawal {
1527                        effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue
1528                    } else {
1529                        effectiveDebtAfterWithdrawal = FlowCreditMarketMath.zero
1530                    }
1531                    healthAfterWithdrawal = potentialHealth
1532                }
1533            }
1534
1535            // At this point, we're either dealing with a position that didn't have a debt position in the deposit
1536            // token, or we've accounted for the debt payoff and adjusted the effective debt above.
1537            // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the
1538            // target health. We can rearrange the health equation to solve for the required collateral:
1539
1540            // We need to increase the effective collateral from its current value to the required value, so we
1541            // multiply the required health change by the effective debt, and turn that into a token amount.
1542            let healthChangeU = targetHealth - healthAfterWithdrawal
1543            // TODO: apply the same logic as below to the early return blocks above
1544            let depositCollateralFactor = FlowCreditMarketMath.toUFix128(self.collateralFactor[depositType]!)
1545            var requiredEffectiveCollateral = healthChangeU * effectiveDebtAfterWithdrawal
1546            requiredEffectiveCollateral = FlowCreditMarketMath.div(requiredEffectiveCollateral, depositCollateralFactor)
1547
1548            // The amount of the token to deposit, in units of the token.
1549            let collateralTokenCount = FlowCreditMarketMath.div(requiredEffectiveCollateral, depositPrice)
1550            if self.debugLogging {
1551                log("    [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)")
1552                log("    [CONTRACT] collateralTokenCount: \(collateralTokenCount)")
1553                log("    [CONTRACT] debtTokenCount: \(debtTokenCount)")
1554                log("    [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)")
1555            }
1556
1557            // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt.
1558            return FlowCreditMarketMath.toUFix64Round(collateralTokenCount + debtTokenCount)
1559        }
1560
1561        /// Returns the quantity of the specified token that could be withdrawn while still keeping the position's
1562        /// health at or above the provided target.
1563        access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix128): UFix64 {
1564            return self.fundsAvailableAboveTargetHealthAfterDepositing(
1565                pid: pid,
1566                withdrawType: type,
1567                targetHealth: targetHealth,
1568                depositType: self.defaultToken,
1569                depositAmount: 0.0
1570            )
1571        }
1572
1573        /// Returns the quantity of the specified token that could be withdrawn while still keeping the position's health
1574        /// at or above the provided target, assuming we also deposit a specified amount of another token.
1575        access(all) fun fundsAvailableAboveTargetHealthAfterDepositing(
1576            pid: UInt64,
1577            withdrawType: Type,
1578            targetHealth: UFix128,
1579            depositType: Type,
1580            depositAmount: UFix64
1581        ): UFix64 {
1582            if self.debugLogging { log("    [CONTRACT] fundsAvailableAboveTargetHealthAfterDepositing(pid: \(pid), withdrawType: \(withdrawType.contractName!), targetHealth: \(targetHealth), depositType: \(depositType.contractName!), depositAmount: \(depositAmount))") }
1583            if depositType == withdrawType && depositAmount > 0.0 {
1584                // If the deposit and withdrawal types are the same, we compute the available funds assuming
1585                // no deposit (which is less work) and increase that by the deposit amount at the end
1586                return self.fundsAvailableAboveTargetHealth(pid: pid, type: withdrawType, targetHealth: targetHealth) + depositAmount
1587            }
1588
1589            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1590            let position = self._borrowPosition(pid: pid)
1591
1592            let adjusted = self.computeAdjustedBalancesAfterDeposit(
1593                balanceSheet: balanceSheet,
1594                position: position,
1595                depositType: depositType,
1596                depositAmount: depositAmount
1597            )
1598
1599            return self.computeAvailableWithdrawal(
1600                position: position,
1601                withdrawType: withdrawType,
1602                effectiveCollateral: adjusted.effectiveCollateral,
1603                effectiveDebt: adjusted.effectiveDebt,
1604                targetHealth: targetHealth
1605            )
1606        }
1607
1608        // Helper function to compute balances after deposit
1609        access(self) fun computeAdjustedBalancesAfterDeposit(
1610            balanceSheet: BalanceSheet,
1611            position: &InternalPosition,
1612            depositType: Type,
1613            depositAmount: UFix64
1614        ): BalanceSheet {
1615            var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral
1616            var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt
1617
1618            if self.debugLogging {
1619                log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
1620                log("    [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)")
1621            }
1622            if depositAmount == 0.0 {
1623                return BalanceSheet(effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit)
1624            }
1625
1626            let depositAmountCasted = FlowCreditMarketMath.toUFix128(depositAmount)
1627            let depositPriceCasted = FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: depositType)!)
1628            let depositBorrowFactorCasted = FlowCreditMarketMath.toUFix128(self.borrowFactor[depositType]!)
1629            let depositCollateralFactorCasted = FlowCreditMarketMath.toUFix128(self.collateralFactor[depositType]!)
1630            let maybeBalance = position.balances[depositType]
1631                if maybeBalance == nil || maybeBalance!.direction == BalanceDirection.Credit {
1632                    // If there's no debt for the deposit token, we can just compute how much additional effective collateral the deposit will create.
1633                    effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral +
1634                        (depositAmountCasted * depositPriceCasted) * depositCollateralFactorCasted
1635                } else {
1636                    let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
1637
1638                    // The user has a debt position in the given token, we need to figure out if this deposit
1639                    // will result in net collateral, or just bring down the debt.
1640                    let debtBalance = maybeBalance!.scaledBalance
1641                    let trueDebt = FlowCreditMarket.scaledBalanceToTrueBalance(debtBalance,
1642                        interestIndex: depositTokenState.debitInterestIndex
1643                    )
1644                    if self.debugLogging { log("    [CONTRACT] trueDebt: \(trueDebt)") }
1645
1646                    if trueDebt >= depositAmountCasted {
1647                        // This deposit will pay down some debt, but won't result in net collateral, we
1648                        // just need to account for the debt decrease.
1649                        // TODO - validate if this should deal with withdrawType or depositType
1650                        effectiveDebtAfterDeposit = balanceSheet.effectiveDebt -
1651                            FlowCreditMarketMath.div(depositAmountCasted * depositPriceCasted, depositBorrowFactorCasted)
1652                    } else {
1653                        // The deposit will wipe out all of the debt, and create some collateral.
1654                        // TODO - validate if this should deal with withdrawType or depositType
1655                        effectiveDebtAfterDeposit = balanceSheet.effectiveDebt -
1656                            FlowCreditMarketMath.div(trueDebt * depositPriceCasted, depositBorrowFactorCasted)
1657                        effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral +
1658                            (depositAmountCasted - trueDebt) * depositPriceCasted * depositCollateralFactorCasted
1659                    }
1660                }
1661
1662            if self.debugLogging {
1663                log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
1664                log("    [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)")
1665            }
1666
1667            // We now have new effective collateral and debt values that reflect the proposed deposit (if any!)
1668            // Now we can figure out how many of the withdrawal token are available while keeping the position
1669            // at or above the target health value.
1670            return BalanceSheet(effectiveCollateral: effectiveCollateralAfterDeposit, effectiveDebt: effectiveDebtAfterDeposit)
1671        }
1672
1673        // Helper function to compute available withdrawal
1674        access(self) fun computeAvailableWithdrawal(
1675            position: &InternalPosition,
1676            withdrawType: Type,
1677            effectiveCollateral: UFix128,
1678            effectiveDebt: UFix128,
1679            targetHealth: UFix128 
1680        ): UFix64 {
1681            var effectiveCollateralAfterDeposit = effectiveCollateral
1682            var effectiveDebtAfterDeposit = effectiveDebt
1683
1684            var healthAfterDeposit = FlowCreditMarket.healthComputation(
1685                effectiveCollateral: effectiveCollateralAfterDeposit,
1686                effectiveDebt: effectiveDebtAfterDeposit
1687            )
1688            if self.debugLogging { log("    [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") }
1689
1690            if healthAfterDeposit <= targetHealth {
1691                // The position is already at or below the provided target health, so we can't withdraw anything.
1692                return 0.0
1693            }
1694
1695            // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep
1696            // track of the number of tokens that are available from collateral
1697            var collateralTokenCount: UFix128 = FlowCreditMarketMath.zero
1698
1699            let withdrawPrice = FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: withdrawType)!)
1700            let withdrawCollateralFactor = FlowCreditMarketMath.toUFix128(self.collateralFactor[withdrawType]!)
1701            let withdrawBorrowFactor = FlowCreditMarketMath.toUFix128(self.borrowFactor[withdrawType]!)
1702
1703            let maybeBalance = position.balances[withdrawType]
1704            if maybeBalance?.direction == BalanceDirection.Credit {
1705                // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all
1706                // of that collateral
1707                let withdrawTokenState = self._borrowUpdatedTokenState(type: withdrawType)
1708                let creditBalance = maybeBalance!.scaledBalance
1709                let trueCredit = FlowCreditMarket.scaledBalanceToTrueBalance(creditBalance,
1710                    interestIndex: withdrawTokenState.creditInterestIndex
1711                )
1712                let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor
1713
1714                // Check what the new health would be if we took out all of this collateral
1715                let potentialHealth = FlowCreditMarket.healthComputation(
1716                    effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract?
1717                    effectiveDebt: effectiveDebtAfterDeposit
1718                )
1719
1720
1721                // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only.
1722                if potentialHealth <= targetHealth {
1723                    // We will hit the health target before using up all of the withdraw token credit. We can easily
1724                    // compute how many units of the token would bring the position down to the target health.
1725                // We will hit the health target before using up all available withdraw credit.
1726
1727            let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit)
1728                    if self.debugLogging { log("    [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") }
1729
1730                    // The amount of the token we can take using that amount of health
1731            let availableTokenCount = FlowCreditMarketMath.div(FlowCreditMarketMath.div(availableEffectiveValue, withdrawCollateralFactor), withdrawPrice)
1732                    if self.debugLogging { log("    [CONTRACT] availableTokenCount: \(availableTokenCount)") }
1733
1734                    return FlowCreditMarketMath.toUFix64RoundDown(availableTokenCount)
1735                } else {
1736                    // We can flip this credit position into a debit position, before hitting the target health.
1737                    // We have logic below that can determine health changes for debit positions. We've copied it here
1738                    // with an added handling for the case where the health after deposit is an edgecase
1739                    collateralTokenCount = trueCredit
1740                    effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue
1741                    if self.debugLogging {
1742                        log("    [CONTRACT] collateralTokenCount: \(collateralTokenCount)")
1743                        log("    [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)")
1744                    }
1745
1746                    // We can calculate the available debt increase that would bring us to the target health
1747                    var availableDebtIncrease = FlowCreditMarketMath.div(effectiveCollateralAfterDeposit, targetHealth) - effectiveDebtAfterDeposit
1748                    let availableTokens = FlowCreditMarketMath.div(availableDebtIncrease * withdrawBorrowFactor, withdrawPrice)
1749                    if self.debugLogging {
1750                        log("    [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)")
1751                        log("    [CONTRACT] availableTokens: \(availableTokens)")
1752                        log("    [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)")
1753                    }
1754                    return FlowCreditMarketMath.toUFix64RoundDown(availableTokens + collateralTokenCount)
1755                }
1756            }
1757
1758            // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw
1759            // token, or we've accounted for the credit balance and adjusted the effective collateral above.
1760
1761            // We can calculate the available debt increase that would bring us to the target health
1762            var availableDebtIncrease = FlowCreditMarketMath.div(effectiveCollateralAfterDeposit, targetHealth) - effectiveDebtAfterDeposit
1763            let availableTokens = FlowCreditMarketMath.div(availableDebtIncrease * withdrawBorrowFactor, withdrawPrice)
1764            if self.debugLogging {
1765                log("    [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)")
1766                log("    [CONTRACT] availableTokens: \(availableTokens)")
1767                log("    [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)")
1768            }
1769            return FlowCreditMarketMath.toUFix64RoundDown(availableTokens + collateralTokenCount)
1770        }
1771
1772        /// Returns the position's health if the given amount of the specified token were deposited
1773        access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix128 {
1774            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1775            let position = self._borrowPosition(pid: pid)
1776            let tokenState = self._borrowUpdatedTokenState(type: type)
1777
1778            var effectiveCollateralIncrease: UFix128 = FlowCreditMarketMath.zero
1779            var effectiveDebtDecrease: UFix128 = FlowCreditMarketMath.zero
1780
1781            let amountU = FlowCreditMarketMath.toUFix128(amount)
1782            let price = FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: type)!)
1783            let collateralFactor = FlowCreditMarketMath.toUFix128(self.collateralFactor[type]!)
1784            let borrowFactor = FlowCreditMarketMath.toUFix128(self.borrowFactor[type]!)
1785            if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Credit {
1786                // Since the user has no debt in the given token, we can just compute how much
1787                // additional collateral this deposit will create.
1788                effectiveCollateralIncrease = (amountU * price) * collateralFactor
1789            } else {
1790                // The user has a debit position in the given token, we need to figure out if this deposit
1791                // will only pay off some of the debt, or if it will also create new collateral.
1792                let debtBalance = position.balances[type]!.scaledBalance
1793                let trueDebt = FlowCreditMarket.scaledBalanceToTrueBalance(debtBalance,
1794                    interestIndex: tokenState.debitInterestIndex
1795                )
1796
1797                if trueDebt >= amountU {
1798                    // This deposit will wipe out some or all of the debt, but won't create new collateral, we
1799                    // just need to account for the debt decrease.
1800                    effectiveDebtDecrease = FlowCreditMarketMath.div(amountU * price, borrowFactor)
1801                } else {
1802                    // This deposit will wipe out all of the debt, and create new collateral.
1803                    effectiveDebtDecrease = FlowCreditMarketMath.div(trueDebt * price, borrowFactor)
1804                    effectiveCollateralIncrease = ((amountU - trueDebt) * price) * collateralFactor
1805                }
1806            }
1807
1808            return FlowCreditMarket.healthComputation(
1809                effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease,
1810                effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease
1811            )
1812        }
1813
1814        // Returns health value of this position if the given amount of the specified token were withdrawn without
1815        // using the top up source.
1816        // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates
1817        // that the proposed withdrawal would fail (unless a top up source is available and used).
1818        access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix128 {
1819            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
1820            let position = self._borrowPosition(pid: pid)
1821            let tokenState = self._borrowUpdatedTokenState(type: type)
1822
1823            var effectiveCollateralDecrease: UFix128 = FlowCreditMarketMath.zero
1824            var effectiveDebtIncrease: UFix128 = FlowCreditMarketMath.zero
1825
1826            let amountU = FlowCreditMarketMath.toUFix128(amount)
1827            let price = FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: type)!)
1828            let collateralFactor = FlowCreditMarketMath.toUFix128(self.collateralFactor[type]!)
1829            let borrowFactor = FlowCreditMarketMath.toUFix128(self.borrowFactor[type]!)
1830            if position.balances[type] == nil || position.balances[type]!.direction == BalanceDirection.Debit {
1831                // The user has no credit position in the given token, we can just compute how much
1832                // additional effective debt this withdrawal will create.
1833                effectiveDebtIncrease = FlowCreditMarketMath.div(amountU * price, borrowFactor)
1834            } else {
1835                // The user has a credit position in the given token, we need to figure out if this withdrawal
1836                // will only draw down some of the collateral, or if it will also create new debt.
1837                let creditBalance = position.balances[type]!.scaledBalance
1838                let trueCredit = FlowCreditMarket.scaledBalanceToTrueBalance(creditBalance,
1839                    interestIndex: tokenState.creditInterestIndex
1840                )
1841
1842                if trueCredit >= amountU {
1843                    // This withdrawal will draw down some collateral, but won't create new debt, we
1844                    // just need to account for the collateral decrease.
1845                    // effectiveCollateralDecrease = amount * self.priceOracle.price(ofToken: type)! * self.collateralFactor[type]!
1846                    effectiveCollateralDecrease = (amountU * price) * collateralFactor
1847                } else {
1848                    // The withdrawal will wipe out all of the collateral, and create new debt.
1849                    effectiveDebtIncrease = FlowCreditMarketMath.div((amountU - trueCredit) * price, borrowFactor)
1850                    effectiveCollateralDecrease = (trueCredit * price) * collateralFactor
1851                }
1852            }
1853
1854            return FlowCreditMarket.healthComputation(
1855                effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease,
1856                effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease
1857            )
1858        }
1859
1860        ///////////////////////////
1861        // POSITION MANAGEMENT
1862        ///////////////////////////
1863
1864        /// Creates a lending position against the provided collateral funds, depositing the loaned amount to the
1865        /// given Sink. If a Source is provided, the position will be configured to pull loan repayment when the loan
1866        /// becomes undercollateralized, preferring repayment to outright liquidation.
1867        access(EParticipant) fun createPosition(
1868            funds: @{FungibleToken.Vault},
1869            issuanceSink: {DeFiActions.Sink},
1870            repaymentSource: {DeFiActions.Source}?,
1871            pushToDrawDownSink: Bool
1872        ): UInt64 {
1873            pre {
1874                self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier) - not supported by this Pool"
1875            }
1876            // construct a new InternalPosition, assigning it the current position ID
1877            let id = self.nextPositionID
1878            self.nextPositionID = self.nextPositionID + 1
1879            self.positions[id] <-! create InternalPosition()
1880
1881            emit Opened(pid: id, poolUUID: self.uuid)
1882
1883            // assign issuance & repayment connectors within the InternalPosition
1884            let iPos = self._borrowPosition(pid: id)
1885            let fundsType = funds.getType()
1886            iPos.setDrawDownSink(issuanceSink)
1887            if repaymentSource != nil {
1888                iPos.setTopUpSource(repaymentSource)
1889            }
1890
1891            // deposit the initial funds & return the position ID
1892            self.depositAndPush(
1893                pid: id,
1894                from: <-funds,
1895                pushToDrawDownSink: pushToDrawDownSink
1896            )
1897            return id
1898        }
1899
1900        /// Allows anyone to deposit funds into any position. If the provided Vault is not supported by the Pool, the
1901        /// operation reverts.
1902        access(EParticipant) fun depositToPosition(pid: UInt64, from: @{FungibleToken.Vault}) {
1903            self.depositAndPush(pid: pid, from: <-from, pushToDrawDownSink: false)
1904        }
1905
1906        /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option. If
1907        /// `pushToDrawDownSink` is true, excess value putting the position above its max health is pushed to the
1908        /// position's configured `drawDownSink`.
1909        access(EPosition) fun depositAndPush(pid: UInt64, from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) {
1910            pre {
1911                self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool"
1912                self.globalLedger[from.getType()] != nil: "Invalid token type \(from.getType().identifier) - not supported by this Pool"
1913            }
1914            if self.debugLogging { log("    [CONTRACT] depositAndPush(pid: \(pid), pushToDrawDownSink: \(pushToDrawDownSink))") }
1915
1916            if from.balance == 0.0 {
1917                Burner.burn(<-from)
1918                return
1919            }
1920
1921            // Get a reference to the user's position and global token state for the affected token.
1922            let type = from.getType()
1923            let position = self._borrowPosition(pid: pid)
1924            let tokenState = self._borrowUpdatedTokenState(type: type)
1925            let amount = from.balance
1926            let depositedUUID = from.uuid
1927
1928            // Time-based state is handled by the tokenState() helper function
1929
1930            // Deposit rate limiting: prevent a single large deposit from monopolizing capacity.
1931            // Excess is queued to be processed asynchronously (see asyncUpdatePosition).
1932            let depositAmount = from.balance
1933            let depositLimit = tokenState.depositLimit()
1934
1935            if depositAmount > depositLimit {
1936                // The deposit is too big, so we need to queue the excess
1937                let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit)
1938
1939                if position.queuedDeposits[type] == nil {
1940                    position.queuedDeposits[type] <-! queuedDeposit
1941                } else {
1942                    position.queuedDeposits[type]!.deposit(from: <-queuedDeposit)
1943                }
1944            }
1945
1946            // If this position doesn't currently have an entry for this token, create one.
1947            if position.balances[type] == nil {
1948                position.balances[type] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
1949            }
1950
1951            // Create vault if it doesn't exist yet
1952            if self.reserves[type] == nil {
1953                self.reserves[type] <-! from.createEmptyVault()
1954            }
1955            let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
1956
1957            // Reflect the deposit in the position's balance
1958            // This only records the portion of the deposit that was accepted, not any queued portions,
1959            // as the queued deposits will be processed later (by this function being called again), and therefore
1960            // will be recorded at that time.
1961            position.balances[type]!.recordDeposit(amount: FlowCreditMarketMath.toUFix128(from.balance), tokenState: tokenState)
1962
1963            // Add the money to the reserves
1964            reserveVault.deposit(from: <-from)
1965
1966            // Rebalancing and queue management
1967            if pushToDrawDownSink {
1968                self.rebalancePosition(pid: pid, force: true)
1969            }
1970
1971            self._queuePositionForUpdateIfNecessary(pid: pid)
1972            emit Deposited(pid: pid, poolUUID: self.uuid, vaultType: type, amount: amount, depositedUUID: depositedUUID)
1973        }
1974
1975        /// Withdraws the requested funds from the specified position. Callers should be careful that the withdrawal
1976        /// does not put their position under its target health, especially if the position doesn't have a configured
1977        /// `topUpSource` from which to repay borrowed funds in the event of undercollaterlization.
1978        access(EPosition) fun withdraw(pid: UInt64, amount: UFix64, type: Type): @{FungibleToken.Vault} {
1979            // Call the enhanced function with pullFromTopUpSource = false for backward compatibility
1980            return <- self.withdrawAndPull(pid: pid, type: type, amount: amount, pullFromTopUpSource: false)
1981        }
1982
1983        /// Withdraws the requested funds from the specified position with the configurable `pullFromTopUpSource`
1984        /// option. If `pullFromTopUpSource` is true, deficient value putting the position below its min health is
1985        /// pulled from the position's configured `topUpSource`.
1986        access(EPosition) fun withdrawAndPull(
1987            pid: UInt64,
1988            type: Type,
1989            amount: UFix64,
1990            pullFromTopUpSource: Bool
1991        ): @{FungibleToken.Vault} {
1992            pre {
1993                self.positions[pid] != nil: "Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool"
1994                self.globalLedger[type] != nil: "Invalid token type \(type.identifier) - not supported by this Pool"
1995            }
1996            if self.debugLogging { log("    [CONTRACT] withdrawAndPull(pid: \(pid), type: \(type.identifier), amount: \(amount), pullFromTopUpSource: \(pullFromTopUpSource))") }
1997            if amount == 0.0 {
1998                return <- DeFiActionsUtils.getEmptyVault(type)
1999            }
2000
2001            // Get a reference to the user's position and global token state for the affected token.
2002            let position = self._borrowPosition(pid: pid)
2003            let tokenState = self._borrowUpdatedTokenState(type: type)
2004
2005            // Global interest indices are updated via tokenState() helper
2006
2007            // Preflight to see if the funds are available
2008            let topUpSource = position.topUpSource as auth(FungibleToken.Withdraw) &{DeFiActions.Source}?
2009            let topUpType = topUpSource?.getSourceType() ?? self.defaultToken
2010
2011            let requiredDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
2012                pid: pid,
2013                depositType: topUpType,
2014                targetHealth: position.minHealth,
2015                withdrawType: type,
2016                withdrawAmount: amount
2017            )
2018
2019            var canWithdraw = false
2020            var usedTopUp = false
2021
2022            if requiredDeposit == 0.0 {
2023                // We can service this withdrawal without any top up
2024                canWithdraw = true
2025            } else {
2026                // We need more funds to service this withdrawal, see if they are available from the top up source
2027                if pullFromTopUpSource && topUpSource != nil {
2028                    // If we have to rebalance, let's try to rebalance to the target health, not just the minimum
2029                    let idealDeposit = self.fundsRequiredForTargetHealthAfterWithdrawing(
2030                        pid: pid,
2031                        depositType: topUpType,
2032                        targetHealth: position.targetHealth,
2033                        withdrawType: type,
2034                        withdrawAmount: amount
2035                    )
2036
2037                    let pulledVault <- topUpSource!.withdrawAvailable(maxAmount: idealDeposit)
2038                    let pulledAmount = pulledVault.balance
2039
2040                    // NOTE: We requested the "ideal" deposit, but we compare against the required deposit here.
2041                    // The top up source may not have enough funds get us to the target health, but could have
2042                    // enough to keep us over the minimum.
2043                    if pulledAmount >= requiredDeposit {
2044                        // We can service this withdrawal if we deposit funds from our top up source
2045                        self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false)
2046                        usedTopUp = pulledAmount > 0.0
2047                        canWithdraw = true
2048                    } else {
2049                        // We can't get the funds required to service this withdrawal, so we need to redeposit what we got
2050                        self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false)
2051                        usedTopUp = pulledAmount > 0.0
2052                    }
2053                }
2054            }
2055
2056            if !canWithdraw {
2057                // Log detailed information about the failed withdrawal (only if debugging enabled)
2058                if self.debugLogging {
2059                    let availableBalance = self.availableBalance(pid: pid, type: type, pullFromTopUpSource: false)
2060                    log("    [CONTRACT] WITHDRAWAL FAILED:")
2061                    log("    [CONTRACT] Position ID: \(pid)")
2062                    log("    [CONTRACT] Token type: \(type.identifier)")
2063                    log("    [CONTRACT] Requested amount: \(amount)")
2064                    log("    [CONTRACT] Available balance (without topUp): \(availableBalance)")
2065                    log("    [CONTRACT] Required deposit for minHealth: \(requiredDeposit)")
2066                    log("    [CONTRACT] Pull from topUpSource: \(pullFromTopUpSource)")
2067                }
2068
2069                // We can't service this withdrawal, so we just abort
2070                panic("Cannot withdraw \(amount) of \(type.identifier) from position ID \(pid) - Insufficient funds for withdrawal")
2071            }
2072
2073            // If this position doesn't currently have an entry for this token, create one.
2074            if position.balances[type] == nil {
2075                position.balances[type] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
2076            }
2077
2078            let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)!
2079
2080            // Reflect the withdrawal in the position's balance
2081            let uintAmount = FlowCreditMarketMath.toUFix128(amount)
2082            position.balances[type]!.recordWithdrawal(amount: uintAmount, tokenState: tokenState)
2083            // Ensure that this withdrawal doesn't cause the position to be overdrawn.
2084            // Skip the assertion only when a top-up was used in this call and the immediate
2085            // post-withdrawal health is 0 (transitional state before top-up effects fully reflect).
2086            let postHealth = self.positionHealth(pid: pid)
2087            if !(usedTopUp && postHealth == 0.0 as UFix128) {
2088                assert(position.minHealth <= postHealth, message: "Position is overdrawn")
2089            }
2090
2091            // Queue for update if necessary
2092            self._queuePositionForUpdateIfNecessary(pid: pid)
2093
2094            let withdrawn <- reserveVault.withdraw(amount: amount)
2095
2096            emit Withdrawn(pid: pid, poolUUID: self.uuid, vaultType: type, amount: withdrawn.balance, withdrawnUUID: withdrawn.uuid)
2097
2098            return <- withdrawn
2099        }
2100
2101        /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when
2102        /// the position exceeds its maximum health. Note, if a non-nil value is provided, the Sink MUST accept the
2103        /// Pool's default deposits or the operation will revert.
2104        access(EPosition) fun provideDrawDownSink(pid: UInt64, sink: {DeFiActions.Sink}?) {
2105            let position = self._borrowPosition(pid: pid)
2106            position.setDrawDownSink(sink)
2107        }
2108
2109        /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when
2110        /// the position falls below its minimum health which may result in liquidation.
2111        access(EPosition) fun provideTopUpSource(pid: UInt64, source: {DeFiActions.Source}?) {
2112            let position = self._borrowPosition(pid: pid)
2113            position.setTopUpSource(source)
2114        }
2115
2116        // ---- Position health accessors (called via Position using EPosition capability) ----
2117        access(EPosition) view fun readTargetHealth(pid: UInt64): UFix128 {
2118            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2119            return pos.targetHealth
2120        }
2121        access(EPosition) view fun readMinHealth(pid: UInt64): UFix128 {
2122            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2123            return pos.minHealth
2124        }
2125        access(EPosition) view fun readMaxHealth(pid: UInt64): UFix128 {
2126            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2127            return pos.maxHealth
2128        }
2129        access(EPosition) fun writeTargetHealth(pid: UInt64, targetHealth: UFix128) {
2130            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2131            assert(targetHealth >= pos.minHealth, message: "targetHealth must be ≥ minHealth")
2132            assert(targetHealth <= pos.maxHealth, message: "targetHealth must be ≤ maxHealth")
2133            pos.setTargetHealth(targetHealth)
2134        }
2135        access(EPosition) fun writeMinHealth(pid: UInt64, minHealth: UFix128) {
2136            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2137            assert(minHealth <= pos.targetHealth, message: "minHealth must be ≤ targetHealth")
2138            pos.setMinHealth(minHealth)
2139        }
2140        access(EPosition) fun writeMaxHealth(pid: UInt64, maxHealth: UFix128) {
2141            let pos: auth(EImplementation) &InternalPosition = self._borrowPosition(pid: pid)
2142            assert(maxHealth >= pos.targetHealth, message: "maxHealth must be ≥ targetHealth")
2143            pos.setMaxHealth(maxHealth)
2144        }
2145
2146        ///////////////////////
2147        // POOL MANAGEMENT
2148        ///////////////////////
2149
2150        /// Updates liquidation-related parameters (any nil values are ignored)
2151        access(EGovernance) fun setLiquidationParams(
2152            targetHF: UFix128?,
2153            warmupSec: UInt64?,
2154            protocolFeeBps: UInt16?
2155        ) {
2156            var newTarget: UFix128 = self.liquidationTargetHF
2157            var newWarmup: UInt64 = self.liquidationWarmupSec
2158            var newProtocolFee: UInt16 = self.protocolLiquidationFeeBps
2159            if targetHF != nil {
2160                assert(targetHF! > FlowCreditMarketMath.one, message: "targetHF must be > 1.0")
2161                self.liquidationTargetHF = targetHF!
2162                newTarget = targetHF!
2163            }
2164            if warmupSec != nil {
2165                self.liquidationWarmupSec = warmupSec!
2166                newWarmup = warmupSec!
2167            }
2168            if protocolFeeBps != nil {
2169                self.protocolLiquidationFeeBps = protocolFeeBps!
2170                newProtocolFee = protocolFeeBps!
2171            }
2172            emit LiquidationParamsUpdated(poolUUID: self.uuid, targetHF: newTarget, warmupSec: newWarmup, protocolFeeBps: newProtocolFee)
2173        }
2174
2175        /// Governance: set DEX oracle deviation guard and toggle allowlisted swapper types
2176        access(EGovernance) fun setDexLiquidationConfig(
2177            dexOracleDeviationBps: UInt16?,
2178            allowSwappers: [Type]?,
2179            disallowSwappers: [Type]?,
2180            dexMaxSlippageBps: UInt64?,
2181            dexMaxRouteHops: UInt64?
2182        ) {
2183            if dexOracleDeviationBps != nil { self.dexOracleDeviationBps = dexOracleDeviationBps! }
2184            if allowSwappers != nil {
2185                for t in allowSwappers! {
2186                    self.allowedSwapperTypes[t] = true
2187                }
2188            }
2189            if disallowSwappers != nil {
2190                for t in disallowSwappers! {
2191                    self.allowedSwapperTypes.remove(key: t)
2192                }
2193            }
2194            if dexMaxSlippageBps != nil { self.dexMaxSlippageBps = dexMaxSlippageBps! }
2195            if dexMaxRouteHops != nil { self.dexMaxRouteHops = dexMaxRouteHops! }
2196        }
2197
2198        /// Pauses or unpauses liquidations; when unpausing, starts a warm-up window
2199        access(EGovernance) fun pauseLiquidations(flag: Bool) {
2200            if flag {
2201                self.liquidationsPaused = true
2202                emit LiquidationsPaused(poolUUID: self.uuid)
2203            } else {
2204                self.liquidationsPaused = false
2205                let now = UInt64(getCurrentBlock().timestamp)
2206                self.lastUnpausedAt = now
2207                emit LiquidationsUnpaused(poolUUID: self.uuid, warmupEndsAt: now + self.liquidationWarmupSec)
2208            }
2209        }
2210
2211        /// Adds a new token type to the pool with the given parameters defining borrowing limits on collateral,
2212        /// interest accumulation, deposit rate limiting, and deposit size capacity
2213        access(EGovernance) fun addSupportedToken(
2214            tokenType: Type,
2215            collateralFactor: UFix64,
2216            borrowFactor: UFix64,
2217            interestCurve: {InterestCurve},
2218            depositRate: UFix64,
2219            depositCapacityCap: UFix64
2220        ) {
2221            pre {
2222                self.globalLedger[tokenType] == nil: "Token type already supported"
2223                tokenType.isSubtype(of: Type<@{FungibleToken.Vault}>()):
2224                "Invalid token type \(tokenType.identifier) - tokenType must be a FungibleToken Vault implementation"
2225                collateralFactor > 0.0 && collateralFactor <= 1.0: "Collateral factor must be between 0 and 1"
2226                borrowFactor > 0.0 && borrowFactor <= 1.0: "Borrow factor must be between 0 and 1"
2227                depositRate > 0.0: "Deposit rate must be positive"
2228                depositCapacityCap > 0.0: "Deposit capacity cap must be positive"
2229                DeFiActionsUtils.definingContractIsFungibleToken(tokenType):
2230                "Invalid token contract definition for tokenType \(tokenType.identifier) - defining contract is not FungibleToken conformant"
2231            }
2232
2233            // Add token to global ledger with its interest curve and deposit parameters
2234            self.globalLedger[tokenType] = TokenState(
2235                interestCurve: interestCurve,
2236                depositRate: depositRate,
2237                depositCapacityCap: depositCapacityCap
2238            )
2239
2240            // Set collateral factor (what percentage of value can be used as collateral)
2241            self.collateralFactor[tokenType] = collateralFactor
2242
2243            // Set borrow factor (risk adjustment for borrowed amounts)
2244            self.borrowFactor[tokenType] = borrowFactor
2245
2246            // Default liquidation bonus per token = 5%
2247            self.liquidationBonus[tokenType] = 0.05
2248        }
2249
2250        // Removed: addSupportedTokenWithLiquidationBonus — callers should use addSupportedToken then setTokenLiquidationBonus if needed
2251
2252        /// Sets per-token liquidation bonus fraction (0.0 to 1.0). E.g., 0.05 means +5% seize bonus.
2253        access(EGovernance) fun setTokenLiquidationBonus(tokenType: Type, bonus: UFix64) {
2254            pre {
2255                self.globalLedger[tokenType] != nil: "Unsupported token type"
2256                bonus >= 0.0 && bonus <= 1.0: "Liquidation bonus must be between 0 and 1"
2257            }
2258            self.liquidationBonus[tokenType] = bonus
2259        }
2260
2261        /// Updates the insurance rate for a given token (fraction in [0,1])
2262        access(EGovernance) fun setInsuranceRate(tokenType: Type, insuranceRate: UFix64) {
2263            pre {
2264                self.globalLedger[tokenType] != nil: "Unsupported token type"
2265                insuranceRate >= 0.0 && insuranceRate <= 1.0: "insuranceRate must be between 0 and 1"
2266            }
2267            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
2268                ?? panic("Invariant: token state missing")
2269            tsRef.setInsuranceRate(insuranceRate)
2270        }
2271
2272        /// Updates the per-deposit limit fraction for a given token (fraction in [0,1])
2273        access(EGovernance) fun setDepositLimitFraction(tokenType: Type, fraction: UFix64) {
2274            pre {
2275                self.globalLedger[tokenType] != nil: "Unsupported token type"
2276                fraction > 0.0 && fraction <= 1.0: "fraction must be in (0,1]"
2277            }
2278            let tsRef = &self.globalLedger[tokenType] as auth(EImplementation) &TokenState?
2279                ?? panic("Invariant: token state missing")
2280            tsRef.setDepositLimitFraction(fraction)
2281        }
2282
2283        /// Enables or disables verbose logging inside the Pool for testing and diagnostics
2284        access(EGovernance) fun setDebugLogging(_ enabled: Bool) {
2285            self.debugLogging = enabled
2286        }
2287
2288        /// Rebalances the position to the target health value. If `force` is `true`, the position will be rebalanced
2289        /// even if it is currently healthy. Otherwise, this function will do nothing if the position is within the
2290        /// min/max health bounds.
2291        access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) {
2292            if self.debugLogging { log("    [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") }
2293            let position = self._borrowPosition(pid: pid)
2294            let balanceSheet = self._getUpdatedBalanceSheet(pid: pid)
2295
2296            if !force && (position.minHealth <= balanceSheet.health  && balanceSheet.health <= position.maxHealth) {
2297                // We aren't forcing the update, and the position is already between its desired min and max. Nothing to do!
2298                return
2299            }
2300
2301            if balanceSheet.health < position.targetHealth {
2302                // The position is undercollateralized, see if the source can get more collateral to bring it up to the target health.
2303                if position.topUpSource != nil {
2304                    let topUpSource = position.topUpSource! as auth(FungibleToken.Withdraw) &{DeFiActions.Source}
2305                    let idealDeposit = self.fundsRequiredForTargetHealth(
2306                        pid: pid,
2307                        type: topUpSource.getSourceType(),
2308                        targetHealth: position.targetHealth
2309                    )
2310                    if self.debugLogging { log("    [CONTRACT] idealDeposit: \(idealDeposit)") }
2311
2312                    let pulledVault <- topUpSource.withdrawAvailable(maxAmount: idealDeposit)
2313
2314                    emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: pulledVault.balance, fromUnder: true)
2315
2316                    self.depositAndPush(pid: pid, from: <-pulledVault, pushToDrawDownSink: false)
2317                }
2318            } else if balanceSheet.health > position.targetHealth {
2319                // The position is overcollateralized, we'll withdraw funds to match the target health and offer it to the sink.
2320                if position.drawDownSink != nil {
2321                    let drawDownSink = position.drawDownSink!
2322                    let sinkType = drawDownSink.getSinkType()
2323                    let idealWithdrawal = self.fundsAvailableAboveTargetHealth(
2324                        pid: pid,
2325                        type: sinkType,
2326                        targetHealth: position.targetHealth
2327                    )
2328                    if self.debugLogging { log("    [CONTRACT] idealWithdrawal: \(idealWithdrawal)") }
2329
2330                    // Compute how many tokens of the sink's type are available to hit our target health.
2331                    let sinkCapacity = drawDownSink.minimumCapacity()
2332                    let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal
2333
2334                    if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { 
2335                        let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>())
2336                        if position.balances[Type<@MOET.Vault>()] == nil {
2337                            position.balances[Type<@MOET.Vault>()] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0 as UFix128)
2338                        }
2339                        // record the withdrawal and mint the tokens
2340                        let uintSinkAmount = FlowCreditMarketMath.toUFix128(sinkAmount)
2341                        position.balances[Type<@MOET.Vault>()]!.recordWithdrawal(amount: uintSinkAmount, tokenState: tokenState)
2342                        let sinkVault <- FlowCreditMarket._borrowMOETMinter().mintTokens(amount: sinkAmount)
2343
2344                        emit Rebalanced(pid: pid, poolUUID: self.uuid, atHealth: balanceSheet.health, amount: sinkVault.balance, fromUnder: false)
2345
2346                        // Push what we can into the sink, and redeposit the rest
2347                        drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault})
2348                        if sinkVault.balance > 0.0 {
2349                            self.depositAndPush(pid: pid, from: <-sinkVault, pushToDrawDownSink: false)
2350                        } else {
2351                            Burner.burn(<-sinkVault)
2352                        }
2353                    }
2354                }
2355            }
2356        }
2357
2358        /// Executes asynchronous updates on positions that have been queued up to the lesser of the queue length or
2359        /// the configured positionsProcessedPerCallback value
2360        access(EImplementation) fun asyncUpdate() {
2361            // TODO: In the production version, this function should only process some positions (limited by positionsProcessedPerCallback) AND
2362            // it should schedule each update to run in its own callback, so a revert() call from one update (for example, if a source or
2363            // sink aborts) won't prevent other positions from being updated.
2364            var processed: UInt64 = 0
2365            while self.positionsNeedingUpdates.length > 0 && processed < self.positionsProcessedPerCallback {
2366                let pid = self.positionsNeedingUpdates.removeFirst()
2367                self.asyncUpdatePosition(pid: pid)
2368                self._queuePositionForUpdateIfNecessary(pid: pid)
2369                processed = processed + 1
2370            }
2371        }
2372
2373        /// Executes an asynchronous update on the specified position
2374        access(EImplementation) fun asyncUpdatePosition(pid: UInt64) {
2375            let position = self._borrowPosition(pid: pid)
2376
2377            // First check queued deposits, their addition could affect the rebalance we attempt later
2378            for depositType in position.queuedDeposits.keys {
2379                let queuedVault <- position.queuedDeposits.remove(key: depositType)!
2380                let queuedAmount = queuedVault.balance
2381                let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
2382                let maxDeposit = depositTokenState.depositLimit()
2383
2384                if maxDeposit >= queuedAmount {
2385                    // We can deposit all of the queued deposit, so just do it and remove it from the queue
2386                    self.depositAndPush(pid: pid, from: <-queuedVault, pushToDrawDownSink: false)
2387                } else {
2388                    // We can only deposit part of the queued deposit, so do that and leave the rest in the queue
2389                    // for the next time we run.
2390                    let depositVault <- queuedVault.withdraw(amount: maxDeposit)
2391                    self.depositAndPush(pid: pid, from: <-depositVault, pushToDrawDownSink: false)
2392
2393                    // We need to update the queued vault to reflect the amount we used up
2394                    position.queuedDeposits[depositType] <-! queuedVault
2395                }
2396            }
2397
2398            // Now that we've deposited a non-zero amount of any queued deposits, we can rebalance
2399            // the position if necessary.
2400            self.rebalancePosition(pid: pid, force: false)
2401        }
2402
2403        ////////////////
2404        // INTERNAL
2405        ////////////////
2406
2407        /// Queues a position for asynchronous updates if the position has been marked as requiring an update
2408        access(self) fun _queuePositionForUpdateIfNecessary(pid: UInt64) {
2409            if self.positionsNeedingUpdates.contains(pid) {
2410                // If this position is already queued for an update, no need to check anything else
2411                return
2412            } else {
2413                // If this position is not already queued for an update, we need to check if it needs one
2414                let position = self._borrowPosition(pid: pid)
2415
2416                if position.queuedDeposits.length > 0 {
2417                    // This position has deposits that need to be processed, so we need to queue it for an update
2418                    self.positionsNeedingUpdates.append(pid)
2419                    return
2420                }
2421
2422                let positionHealth = self.positionHealth(pid: pid)
2423
2424                if positionHealth < position.minHealth || positionHealth > position.maxHealth {
2425                    // This position is outside the configured health bounds, we queue it for an update
2426                    self.positionsNeedingUpdates.append(pid)
2427                    return
2428                }
2429            }
2430        }
2431
2432        /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health
2433        access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet {
2434            let position = self._borrowPosition(pid: pid)
2435            let priceOracle = &self.priceOracle as &{DeFiActions.PriceOracle}
2436
2437            // Get the position's collateral and debt values in terms of the default token.
2438            var effectiveCollateral: UFix128 = 0.0 as UFix128
2439            var effectiveDebt: UFix128 = 0.0 as UFix128
2440
2441            for type in position.balances.keys {
2442                let balance = position.balances[type]!
2443                let tokenState = self._borrowUpdatedTokenState(type: type)
2444                if balance.direction == BalanceDirection.Credit {
2445                    let trueBalance = FlowCreditMarket.scaledBalanceToTrueBalance(balance.scaledBalance,
2446                        interestIndex: tokenState.creditInterestIndex)
2447
2448                    let convertedPrice = FlowCreditMarketMath.toUFix128(priceOracle.price(ofToken: type)!)
2449                    let value = convertedPrice * trueBalance
2450
2451                    let convertedCollateralFactor = FlowCreditMarketMath.toUFix128(self.collateralFactor[type]!)
2452                    effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor)
2453                } else {
2454                    let trueBalance = FlowCreditMarket.scaledBalanceToTrueBalance(balance.scaledBalance,
2455                        interestIndex: tokenState.debitInterestIndex)
2456
2457                    let convertedPrice = FlowCreditMarketMath.toUFix128(priceOracle.price(ofToken: type)!)
2458                    let value = convertedPrice * trueBalance
2459
2460                    let convertedBorrowFactor = FlowCreditMarketMath.toUFix128(self.borrowFactor[type]!)
2461                    effectiveDebt = effectiveDebt + FlowCreditMarketMath.div(value, convertedBorrowFactor)
2462                }
2463            }
2464
2465            return BalanceSheet(effectiveCollateral: effectiveCollateral, effectiveDebt: effectiveDebt)
2466        }
2467
2468        /// A convenience function that returns a reference to a particular token state, making sure it's up-to-date for
2469        /// the passage of time. This should always be used when accessing a token state to avoid missing interest
2470        /// updates (duplicate calls to updateForTimeChange() are a nop within a single block).
2471        access(self) fun _borrowUpdatedTokenState(type: Type): auth(EImplementation) &TokenState {
2472            let state = &self.globalLedger[type]! as auth(EImplementation) &TokenState
2473            state.updateForTimeChange()
2474            return state
2475        }
2476
2477        /// Returns an authorized reference to the requested InternalPosition or `nil` if the position does not exist
2478        access(self) view fun _borrowPosition(pid: UInt64): auth(EImplementation) &InternalPosition {
2479            return &self.positions[pid] as auth(EImplementation) &InternalPosition?
2480                ?? panic("Invalid position ID \(pid) - could not find an InternalPosition with the requested ID in the Pool")
2481        }
2482
2483        /// Build a PositionView for the given position ID
2484        access(all) fun buildPositionView(pid: UInt64): FlowCreditMarket.PositionView {
2485            let position = self._borrowPosition(pid: pid)
2486            let snaps: {Type: FlowCreditMarket.TokenSnapshot} = {}
2487            let balancesCopy: {Type: FlowCreditMarket.InternalBalance} = position.copyBalances()
2488            for t in position.balances.keys {
2489                let tokenState = self._borrowUpdatedTokenState(type: t)
2490                snaps[t] = FlowCreditMarket.TokenSnapshot(
2491                    price: FlowCreditMarketMath.toUFix128(self.priceOracle.price(ofToken: t)!),
2492                    credit: tokenState.creditInterestIndex,
2493                    debit: tokenState.debitInterestIndex,
2494                    risk: FlowCreditMarket.RiskParams(
2495                        cf: FlowCreditMarketMath.toUFix128(self.collateralFactor[t]!),
2496                        bf: FlowCreditMarketMath.toUFix128(self.borrowFactor[t]!),
2497                        lb: FlowCreditMarketMath.toUFix128(self.liquidationBonus[t]!)
2498                    )
2499                )
2500            }
2501            return FlowCreditMarket.PositionView(
2502                balances: balancesCopy,
2503                snapshots: snaps,
2504                def: self.defaultToken,
2505                min: position.minHealth,
2506                max: position.maxHealth
2507            )
2508        }
2509        access(EGovernance) fun setPriceOracle(_ newOracle: {DeFiActions.PriceOracle}) {
2510            pre {
2511                newOracle.unitOfAccount() == self.defaultToken:
2512                    "Price oracle must return prices in terms of the pool's default token"
2513            }
2514            self.priceOracle = newOracle
2515            self.positionsNeedingUpdates = self.positions.keys
2516
2517            emit PriceOracleUpdated(poolUUID: self.uuid, newOracleType: newOracle.getType().identifier)
2518        }
2519        access(all) fun getDefaultToken(): Type {
2520            return self.defaultToken
2521        }
2522    }
2523
2524    /// PoolFactory
2525    ///
2526    /// Resource enabling the contract account to create the contract's Pool. This pattern is used in place of contract
2527    /// methods to ensure limited access to pool creation. While this could be done in contract's init, doing so here
2528    /// will allow for the setting of the Pool's PriceOracle without the introduction of a concrete PriceOracle defining
2529    /// contract which would include an external contract dependency.
2530    ///
2531    access(all) resource PoolFactory {
2532        /// Creates the contract-managed Pool and saves it to the canonical path, reverting if one is already stored
2533        access(all) fun createPool(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}) {
2534            pre {
2535                FlowCreditMarket.account.storage.type(at: FlowCreditMarket.PoolStoragePath) == nil:
2536                "Storage collision - Pool has already been created & saved to \(FlowCreditMarket.PoolStoragePath)"
2537            }
2538            let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle)
2539            FlowCreditMarket.account.storage.save(<-pool, to: FlowCreditMarket.PoolStoragePath)
2540            let cap = FlowCreditMarket.account.capabilities.storage.issue<&Pool>(FlowCreditMarket.PoolStoragePath)
2541            FlowCreditMarket.account.capabilities.unpublish(FlowCreditMarket.PoolPublicPath)
2542            FlowCreditMarket.account.capabilities.publish(cap, at: FlowCreditMarket.PoolPublicPath)
2543        }
2544    }
2545
2546    /// Position
2547    ///
2548    /// A Position is an external object representing ownership of value deposited to the protocol. From a Position, an
2549    /// actor can deposit and withdraw funds as well as construct DeFiActions components enabling value flows in and out
2550    /// of the Position from within the context of DeFiActions stacks.
2551    ///
2552    // TODO: Consider making this a resource given how critical it is to accessing a loan
2553    access(all) struct Position {
2554        /// The unique ID of the Position used to track deposits and withdrawals to the Pool
2555        access(self) let id: UInt64
2556        /// An authorized Capability to which the Position was opened
2557        access(self) let pool: Capability<auth(EPosition, EParticipant) &Pool>
2558
2559        init(id: UInt64, pool: Capability<auth(EPosition, EParticipant) &Pool>) {
2560            pre {
2561                pool.check(): "Invalid Pool Capability provided - cannot construct Position"
2562            }
2563            self.id = id
2564            self.pool = pool
2565        }
2566
2567        /// Returns the balances (both positive and negative) for all tokens in this position.
2568        access(all) fun getBalances(): [PositionBalance] {
2569            let pool = self.pool.borrow()!
2570            return pool.getPositionDetails(pid: self.id).balances
2571        }
2572        /// Returns the balance available for withdrawal of a given Vault type. If pullFromTopUpSource is true, the
2573        /// calculation will be made assuming the position is topped up if the withdrawal amount puts the Position
2574        /// below its min health. If pullFromTopUpSource is false, the calculation will return the balance currently
2575        /// available without topping up the position.
2576        access(all) fun availableBalance(type: Type, pullFromTopUpSource: Bool): UFix64 {
2577            let pool = self.pool.borrow()!
2578            return pool.availableBalance(pid: self.id, type: type, pullFromTopUpSource: pullFromTopUpSource)
2579        }
2580        /// Returns the current health of the position
2581        access(all) fun getHealth(): UFix128 {
2582            let pool = self.pool.borrow()!
2583            return pool.positionHealth(pid: self.id)
2584        }
2585        /// Returns the Position's target health (unitless ratio ≥ 1.0)
2586        access(all) fun getTargetHealth(): UFix64 {
2587            let pool = self.pool.borrow()!
2588            let uint = pool.readTargetHealth(pid: self.id)
2589            return FlowCreditMarketMath.toUFix64Round(uint)
2590        }
2591        /// Sets the target health of the Position
2592        access(all) fun setTargetHealth(targetHealth: UFix64) {
2593            let pool = self.pool.borrow()!
2594            let uint = FlowCreditMarketMath.toUFix128(targetHealth)
2595            pool.writeTargetHealth(pid: self.id, targetHealth: uint)
2596        }
2597        /// Returns the minimum health of the Position
2598        access(all) fun getMinHealth(): UFix64 {
2599            let pool = self.pool.borrow()!
2600            let uint = pool.readMinHealth(pid: self.id)
2601            return FlowCreditMarketMath.toUFix64Round(uint)
2602        }
2603        /// Sets the minimum health of the Position
2604        access(all) fun setMinHealth(minHealth: UFix64) {
2605            let pool = self.pool.borrow()!
2606            let uint = FlowCreditMarketMath.toUFix128(minHealth)
2607            pool.writeMinHealth(pid: self.id, minHealth: uint)
2608        }
2609        /// Returns the maximum health of the Position
2610        access(all) fun getMaxHealth(): UFix64 {
2611            let pool = self.pool.borrow()!
2612            let uint = pool.readMaxHealth(pid: self.id)
2613            return FlowCreditMarketMath.toUFix64Round(uint)
2614        }
2615        /// Sets the maximum health of the position
2616        access(all) fun setMaxHealth(maxHealth: UFix64) {
2617            let pool = self.pool.borrow()!
2618            let uint = FlowCreditMarketMath.toUFix128(maxHealth)
2619            pool.writeMaxHealth(pid: self.id, maxHealth: uint)
2620        }
2621        /// Returns the maximum amount of the given token type that could be deposited into this position
2622        access(all) fun getDepositCapacity(type: Type): UFix64 {
2623            // There's no limit on deposits from the position's perspective
2624            return UFix64.max
2625        }
2626        /// Deposits funds to the Position without pushing to the drawDownSink if the deposit puts the Position above
2627        /// its maximum health
2628        access(EParticipant) fun deposit(from: @{FungibleToken.Vault}) {
2629            let pool = self.pool.borrow()!
2630            pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: false)
2631        }
2632        /// Deposits funds to the Position enabling the caller to configure whether excess value should be pushed to the
2633        /// drawDownSink if the deposit puts the Position above its maximum health
2634        access(EParticipant) fun depositAndPush(from: @{FungibleToken.Vault}, pushToDrawDownSink: Bool) {
2635            let pool = self.pool.borrow()!
2636            pool.depositAndPush(pid: self.id, from: <-from, pushToDrawDownSink: pushToDrawDownSink)
2637        }
2638        /// Withdraws funds from the Position without pulling from the topUpSource if the deposit puts the Position below
2639        /// its minimum health
2640        access(FungibleToken.Withdraw) fun withdraw(type: Type, amount: UFix64): @{FungibleToken.Vault} {
2641            return <- self.withdrawAndPull(type: type, amount: amount, pullFromTopUpSource: false)
2642        }
2643        /// Withdraws funds from the Position enabling the caller to configure whether insufficient value should be
2644        /// pulled from the topUpSource if the deposit puts the Position below its minimum health
2645        access(FungibleToken.Withdraw) fun withdrawAndPull(type: Type, amount: UFix64, pullFromTopUpSource: Bool): @{FungibleToken.Vault} {
2646            let pool = self.pool.borrow()!
2647            return <- pool.withdrawAndPull(pid: self.id, type: type, amount: amount, pullFromTopUpSource: pullFromTopUpSource)
2648        }
2649        /// Returns a new Sink for the given token type that will accept deposits of that token and update the
2650        /// position's collateral and/or debt accordingly. Note that calling this method multiple times will create
2651        /// multiple sinks, each of which will continue to work regardless of how many other sinks have been created.
2652        access(all) fun createSink(type: Type): {DeFiActions.Sink} {
2653            // create enhanced sink with pushToDrawDownSink option
2654            return self.createSinkWithOptions(type: type, pushToDrawDownSink: false)
2655        }
2656        /// Returns a new Sink for the given token type and pushToDrawDownSink opetion that will accept deposits of that
2657        /// token and update the position's collateral and/or debt accordingly. Note that calling this method multiple
2658        /// times will create multiple sinks, each of which will continue to work regardless of how many other sinks
2659        /// have been created.
2660        access(all) fun createSinkWithOptions(type: Type, pushToDrawDownSink: Bool): {DeFiActions.Sink} {
2661            let pool = self.pool.borrow()!
2662            return PositionSink(id: self.id, pool: self.pool, type: type, pushToDrawDownSink: pushToDrawDownSink)
2663        }
2664        /// Returns a new Source for the given token type that will service withdrawals of that token and update the
2665        /// position's collateral and/or debt accordingly. Note that calling this method multiple times will create
2666        /// multiple sources, each of which will continue to work regardless of how many other sources have been created.
2667        access(FungibleToken.Withdraw) fun createSource(type: Type): {DeFiActions.Source} {
2668            // Create enhanced source with pullFromTopUpSource = true
2669            return self.createSourceWithOptions(type: type, pullFromTopUpSource: false)
2670        }
2671        /// Returns a new Source for the given token type and pullFromTopUpSource option that will service withdrawals
2672        /// of that token and update the position's collateral and/or debt accordingly. Note that calling this method
2673        /// multiple times will create multiple sources, each of which will continue to work regardless of how many
2674        /// other sources have been created.
2675        access(FungibleToken.Withdraw) fun createSourceWithOptions(type: Type, pullFromTopUpSource: Bool): {DeFiActions.Source} {
2676            let pool = self.pool.borrow()!
2677            return PositionSource(id: self.id, pool: self.pool, type: type, pullFromTopUpSource: pullFromTopUpSource)
2678        }
2679        /// Provides a sink to the Position that will have tokens proactively pushed into it when the position has
2680        /// excess collateral. (Remember that sinks do NOT have to accept all tokens provided to them; the sink can
2681        /// choose to accept only some (or none) of the tokens provided, leaving the position overcollateralized).
2682        ///
2683        /// Each position can have only one sink, and the sink must accept the default token type configured for the
2684        /// pool. Providing a new sink will replace the existing sink. Pass nil to configure the position to not push
2685        /// tokens when the Position exceeds its maximum health.
2686        access(FungibleToken.Withdraw) fun provideSink(sink: {DeFiActions.Sink}?) {
2687            let pool = self.pool.borrow()!
2688            pool.provideDrawDownSink(pid: self.id, sink: sink)
2689        }
2690        /// Provides a source to the Position that will have tokens proactively pulled from it when the position has
2691        /// insufficient collateral. If the source can cover the position's debt, the position will not be liquidated.
2692        ///
2693        /// Each position can have only one source, and the source must accept the default token type configured for the
2694        /// pool. Providing a new source will replace the existing source. Pass nil to configure the position to not
2695        /// pull tokens.
2696        access(EParticipant) fun provideSource(source: {DeFiActions.Source}?) {
2697            let pool = self.pool.borrow()!
2698            pool.provideTopUpSource(pid: self.id, source: source)
2699        }
2700    }
2701
2702    /// PositionSink
2703    ///
2704    /// A DeFiActions connector enabling deposits to a Position from within a DeFiActions stack. This Sink is intended to
2705    /// be constructed from a Position object.
2706    access(all) struct PositionSink: DeFiActions.Sink {
2707        /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of
2708        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
2709        /// An authorized Capability on the Pool for which the related Position is in
2710        access(self) let pool: Capability<auth(EPosition) &Pool>
2711        /// The ID of the position in the Pool
2712        access(self) let positionID: UInt64
2713        /// The Type of Vault this Sink accepts
2714        access(self) let type: Type
2715        /// Whether deposits through this Sink to the Position should push available value to the Position's
2716        /// drawDownSink
2717        access(self) let pushToDrawDownSink: Bool
2718
2719        init(id: UInt64, pool: Capability<auth(EPosition) &Pool>, type: Type, pushToDrawDownSink: Bool) {
2720            self.uniqueID = nil
2721            self.positionID = id
2722            self.pool = pool
2723            self.type = type
2724            self.pushToDrawDownSink = pushToDrawDownSink
2725        }
2726
2727        /// Returns the Type of Vault this Sink accepts on deposits
2728        access(all) view fun getSinkType(): Type {
2729            return self.type
2730        }
2731        /// Returns the minimum capacity this Sink can accept as deposits
2732        access(all) fun minimumCapacity(): UFix64 {
2733            return self.pool.check() ? UFix64.max : 0.0
2734        }
2735        /// Deposits the funds from the provided Vault reference to the related Position
2736        access(all) fun depositCapacity(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) {
2737            if let pool = self.pool.borrow() {
2738                pool.depositAndPush(
2739                    pid: self.positionID,
2740                    from: <-from.withdraw(amount: from.balance),
2741                    pushToDrawDownSink: self.pushToDrawDownSink
2742                )
2743            }
2744        }
2745        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
2746            return DeFiActions.ComponentInfo(
2747                type: self.getType(),
2748                id: self.id(),
2749                innerComponents: []
2750            )
2751        }
2752        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
2753            return self.uniqueID
2754        }
2755        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
2756            self.uniqueID = id
2757        }
2758    }
2759
2760    /// PositionSource
2761    ///
2762    /// A DeFiActions connector enabling withdrawals from a Position from within a DeFiActions stack. This Source is
2763    /// intended to be constructed from a Position object.
2764    ///
2765    access(all) struct PositionSource: DeFiActions.Source {
2766        /// An optional DeFiActions.UniqueIdentifier that identifies this Sink with the DeFiActions stack its a part of
2767        access(contract) var uniqueID: DeFiActions.UniqueIdentifier?
2768        /// An authorized Capability on the Pool for which the related Position is in
2769        access(self) let pool: Capability<auth(EPosition) &Pool>
2770        /// The ID of the position in the Pool
2771        access(self) let positionID: UInt64
2772        /// The Type of Vault this Sink provides
2773        access(self) let type: Type
2774        /// Whether withdrawals through this Sink from the Position should pull value from the Position's topUpSource
2775        /// in the event the withdrawal puts the position under its target health
2776        access(self) let pullFromTopUpSource: Bool
2777
2778        init(id: UInt64, pool: Capability<auth(EPosition) &Pool>, type: Type, pullFromTopUpSource: Bool) {
2779            self.uniqueID = nil
2780            self.positionID = id
2781            self.pool = pool
2782            self.type = type
2783            self.pullFromTopUpSource = pullFromTopUpSource
2784        }
2785
2786        /// Returns the Type of Vault this Source provides on withdrawals
2787        access(all) view fun getSourceType(): Type {
2788            return self.type
2789        }
2790        /// Returns the minimum availble this Source can provide on withdrawal
2791        access(all) fun minimumAvailable(): UFix64 {
2792            if !self.pool.check() {
2793                return 0.0
2794            }
2795            let pool = self.pool.borrow()!
2796            return pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource)
2797        }
2798        /// Withdraws up to the max amount as the sourceType Vault
2799        access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} {
2800            if !self.pool.check() {
2801                return <- DeFiActionsUtils.getEmptyVault(self.type)
2802            }
2803            let pool = self.pool.borrow()!
2804            let available = pool.availableBalance(pid: self.positionID, type: self.type, pullFromTopUpSource: self.pullFromTopUpSource)
2805            let withdrawAmount = (available > maxAmount) ? maxAmount : available
2806            if withdrawAmount > 0.0 {
2807                return <- pool.withdrawAndPull(pid: self.positionID, type: self.type, amount: withdrawAmount, pullFromTopUpSource: self.pullFromTopUpSource)
2808            } else {
2809                // Create an empty vault - this is a limitation we need to handle properly
2810                return <- DeFiActionsUtils.getEmptyVault(self.type)
2811            }
2812        }
2813        access(all) fun getComponentInfo(): DeFiActions.ComponentInfo {
2814            return DeFiActions.ComponentInfo(
2815                type: self.getType(),
2816                id: self.id(),
2817                innerComponents: []
2818            )
2819        }
2820        access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? {
2821            return self.uniqueID
2822        }
2823        access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) {
2824            self.uniqueID = id
2825        }
2826    }
2827
2828    /// BalanceDirection
2829    ///
2830    /// The direction of a given balance
2831    access(all) enum BalanceDirection: UInt8 {
2832        /// Denotes that a balance that is withdrawable from the protocol
2833        access(all) case Credit
2834        /// Denotes that a balance that is due to the protocol
2835        access(all) case Debit
2836    }
2837
2838    /// PositionBalance
2839    ///
2840    /// A structure returned externally to report a position's balance for a particular token.
2841    /// This structure is NOT used internally.
2842    access(all) struct PositionBalance {
2843        /// The token type for which the balance details relate to
2844        access(all) let vaultType: Type
2845        /// Whether the balance is a Credit or Debit
2846        access(all) let direction: BalanceDirection
2847        /// The balance of the token for the related Position
2848        access(all) let balance: UFix64
2849
2850        init(vaultType: Type, direction: BalanceDirection, balance: UFix64) {
2851            self.vaultType = vaultType
2852            self.direction = direction
2853            self.balance = balance
2854        }
2855    }
2856
2857    /// PositionDetails
2858    ///
2859    /// A structure returned externally to report all of the details associated with a position.
2860    /// This structure is NOT used internally.
2861    access(all) struct PositionDetails {
2862        /// Balance details about each Vault Type deposited to the related Position
2863        access(all) let balances: [PositionBalance]
2864        /// The default token Type of the Pool in which the related position is held
2865        access(all) let poolDefaultToken: Type
2866        /// The available balance of the Pool's default token Type
2867        access(all) let defaultTokenAvailableBalance: UFix64
2868        /// The current health of the related position
2869        access(all) let health: UFix128
2870
2871        init(balances: [PositionBalance], poolDefaultToken: Type, defaultTokenAvailableBalance: UFix64, health: UFix128) {
2872            self.balances = balances
2873            self.poolDefaultToken = poolDefaultToken
2874            self.defaultTokenAvailableBalance = defaultTokenAvailableBalance
2875            self.health = health
2876        }
2877    }
2878
2879    /* --- PUBLIC METHODS ---- */
2880    /// Returns a health value computed from the provided effective collateral and debt values where health is a ratio
2881    /// of effective collateral over effective debt
2882    access(all) view fun healthComputation(effectiveCollateral: UFix128, effectiveDebt: UFix128): UFix128 {
2883        if effectiveDebt == 0.0 as UFix128 {
2884            // Handles X/0 (infinite) including 0/0 (safe empty position)
2885            return UFix128.max
2886        } else if effectiveCollateral == 0.0 as UFix128 {
2887            // 0/Y where Y > 0 is 0 health (unsafe)
2888            return 0.0 as UFix128
2889        } else if FlowCreditMarketMath.div(effectiveDebt, effectiveCollateral) == 0.0 as UFix128 {
2890            // Negligible debt relative to collateral: treat as infinite
2891            return UFix128.max
2892        } else {
2893            return FlowCreditMarketMath.div(effectiveCollateral, effectiveDebt)
2894        }
2895    }
2896
2897    // Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point
2898    // number with 18 decimal places). The input to this function will be just the relative annual interest rate
2899    // (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001).
2900    access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 {
2901        let secondsInYear: UFix128 = 31_536_000.0 as UFix128
2902        let perSecondScaledValue = FlowCreditMarketMath.div(yearlyRate, secondsInYear)
2903        assert(perSecondScaledValue < UFix128.max, message: "Per-second interest rate \(perSecondScaledValue) is too high")
2904        return perSecondScaledValue + FlowCreditMarketMath.one
2905    }
2906
2907    /// Returns the compounded interest index reflecting the passage of time
2908    /// The result is: newIndex = oldIndex * perSecondRate ^ seconds
2909    access(all) view fun compoundInterestIndex(oldIndex: UFix128, perSecondRate: UFix128, elapsedSeconds: UFix64): UFix128 {
2910        // Exponentiation by squaring on UFix128 for performance and precision
2911        let pow = FlowCreditMarketMath.powUFix128(perSecondRate, elapsedSeconds)
2912        return oldIndex * pow
2913    }
2914
2915    /// Transforms the provided `scaledBalance` to a true balance (or actual balance) where the true balance is the
2916    /// scaledBalance + accrued interest and the scaled balance is the amount a borrower has actually interacted with
2917    /// (via deposits or withdrawals)
2918    access(all) view fun scaledBalanceToTrueBalance(_ scaled: UFix128, interestIndex: UFix128): UFix128 {
2919        // The interest index is a fixed point number with 18 decimal places. To maintain precision,
2920        // we multiply the scaled balance by the interest index and then divide by 10^18 to get the
2921        // true balance with proper decimal alignment.
2922        return FlowCreditMarketMath.div(scaled * interestIndex, FlowCreditMarketMath.one)
2923    }
2924
2925    /// Transforms the provided `trueBalance` to a scaled balance where the scaled balance is the amount a borrower has
2926    /// actually interacted with (via deposits or withdrawals) and the true balance is the amount with respect to
2927    /// accrued interest
2928    access(all) view fun trueBalanceToScaledBalance(_ trueBalance: UFix128, interestIndex: UFix128): UFix128 {
2929        // The interest index is a fixed point number with 18 decimal places. To maintain precision,
2930        // we multiply the true balance by 10^18 and then divide by the interest index to get the
2931        // scaled balance with proper decimal alignment.
2932        return FlowCreditMarketMath.div(trueBalance * FlowCreditMarketMath.one, interestIndex)
2933    }
2934
2935    /* --- INTERNAL METHODS --- */
2936
2937    /// Returns a reference to the contract account's MOET Minter resource
2938    access(self) view fun _borrowMOETMinter(): &MOET.Minter {
2939        return self.account.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath)
2940            ?? panic("Could not borrow reference to internal MOET Minter resource")
2941    }
2942
2943    init() {
2944        self.PoolStoragePath = StoragePath(identifier: "flowCreditMarketPool_\(self.account.address)")!
2945        self.PoolFactoryPath = StoragePath(identifier: "flowCreditMarketPoolFactory_\(self.account.address)")!
2946        self.PoolPublicPath = PublicPath(identifier: "flowCreditMarketPool_\(self.account.address)")!
2947        self.PoolCapStoragePath = StoragePath(identifier: "flowCreditMarketPoolCap_\(self.account.address)")!
2948
2949        // save PoolFactory in storage
2950        self.account.storage.save(
2951            <-create PoolFactory(),
2952            to: self.PoolFactoryPath
2953        )
2954        let factory = self.account.storage.borrow<&PoolFactory>(from: self.PoolFactoryPath)!
2955    }
2956
2957    access(all) resource LiquidationResult: Burner.Burnable {
2958        access(all) var seized: @{FungibleToken.Vault}?
2959        access(all) var remainder: @{FungibleToken.Vault}?
2960
2961        init(seized: @{FungibleToken.Vault}, remainder: @{FungibleToken.Vault}) {
2962            self.seized <- seized
2963            self.remainder <- remainder
2964        }
2965
2966        access(all) fun takeSeized(): @{FungibleToken.Vault} {
2967            let s <- self.seized <- nil
2968            return <- s!
2969        }
2970
2971        access(all) fun takeRemainder(): @{FungibleToken.Vault} {
2972            let r <- self.remainder <- nil
2973            return <- r!
2974        }
2975
2976        access(contract) fun burnCallback() {
2977            let s <- self.seized <- nil
2978            let r <- self.remainder <- nil
2979            if s != nil {
2980                Burner.burn(<-s)
2981            } else {
2982                destroy s
2983            }
2984            if r != nil {
2985                Burner.burn(<-r)
2986            } else {
2987                destroy r
2988            }
2989        }
2990    }
2991
2992    // (contract-level helpers removed; resource-scoped versions live in Pool)
2993}
2994