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