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