Smart Contract
FlowCreditMarket
A.6b00ff876c299c61.FlowCreditMarket
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