From 03db5cee00a8e7b3e2878e964977bd28da16837b Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 10:19:30 -0700 Subject: [PATCH 01/19] WIP: In-progress notes for BalanceSheet/HealthStatement refactor Co-Authored-By: Claude Opus 4.6 --- FlowActions | 2 +- cadence/contracts/FlowALPHealth.cdc | 43 ++++++++++++++++++++--------- cadence/contracts/FlowALPModels.cdc | 32 +++++++++++++++++---- cadence/lib/FlowALPMath.cdc | 9 ++++++ 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/FlowActions b/FlowActions index 9788d6ee..6769d4c9 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 9788d6ee9f71e29d19643960ccb86738751065c4 +Subproject commit 6769d4c9f9ded4a5b4404d8c982300e84ccef532 diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index ee50c34d..567b5cd2 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -21,13 +21,13 @@ access(all) contract FlowALPHealth { /// @return A new BalanceSheet reflecting the effective collateral and debt after the withdrawal access(account) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: FlowALPModels.BalanceSheet, - withdrawBalance: FlowALPModels.InternalBalance?, + withdrawBalance: FlowALPModels.InternalBalance?, // make this non-optional withdrawAmount: UFix64, withdrawPrice: UFix128, - withdrawBorrowFactor: UFix128, + withdrawBorrowFactor: UFix128, // pass tokenstate instead withdrawCollateralFactor: UFix128, withdrawCreditInterestIndex: UFix128, - isDebugLogging: Bool + tokenState: &FlowALPModels.TokenSnapshot, ): FlowALPModels.BalanceSheet { var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt @@ -38,14 +38,31 @@ access(all) contract FlowALPHealth { effectiveDebt: effectiveDebtAfterWithdrawal ) } - if isDebugLogging { - log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") - log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") - } + + // Find the effective collateral/debt attributed to the withdrawal token's balance, before the withdrawal occurs. + // Have this already with new BalanceSheet defn + + + // simple cases are decreasing credit to >0, or increasing existing debt (just add to eff debt or sub from eff coll) + // complex case is converting credit to debt. Here we should decompose problem into two withdrawals (one decreasing credit, one increasing debt) + // + // or alternatively: + // - memorize eff debt/credit contribution before withdrawal (B) + // - then compute eff debt/credit contribution after withdrawal (A) + // subtract B, then add + + withdrawBalance!.recordWithdrawal(withdrawAmount, tokenState) + // get true balance + // get effective collateral/debt + // set new value in the per-token map (BalanceSheet) + // re-compute total effective col/debt -> health + // return + + // if credit: + // overwite withdrawType: withdrawBalance. + let withdrawAmountU = UFix128(withdrawAmount) - let withdrawPrice2 = withdrawPrice - let withdrawBorrowFactor2 = withdrawBorrowFactor let balance = withdrawBalance let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 @@ -55,7 +72,7 @@ access(all) contract FlowALPHealth { // If the position doesn't have any collateral for the withdrawn token, // we can just compute how much additional effective debt the withdrawal will create. effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - (withdrawAmountU * withdrawPrice2) / withdrawBorrowFactor2 + (withdrawAmountU * withdrawPrice) / withdrawBorrowFactor case FlowALPModels.BalanceDirection.Credit: // The user has a collateral position in the given token, we need to figure out if this withdrawal @@ -69,13 +86,13 @@ access(all) contract FlowALPHealth { // This withdrawal will draw down collateral, but won't create debt, we just need to account // for the collateral decrease. effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (withdrawAmountU * withdrawPrice2) * collateralFactor + (withdrawAmountU * withdrawPrice) * collateralFactor } else { // The withdrawal will wipe out all of the collateral, and create some debt. effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - ((withdrawAmountU - trueCollateral) * withdrawPrice2) / withdrawBorrowFactor2 + ((withdrawAmountU - trueCollateral) * withdrawPrice) / withdrawBorrowFactor effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (trueCollateral * withdrawPrice2) * collateralFactor + (trueCollateral * withdrawPrice) * collateralFactor } } diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index a784b6a2..527156d1 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -72,6 +72,18 @@ access(all) contract FlowALPModels { access(all) case Debit } + access(all) struct SignedQuantity { + /// The direction (sign) of this quantity. + access(all) let direction: BalanceDirection + /// The unsigned numeric value. + access(all) let quantity: UFix128 + + init(direction: BalanceDirection, quantity: UFix128) { + self.direction = direction + self.quantity = quantity + } + } + /// InternalBalance /// /// A structure used internally to track a position's balance for a particular token @@ -430,12 +442,18 @@ access(all) contract FlowALPModels { ) } + // MAYBE: HealthStatement: derived from balance sheet, only total eff debt/coll + health + /// BalanceSheet /// /// A struct containing a position's overview in terms of its effective collateral and debt /// as well as its current health. access(all) struct BalanceSheet { + access(self) let effectiveCollateralByToken: {Type: UFix128} + + access(self) let effectiveDebtByToken: {Type: UFix128} + /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $. /// In combination with effective debt, this determines how much additional debt can be taken out by this position. access(all) let effectiveCollateral: UFix128 @@ -448,14 +466,16 @@ access(all) contract FlowALPModels { access(all) let health: UFix128 init( - effectiveCollateral: UFix128, - effectiveDebt: UFix128 + effectiveCollateral: {Type: UFix128}, + effectiveDebt: {Type: UFix128} ) { - self.effectiveCollateral = effectiveCollateral - self.effectiveDebt = effectiveDebt + self.effectiveCollateralByToken = effectiveCollateral + self.effectiveCollateral = FlowALPMath.sumUFix128(effectiveCollateral.values) + self.effectiveDebtByToken = effectiveDebt + self.effectiveDebt = FlowALPMath.sumUFix128(effectiveDebt) self.health = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt + effectiveCollateral: self.effectiveCollateral, + effectiveDebt: self.effectiveDebt ) } } diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 1a753d89..56369085 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -38,6 +38,15 @@ access(all) contract FlowALPMath { return result } + /// Returns the sum of a list of UFix128-typed numeric values. + access(all) view fun sumUFix128(_ nums: [UFix128]): UFix128 { + var sum: UFix128 = 0.0 + for num in nums { + sum = sum + num + } + return sum + } + access(all) view fun toUFix64(_ value: UFix128, rounding: RoundingMode): UFix64 { let truncated = UFix64(value) let truncatedAs128 = UFix128(truncated) From 08880a887025a8197f2c37a02ca27c7d115001ca Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 10:51:21 -0700 Subject: [PATCH 02/19] Add HealthStatement, per-token BalanceSheet maps, and withUpdatedContributions - Add HealthStatement struct for aggregate-only health summaries - Refactor BalanceSheet to store per-token effective collateral/debt maps - Add withUpdatedContributions method for immutable per-token updates - Fix sumUFix128 bug (missing .values on effectiveDebt map) - Update _getUpdatedBalanceSheet to build per-token maps - Update FlowALPHealth adjustment functions to use withUpdatedContributions - Bundle effectiveCollateral/effectiveDebt params into HealthStatement for computeRequiredDepositForHealth and computeAvailableWithdrawal Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 253 ++++++++-------------------- cadence/contracts/FlowALPModels.cdc | 73 +++++++- cadence/contracts/FlowALPv0.cdc | 58 +++---- 3 files changed, 158 insertions(+), 226 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 567b5cd2..b8cb3ea5 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -10,95 +10,71 @@ access(all) contract FlowALPHealth { /// in the withdrawn token. If the position has collateral in the token, the withdrawal may /// either draw down collateral, or exhaust it entirely and create new debt. /// - /// @param balanceSheet: The position's current effective collateral and debt + /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any + /// @param withdrawType: The type of token being withdrawn /// @param withdrawAmount: The amount of tokens to withdraw /// @param withdrawPrice: The oracle price of the withdrawn token /// @param withdrawBorrowFactor: The borrow factor applied to debt in the withdrawn token /// @param withdrawCollateralFactor: The collateral factor applied to collateral in the withdrawn token /// @param withdrawCreditInterestIndex: The credit interest index for the withdrawn token - /// @param isDebugLogging: Whether to emit debug log messages /// @return A new BalanceSheet reflecting the effective collateral and debt after the withdrawal access(account) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: FlowALPModels.BalanceSheet, - withdrawBalance: FlowALPModels.InternalBalance?, // make this non-optional + withdrawBalance: FlowALPModels.InternalBalance?, + withdrawType: Type, withdrawAmount: UFix64, withdrawPrice: UFix128, - withdrawBorrowFactor: UFix128, // pass tokenstate instead + withdrawBorrowFactor: UFix128, withdrawCollateralFactor: UFix128, - withdrawCreditInterestIndex: UFix128, - tokenState: &FlowALPModels.TokenSnapshot, + withdrawCreditInterestIndex: UFix128 ): FlowALPModels.BalanceSheet { - var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt - if withdrawAmount == 0.0 { - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal - ) + return balanceSheet } - // Find the effective collateral/debt attributed to the withdrawal token's balance, before the withdrawal occurs. - // Have this already with new BalanceSheet defn - - - // simple cases are decreasing credit to >0, or increasing existing debt (just add to eff debt or sub from eff coll) - // complex case is converting credit to debt. Here we should decompose problem into two withdrawals (one decreasing credit, one increasing debt) - // - // or alternatively: - // - memorize eff debt/credit contribution before withdrawal (B) - // - then compute eff debt/credit contribution after withdrawal (A) - // subtract B, then add - - withdrawBalance!.recordWithdrawal(withdrawAmount, tokenState) - // get true balance - // get effective collateral/debt - // set new value in the per-token map (BalanceSheet) - // re-compute total effective col/debt -> health - // return - - // if credit: - // overwite withdrawType: withdrawBalance. - - let withdrawAmountU = UFix128(withdrawAmount) let balance = withdrawBalance let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 + // Compute the new per-token effective collateral and debt after the withdrawal. + var newEffectiveCollateral: UFix128? = balanceSheet.effectiveCollateralByToken[withdrawType] + var newEffectiveDebt: UFix128? = balanceSheet.effectiveDebtByToken[withdrawType] + switch direction { case FlowALPModels.BalanceDirection.Debit: - // If the position doesn't have any collateral for the withdrawn token, - // we can just compute how much additional effective debt the withdrawal will create. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - (withdrawAmountU * withdrawPrice) / withdrawBorrowFactor + // No collateral for the withdrawn token — the withdrawal creates additional debt. + let additionalDebt = (withdrawAmountU * withdrawPrice) / withdrawBorrowFactor + newEffectiveDebt = (newEffectiveDebt ?? 0.0) + additionalDebt case FlowALPModels.BalanceDirection.Credit: - // The user has a collateral position in the given token, we need to figure out if this withdrawal - // will flip over into debt, or just draw down the collateral. + // The user has a collateral position in the given token. let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, interestIndex: withdrawCreditInterestIndex ) - let collateralFactor = withdrawCollateralFactor if trueCollateral >= withdrawAmountU { - // This withdrawal will draw down collateral, but won't create debt, we just need to account - // for the collateral decrease. - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (withdrawAmountU * withdrawPrice) * collateralFactor + // Withdrawal draws down collateral without creating debt. + let collateralDecrease = (withdrawAmountU * withdrawPrice) * withdrawCollateralFactor + newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) - collateralDecrease } else { - // The withdrawal will wipe out all of the collateral, and create some debt. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - ((withdrawAmountU - trueCollateral) * withdrawPrice) / withdrawBorrowFactor - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (trueCollateral * withdrawPrice) * collateralFactor + // Withdrawal wipes out all collateral and creates some debt. + let existingCollateral = (trueCollateral * withdrawPrice) * withdrawCollateralFactor + newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) - existingCollateral + let additionalDebt = ((withdrawAmountU - trueCollateral) * withdrawPrice) / withdrawBorrowFactor + newEffectiveDebt = (newEffectiveDebt ?? 0.0) + additionalDebt } } - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal + // Clean up zero/nil entries + if newEffectiveCollateral == 0.0 { newEffectiveCollateral = nil } + if newEffectiveDebt == 0.0 { newEffectiveDebt = nil } + + return balanceSheet.withUpdatedContributions( + tokenType: withdrawType, + effectiveCollateral: newEffectiveCollateral, + effectiveDebt: newEffectiveDebt ) } @@ -114,43 +90,33 @@ access(all) contract FlowALPHealth { /// @param depositPrice: The oracle price of the deposit token /// @param depositBorrowFactor: The borrow factor applied to debt in the deposit token /// @param depositCollateralFactor: The collateral factor applied to collateral in the deposit token - /// @param effectiveCollateral: The position's current effective collateral (post any prior withdrawal) - /// @param effectiveDebt: The position's current effective debt (post any prior withdrawal) + /// @param adjusted: The position's current health statement (post any prior withdrawal) /// @param targetHealth: The target health ratio to achieve /// @param isDebugLogging: Whether to emit debug log messages /// @return The amount of tokens (in UFix64) required to reach the target health - // TODO(jord): ~100-line function - consider refactoring access(account) fun computeRequiredDepositForHealth( depositBalance: FlowALPModels.InternalBalance?, depositDebitInterestIndex: UFix128, depositPrice: UFix128, depositBorrowFactor: UFix128, depositCollateralFactor: UFix128, - effectiveCollateral: UFix128, - effectiveDebt: UFix128, + adjusted: FlowALPModels.HealthStatement, targetHealth: UFix128, isDebugLogging: Bool ): UFix64 { - let effectiveCollateralAfterWithdrawal = effectiveCollateral - var effectiveDebtAfterWithdrawal = effectiveDebt + let effectiveCollateralAfterWithdrawal = adjusted.effectiveCollateral + var effectiveDebtAfterWithdrawal = adjusted.effectiveDebt if isDebugLogging { log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } - // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) - // Now we can figure out how many of the given token would need to be deposited to bring the position - // to the target health value. - var healthAfterWithdrawal = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal - ) + var healthAfterWithdrawal = adjusted.health if isDebugLogging { log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") } if healthAfterWithdrawal >= targetHealth { - // The position is already at or above the target health, so we don't need to deposit anything. return 0.0 } @@ -159,8 +125,6 @@ access(all) contract FlowALPHealth { var debtTokenCount: UFix128 = 0.0 let maybeBalance = depositBalance if maybeBalance?.direction == FlowALPModels.BalanceDirection.Debit { - // The user has a debt position in the given token, we start by looking at the health impact of paying off - // the entire debt. let debtBalance = maybeBalance!.scaledBalance let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( debtBalance, @@ -168,42 +132,26 @@ access(all) contract FlowALPHealth { ) let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor - // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal, - // it means we can pay off all debt var effectiveDebtAfterPayment: UFix128 = 0.0 if debtEffectiveValue <= effectiveDebtAfterWithdrawal { effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue } - // Check what the new health would be if we paid off all of this debt let potentialHealth = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterPayment ) - // Does paying off all of the debt reach the target health? Then we're done. if potentialHealth >= targetHealth { - // We can reach the target health by paying off some or all of the debt. We can easily - // compute how many units of the token would be needed to reach the target health. - let healthChange = targetHealth - healthAfterWithdrawal let requiredEffectiveDebt = effectiveDebtAfterWithdrawal - (effectiveCollateralAfterWithdrawal / targetHealth) - - // The amount of the token to pay back, in units of the token. let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice if isDebugLogging { log(" [CONTRACT] paybackAmount: \(paybackAmount)") } - return FlowALPMath.toUFix64RoundUp(paybackAmount) } else { - // We can pay off the entire debt, but we still need to deposit more to reach the target health. - // We have logic below that can determine the collateral deposition required to reach the target health - // from this new health position. Rather than copy that logic here, we fall through into it. But first - // we have to record the amount of tokens that went towards debt payback and adjust the effective - // debt to reflect that it has been paid off. debtTokenCount = trueDebtTokenCount - // Ensure we don't underflow if debtEffectiveValue <= effectiveDebtAfterWithdrawal { effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue } else { @@ -213,18 +161,9 @@ access(all) contract FlowALPHealth { } } - // At this point, we're either dealing with a position that didn't have a debt position in the deposit - // token, or we've accounted for the debt payoff and adjusted the effective debt above. - // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the - // target health. We can rearrange the health equation to solve for the required collateral: - - // We need to increase the effective collateral from its current value to the required value, so we - // multiply the required health change by the effective debt, and turn that into a token amount. let healthChangeU = targetHealth - healthAfterWithdrawal - // TODO: apply the same logic as below to the early return blocks above let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor - // The amount of the token to deposit, in units of the token. let collateralTokenCount = requiredEffectiveCollateral / depositPrice if isDebugLogging { log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") @@ -233,7 +172,6 @@ access(all) contract FlowALPHealth { log(" [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)") } - // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) } @@ -244,18 +182,19 @@ access(all) contract FlowALPHealth { /// in the deposited token. If the position has debt in the token, the deposit first pays /// down debt before accumulating as collateral. /// - /// @param balanceSheet: The position's current effective collateral and debt + /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param depositBalance: The position's existing balance for the deposited token, if any + /// @param depositType: The type of token being deposited /// @param depositAmount: The amount of tokens to deposit /// @param depositPrice: The oracle price of the deposited token /// @param depositBorrowFactor: The borrow factor applied to debt in the deposited token /// @param depositCollateralFactor: The collateral factor applied to collateral in the deposited token /// @param depositDebitInterestIndex: The debit interest index for the deposited token - /// @param isDebugLogging: Whether to emit debug log messages /// @return A new BalanceSheet reflecting the effective collateral and debt after the deposit access(account) fun computeAdjustedBalancesAfterDeposit( balanceSheet: FlowALPModels.BalanceSheet, depositBalance: FlowALPModels.InternalBalance?, + depositType: Type, depositAmount: UFix64, depositPrice: UFix128, depositBorrowFactor: UFix128, @@ -263,38 +202,27 @@ access(all) contract FlowALPHealth { depositDebitInterestIndex: UFix128, isDebugLogging: Bool ): FlowALPModels.BalanceSheet { - var effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral - var effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - if isDebugLogging { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") - } - if depositAmount == 0.0 { - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit - ) + return balanceSheet } - let depositAmountCasted = UFix128(depositAmount) - let depositPriceCasted = depositPrice - let depositBorrowFactorCasted = depositBorrowFactor - let depositCollateralFactorCasted = depositCollateralFactor + let depositAmountU = UFix128(depositAmount) let balance = depositBalance let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 + // Compute the new per-token effective collateral and debt after the deposit. + var newEffectiveCollateral: UFix128? = balanceSheet.effectiveCollateralByToken[depositType] + var newEffectiveDebt: UFix128? = balanceSheet.effectiveDebtByToken[depositType] + switch direction { case FlowALPModels.BalanceDirection.Credit: - // If there's no debt for the deposit token, - // we can just compute how much additional effective collateral the deposit will create. - effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - (depositAmountCasted * depositPriceCasted) * depositCollateralFactorCasted + // No debt for the deposit token — the deposit creates additional collateral. + let additionalCollateral = (depositAmountU * depositPrice) * depositCollateralFactor + newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) + additionalCollateral case FlowALPModels.BalanceDirection.Debit: - // The user has a debt position in the given token, we need to figure out if this deposit - // will result in net collateral, or just bring down the debt. + // The user has a debt position in the given token. let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, interestIndex: depositDebitInterestIndex @@ -303,89 +231,71 @@ access(all) contract FlowALPHealth { log(" [CONTRACT] trueDebt: \(trueDebt)") } - if trueDebt >= depositAmountCasted { - // This deposit will pay down some debt, but won't result in net collateral, we - // just need to account for the debt decrease. - // TODO - validate if this should deal with withdrawType or depositType - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (depositAmountCasted * depositPriceCasted) / depositBorrowFactorCasted + if trueDebt >= depositAmountU { + // Deposit pays down some debt without creating collateral. + let debtDecrease = (depositAmountU * depositPrice) / depositBorrowFactor + newEffectiveDebt = (newEffectiveDebt ?? 0.0) - debtDecrease } else { - // The deposit will wipe out all of the debt, and create some collateral. - // TODO - validate if this should deal with withdrawType or depositType - effectiveDebtAfterDeposit = balanceSheet.effectiveDebt - - (trueDebt * depositPriceCasted) / depositBorrowFactorCasted - effectiveCollateralAfterDeposit = balanceSheet.effectiveCollateral + - (depositAmountCasted - trueDebt) * depositPriceCasted * depositCollateralFactorCasted + // Deposit wipes out all debt and creates some collateral. + let existingDebt = (trueDebt * depositPrice) / depositBorrowFactor + newEffectiveDebt = (newEffectiveDebt ?? 0.0) - existingDebt + let additionalCollateral = ((depositAmountU - trueDebt) * depositPrice) * depositCollateralFactor + newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) + additionalCollateral } } if isDebugLogging { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") + log(" [CONTRACT] effectiveCollateralAfterDeposit: \(newEffectiveCollateral ?? 0.0)") + log(" [CONTRACT] effectiveDebtAfterDeposit: \(newEffectiveDebt ?? 0.0)") } - // We now have new effective collateral and debt values that reflect the proposed deposit (if any!). - // Now we can figure out how many of the withdrawal token are available while keeping the position - // at or above the target health value. - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit + // Clean up zero/nil entries + if newEffectiveCollateral == 0.0 { newEffectiveCollateral = nil } + if newEffectiveDebt == 0.0 { newEffectiveDebt = nil } + + return balanceSheet.withUpdatedContributions( + tokenType: depositType, + effectiveCollateral: newEffectiveCollateral, + effectiveDebt: newEffectiveDebt ) } /// Computes the maximum amount of a given token that can be withdrawn while maintaining a target health. /// - /// This function determines how many tokens are available for withdrawal, accounting for - /// whether the position holds a credit (collateral) balance in the withdrawn token. If the - /// position has collateral, the withdrawal may draw down collateral only, or exhaust it and - /// create new debt. The function finds the maximum withdrawal that keeps health at or above - /// the target. - /// /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any /// @param withdrawCreditInterestIndex: The credit interest index for the withdrawn token /// @param withdrawPrice: The oracle price of the withdrawn token /// @param withdrawCollateralFactor: The collateral factor applied to collateral in the withdrawn token /// @param withdrawBorrowFactor: The borrow factor applied to debt in the withdrawn token - /// @param effectiveCollateral: The position's current effective collateral (post any prior deposit) - /// @param effectiveDebt: The position's current effective debt (post any prior deposit) + /// @param adjusted: The position's current health statement (post any prior deposit) /// @param targetHealth: The minimum health ratio to maintain /// @param isDebugLogging: Whether to emit debug log messages /// @return The maximum amount of tokens (in UFix64) that can be withdrawn - // TODO(jord): ~100-line function - consider refactoring access(account) fun computeAvailableWithdrawal( withdrawBalance: FlowALPModels.InternalBalance?, withdrawCreditInterestIndex: UFix128, withdrawPrice: UFix128, withdrawCollateralFactor: UFix128, withdrawBorrowFactor: UFix128, - effectiveCollateral: UFix128, - effectiveDebt: UFix128, + adjusted: FlowALPModels.HealthStatement, targetHealth: UFix128, isDebugLogging: Bool ): UFix64 { - var effectiveCollateralAfterDeposit = effectiveCollateral - let effectiveDebtAfterDeposit = effectiveDebt + var effectiveCollateralAfterDeposit = adjusted.effectiveCollateral + let effectiveDebtAfterDeposit = adjusted.effectiveDebt - let healthAfterDeposit = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit - ) + let healthAfterDeposit = adjusted.health if isDebugLogging { log(" [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") } if healthAfterDeposit <= targetHealth { - // The position is already at or below the provided target health, so we can't withdraw anything. return 0.0 } - // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep - // track of the number of tokens that are available from collateral var collateralTokenCount: UFix128 = 0.0 let maybeBalance = withdrawBalance if maybeBalance?.direction == FlowALPModels.BalanceDirection.Credit { - // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all - // of that collateral let creditBalance = maybeBalance!.scaledBalance let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( creditBalance, @@ -393,24 +303,17 @@ access(all) contract FlowALPHealth { ) let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor - // Check what the new health would be if we took out all of this collateral let potentialHealth = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? + effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, effectiveDebt: effectiveDebtAfterDeposit ) - // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. if potentialHealth <= targetHealth { - // We will hit the health target before using up all of the withdraw token credit. We can easily - // compute how many units of the token would bring the position down to the target health. - // We will hit the health target before using up all available withdraw credit. - let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) if isDebugLogging { log(" [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") } - // The amount of the token we can take using that amount of health let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice if isDebugLogging { log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") @@ -418,9 +321,6 @@ access(all) contract FlowALPHealth { return FlowALPMath.toUFix64RoundDown(availableTokenCount) } else { - // We can flip this credit position into a debit position, before hitting the target health. - // We have logic below that can determine health changes for debit positions. We've copied it here - // with an added handling for the case where the health after deposit is an edgecase collateralTokenCount = trueCredit effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue if isDebugLogging { @@ -428,7 +328,6 @@ access(all) contract FlowALPHealth { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") } - // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice if isDebugLogging { @@ -441,10 +340,6 @@ access(all) contract FlowALPHealth { } } - // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw - // token, or we've accounted for the credit balance and adjusted the effective collateral above. - - // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice if isDebugLogging { diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 527156d1..86304f64 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -442,7 +442,25 @@ access(all) contract FlowALPModels { ) } - // MAYBE: HealthStatement: derived from balance sheet, only total eff debt/coll + health + /// HealthStatement + /// + /// A lightweight summary of a position's health, containing only aggregate totals. + /// Use this when you only need total effective collateral/debt and health, + /// without per-token breakdowns. + access(all) struct HealthStatement { + access(all) let effectiveCollateral: UFix128 + access(all) let effectiveDebt: UFix128 + access(all) let health: UFix128 + + init(effectiveCollateral: UFix128, effectiveDebt: UFix128) { + self.effectiveCollateral = effectiveCollateral + self.effectiveDebt = effectiveDebt + self.health = FlowALPMath.healthComputation( + effectiveCollateral: effectiveCollateral, + effectiveDebt: effectiveDebt + ) + } + } /// BalanceSheet /// @@ -450,9 +468,12 @@ access(all) contract FlowALPModels { /// as well as its current health. access(all) struct BalanceSheet { - access(self) let effectiveCollateralByToken: {Type: UFix128} + access(all) let effectiveCollateralByToken: {Type: UFix128} - access(self) let effectiveDebtByToken: {Type: UFix128} + access(all) let effectiveDebtByToken: {Type: UFix128} + + /// Aggregate summary of the balance sheet (totals + health). + access(all) let summary: HealthStatement /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $. /// In combination with effective debt, this determines how much additional debt can be taken out by this position. @@ -470,12 +491,48 @@ access(all) contract FlowALPModels { effectiveDebt: {Type: UFix128} ) { self.effectiveCollateralByToken = effectiveCollateral - self.effectiveCollateral = FlowALPMath.sumUFix128(effectiveCollateral.values) self.effectiveDebtByToken = effectiveDebt - self.effectiveDebt = FlowALPMath.sumUFix128(effectiveDebt) - self.health = FlowALPMath.healthComputation( - effectiveCollateral: self.effectiveCollateral, - effectiveDebt: self.effectiveDebt + self.summary = HealthStatement( + effectiveCollateral: FlowALPMath.sumUFix128(effectiveCollateral.values), + effectiveDebt: FlowALPMath.sumUFix128(effectiveDebt.values) + ) + self.effectiveCollateral = self.summary.effectiveCollateral + self.effectiveDebt = self.summary.effectiveDebt + self.health = self.summary.health + } + + /// Returns the per-token effective collateral map. + access(all) view fun getEffectiveCollateralByToken(): {Type: UFix128} { + return self.effectiveCollateralByToken + } + + /// Returns the per-token effective debt map. + access(all) view fun getEffectiveDebtByToken(): {Type: UFix128} { + return self.effectiveDebtByToken + } + + /// Returns a new BalanceSheet with one token's contributions replaced. + /// Pass nil to remove a token's entry from the corresponding map. + access(all) fun withUpdatedContributions( + tokenType: Type, + effectiveCollateral: UFix128?, + effectiveDebt: UFix128? + ): BalanceSheet { + let newCollateral = self.effectiveCollateralByToken + let newDebt = self.effectiveDebtByToken + if let coll = effectiveCollateral { + newCollateral[tokenType] = coll + } else { + newCollateral.remove(key: tokenType) + } + if let debt = effectiveDebt { + newDebt[tokenType] = debt + } else { + newDebt.remove(key: tokenType) + } + return BalanceSheet( + effectiveCollateral: newCollateral, + effectiveDebt: newDebt ) } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 5051d079..84df5a5d 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -688,13 +688,11 @@ access(all) contract FlowALPv0 { position: position, depositType: depositType, withdrawType: withdrawType, - effectiveCollateral: adjusted.effectiveCollateral, - effectiveDebt: adjusted.effectiveDebt, + adjusted: adjusted.summary, targetHealth: targetHealth ) } - // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, @@ -710,23 +708,20 @@ access(all) contract FlowALPv0 { return FlowALPHealth.computeAdjustedBalancesAfterWithdrawal( balanceSheet: balanceSheet, withdrawBalance: balance, + withdrawType: withdrawType, withdrawAmount: withdrawAmount, withdrawPrice: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), withdrawCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), - withdrawCreditInterestIndex: withdrawCreditInterestIndex, - isDebugLogging: self.config.isDebugLogging() + withdrawCreditInterestIndex: withdrawCreditInterestIndex ) } - // TODO(jord): ~100-line function - consider refactoring - // TODO: documentation - access(self) fun computeRequiredDepositForHealth( + access(self) fun computeRequiredDepositForHealth( position: &{FlowALPModels.InternalPosition}, depositType: Type, withdrawType: Type, - effectiveCollateral: UFix128, - effectiveDebt: UFix128, + adjusted: FlowALPModels.HealthStatement, targetHealth: UFix128 ): UFix64 { let depositBalance = position.getBalance(depositType) @@ -741,8 +736,7 @@ access(all) contract FlowALPv0 { depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt, + adjusted: adjusted, targetHealth: targetHealth, isDebugLogging: self.config.isDebugLogging() ) @@ -797,13 +791,11 @@ access(all) contract FlowALPv0 { return self.computeAvailableWithdrawal( position: position, withdrawType: withdrawType, - effectiveCollateral: adjusted.effectiveCollateral, - effectiveDebt: adjusted.effectiveDebt, + adjusted: adjusted.summary, targetHealth: targetHealth ) } - // Helper function to compute balances after deposit access(self) fun computeAdjustedBalancesAfterDeposit( balanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, @@ -819,6 +811,7 @@ access(all) contract FlowALPv0 { return FlowALPHealth.computeAdjustedBalancesAfterDeposit( balanceSheet: balanceSheet, depositBalance: depositBalance, + depositType: depositType, depositAmount: depositAmount, depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), @@ -828,13 +821,10 @@ access(all) contract FlowALPv0 { ) } - // Helper function to compute available withdrawal - // TODO(jord): ~100-line function - consider refactoring access(self) fun computeAvailableWithdrawal( position: &{FlowALPModels.InternalPosition}, withdrawType: Type, - effectiveCollateral: UFix128, - effectiveDebt: UFix128, + adjusted: FlowALPModels.HealthStatement, targetHealth: UFix128 ): UFix64 { let withdrawBalance = position.getBalance(withdrawType) @@ -849,8 +839,7 @@ access(all) contract FlowALPv0 { withdrawPrice: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), withdrawCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt, + adjusted: adjusted, targetHealth: targetHealth, isDebugLogging: self.config.isDebugLogging() ) @@ -1998,13 +1987,13 @@ access(all) contract FlowALPv0 { access(self) fun _getUpdatedBalanceSheet(pid: UInt64): FlowALPModels.BalanceSheet { let position = self._borrowPosition(pid: pid) - // Get the position's collateral and debt values in terms of the default token. - var effectiveCollateral: UFix128 = 0.0 - var effectiveDebt: UFix128 = 0.0 + var effectiveCollateralByToken: {Type: UFix128} = {} + var effectiveDebtByToken: {Type: UFix128} = {} for type in position.getBalanceKeys() { let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) + let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) switch balance.direction { case FlowALPModels.BalanceDirection.Credit: @@ -2012,31 +2001,22 @@ access(all) contract FlowALPv0 { balance.scaledBalance, interestIndex: tokenState.getCreditInterestIndex() ) - - let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - let value = convertedPrice * trueBalance - - let convertedCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) - effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor) + let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) + effectiveCollateralByToken[type] = (price * trueBalance) * collateralFactor case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( balance.scaledBalance, interestIndex: tokenState.getDebitInterestIndex() ) - - let convertedPrice = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - let value = convertedPrice * trueBalance - - let convertedBorrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) - effectiveDebt = effectiveDebt + (value / convertedBorrowFactor) - + let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) + effectiveDebtByToken[type] = (price * trueBalance) / borrowFactor } } return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt + effectiveCollateral: effectiveCollateralByToken, + effectiveDebt: effectiveDebtByToken ) } From 1d4d723c8eaac6a12e6909c8247fa9f99b583646 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 10:57:29 -0700 Subject: [PATCH 03/19] Refactor computeAdjustedBalancesAfterWithdrawal to remove-old/add-new pattern Replace delta-based switch/case logic with a cleaner approach: 1. Compute post-withdrawal true balance via trueBalanceAfterWithdrawal helper 2. Compute new effective contribution from the resulting balance 3. Use withUpdatedContributions to build the new BalanceSheet The wrapper now constructs a TokenSnapshot instead of passing individual price/factor/index parameters. Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 108 +++++++++++++++++----------- cadence/contracts/FlowALPv0.cdc | 22 +++--- 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index b8cb3ea5..003933ff 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -5,77 +5,99 @@ access(all) contract FlowALPHealth { /// Computes adjusted effective collateral and debt after a hypothetical withdrawal. /// - /// This function determines how a withdrawal would affect the position's balance sheet, - /// accounting for whether the position holds a credit (collateral) or debit (debt) balance - /// in the withdrawn token. If the position has collateral in the token, the withdrawal may - /// either draw down collateral, or exhaust it entirely and create new debt. + /// Uses a "remove old contribution, add new contribution" approach: + /// 1. Remove the current per-token effective collateral/debt entry + /// 2. Compute the new true balance after the withdrawal + /// 3. Compute the new effective contribution from the post-withdrawal balance + /// 4. Return a new BalanceSheet with the updated per-token entry /// /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any /// @param withdrawType: The type of token being withdrawn /// @param withdrawAmount: The amount of tokens to withdraw - /// @param withdrawPrice: The oracle price of the withdrawn token - /// @param withdrawBorrowFactor: The borrow factor applied to debt in the withdrawn token - /// @param withdrawCollateralFactor: The collateral factor applied to collateral in the withdrawn token - /// @param withdrawCreditInterestIndex: The credit interest index for the withdrawn token + /// @param tokenSnapshot: Snapshot of the withdrawn token's price, interest indices, and risk params /// @return A new BalanceSheet reflecting the effective collateral and debt after the withdrawal access(account) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: FlowALPModels.BalanceSheet, withdrawBalance: FlowALPModels.InternalBalance?, withdrawType: Type, withdrawAmount: UFix64, - withdrawPrice: UFix128, - withdrawBorrowFactor: UFix128, - withdrawCollateralFactor: UFix128, - withdrawCreditInterestIndex: UFix128 + tokenSnapshot: FlowALPModels.TokenSnapshot ): FlowALPModels.BalanceSheet { if withdrawAmount == 0.0 { return balanceSheet } let withdrawAmountU = UFix128(withdrawAmount) - let balance = withdrawBalance + + // Compute the post-withdrawal true balance and direction. + let after = self.trueBalanceAfterWithdrawal( + balance: withdrawBalance, + withdrawAmount: withdrawAmountU, + tokenSnapshot: tokenSnapshot + ) + + // Compute new per-token effective values from the post-withdrawal true balance. + var newEffectiveCollateral: UFix128? = nil + var newEffectiveDebt: UFix128? = nil + if after.quantity > 0.0 { + switch after.direction { + case FlowALPModels.BalanceDirection.Credit: + newEffectiveCollateral = tokenSnapshot.effectiveCollateral(creditBalance: after.quantity) + case FlowALPModels.BalanceDirection.Debit: + newEffectiveDebt = tokenSnapshot.effectiveDebt(debitBalance: after.quantity) + } + } + + return balanceSheet.withUpdatedContributions( + tokenType: withdrawType, + effectiveCollateral: newEffectiveCollateral, + effectiveDebt: newEffectiveDebt + ) + } + + /// Computes the true balance (direction + amount) after a withdrawal. + /// + /// Starting from the current balance (credit or debit), subtracts the withdrawal amount. + /// If the position has credit, the withdrawal draws it down and may flip into debt. + /// If the position has debt (or no balance), the withdrawal increases debt. + access(self) fun trueBalanceAfterWithdrawal( + balance: FlowALPModels.InternalBalance?, + withdrawAmount: UFix128, + tokenSnapshot: FlowALPModels.TokenSnapshot + ): FlowALPModels.SignedQuantity { let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit let scaledBalance = balance?.scaledBalance ?? 0.0 - // Compute the new per-token effective collateral and debt after the withdrawal. - var newEffectiveCollateral: UFix128? = balanceSheet.effectiveCollateralByToken[withdrawType] - var newEffectiveDebt: UFix128? = balanceSheet.effectiveDebtByToken[withdrawType] - switch direction { case FlowALPModels.BalanceDirection.Debit: - // No collateral for the withdrawn token — the withdrawal creates additional debt. - let additionalDebt = (withdrawAmountU * withdrawPrice) / withdrawBorrowFactor - newEffectiveDebt = (newEffectiveDebt ?? 0.0) + additionalDebt + // Currently in debt — withdrawal adds more debt. + let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, interestIndex: tokenSnapshot.debitIndex + ) + return FlowALPModels.SignedQuantity( + direction: FlowALPModels.BalanceDirection.Debit, + quantity: trueDebt + withdrawAmount + ) case FlowALPModels.BalanceDirection.Credit: - // The user has a collateral position in the given token. - let trueCollateral = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, - interestIndex: withdrawCreditInterestIndex + // Currently has credit — withdrawal draws it down, possibly flipping to debt. + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, interestIndex: tokenSnapshot.creditIndex ) - if trueCollateral >= withdrawAmountU { - // Withdrawal draws down collateral without creating debt. - let collateralDecrease = (withdrawAmountU * withdrawPrice) * withdrawCollateralFactor - newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) - collateralDecrease + if trueCredit >= withdrawAmount { + return FlowALPModels.SignedQuantity( + direction: FlowALPModels.BalanceDirection.Credit, + quantity: trueCredit - withdrawAmount + ) } else { - // Withdrawal wipes out all collateral and creates some debt. - let existingCollateral = (trueCollateral * withdrawPrice) * withdrawCollateralFactor - newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) - existingCollateral - let additionalDebt = ((withdrawAmountU - trueCollateral) * withdrawPrice) / withdrawBorrowFactor - newEffectiveDebt = (newEffectiveDebt ?? 0.0) + additionalDebt + return FlowALPModels.SignedQuantity( + direction: FlowALPModels.BalanceDirection.Debit, + quantity: withdrawAmount - trueCredit + ) } } - - // Clean up zero/nil entries - if newEffectiveCollateral == 0.0 { newEffectiveCollateral = nil } - if newEffectiveDebt == 0.0 { newEffectiveDebt = nil } - - return balanceSheet.withUpdatedContributions( - tokenType: withdrawType, - effectiveCollateral: newEffectiveCollateral, - effectiveDebt: newEffectiveDebt - ) + panic("unreachable") } /// Computes the amount of a given token that must be deposited to bring a position to a target health. diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 84df5a5d..9ea15b8a 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -699,21 +699,23 @@ access(all) contract FlowALPv0 { withdrawType: Type, withdrawAmount: UFix64 ): FlowALPModels.BalanceSheet { - let balance = position.getBalance(withdrawType) - var withdrawCreditInterestIndex: UFix128 = 1.0 - if balance?.direction == FlowALPModels.BalanceDirection.Credit { - withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() - } + let tokenState = self._borrowUpdatedTokenState(type: withdrawType) + let snapshot = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) + ) + ) return FlowALPHealth.computeAdjustedBalancesAfterWithdrawal( balanceSheet: balanceSheet, - withdrawBalance: balance, + withdrawBalance: position.getBalance(withdrawType), withdrawType: withdrawType, withdrawAmount: withdrawAmount, - withdrawPrice: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), - withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), - withdrawCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), - withdrawCreditInterestIndex: withdrawCreditInterestIndex + tokenSnapshot: snapshot ) } From eedb2315708472ef2a5d1e622e636b261da93b47 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 11:04:15 -0700 Subject: [PATCH 04/19] Refactor computeAdjustedBalancesAfterDeposit to remove-old/add-new pattern Symmetric to the withdrawal refactor. Adds trueBalanceAfterDeposit helper. The wrapper now constructs a TokenSnapshot instead of passing individual price/factor/index parameters. Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 115 ++++++++++++++++------------ cadence/contracts/FlowALPv0.cdc | 23 +++--- 2 files changed, 76 insertions(+), 62 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 003933ff..3629bfec 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -199,86 +199,99 @@ access(all) contract FlowALPHealth { /// Computes adjusted effective collateral and debt after a hypothetical deposit. /// - /// This function determines how a deposit would affect the position's balance sheet, - /// accounting for whether the position holds a credit (collateral) or debit (debt) balance - /// in the deposited token. If the position has debt in the token, the deposit first pays - /// down debt before accumulating as collateral. + /// Uses a "remove old contribution, add new contribution" approach: + /// 1. Remove the current per-token effective collateral/debt entry + /// 2. Compute the new true balance after the deposit + /// 3. Compute the new effective contribution from the post-deposit balance + /// 4. Return a new BalanceSheet with the updated per-token entry /// /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param depositBalance: The position's existing balance for the deposited token, if any /// @param depositType: The type of token being deposited /// @param depositAmount: The amount of tokens to deposit - /// @param depositPrice: The oracle price of the deposited token - /// @param depositBorrowFactor: The borrow factor applied to debt in the deposited token - /// @param depositCollateralFactor: The collateral factor applied to collateral in the deposited token - /// @param depositDebitInterestIndex: The debit interest index for the deposited token + /// @param tokenSnapshot: Snapshot of the deposited token's price, interest indices, and risk params /// @return A new BalanceSheet reflecting the effective collateral and debt after the deposit access(account) fun computeAdjustedBalancesAfterDeposit( balanceSheet: FlowALPModels.BalanceSheet, depositBalance: FlowALPModels.InternalBalance?, depositType: Type, depositAmount: UFix64, - depositPrice: UFix128, - depositBorrowFactor: UFix128, - depositCollateralFactor: UFix128, - depositDebitInterestIndex: UFix128, - isDebugLogging: Bool + tokenSnapshot: FlowALPModels.TokenSnapshot ): FlowALPModels.BalanceSheet { if depositAmount == 0.0 { return balanceSheet } let depositAmountU = UFix128(depositAmount) - let balance = depositBalance + + // Compute the post-deposit true balance and direction. + let after = self.trueBalanceAfterDeposit( + balance: depositBalance, + depositAmount: depositAmountU, + tokenSnapshot: tokenSnapshot + ) + + // Compute new per-token effective values from the post-deposit true balance. + var newEffectiveCollateral: UFix128? = nil + var newEffectiveDebt: UFix128? = nil + if after.quantity > 0.0 { + switch after.direction { + case FlowALPModels.BalanceDirection.Credit: + newEffectiveCollateral = tokenSnapshot.effectiveCollateral(creditBalance: after.quantity) + case FlowALPModels.BalanceDirection.Debit: + newEffectiveDebt = tokenSnapshot.effectiveDebt(debitBalance: after.quantity) + } + } + + return balanceSheet.withUpdatedContributions( + tokenType: depositType, + effectiveCollateral: newEffectiveCollateral, + effectiveDebt: newEffectiveDebt + ) + } + + /// Computes the true balance (direction + amount) after a deposit. + /// + /// Starting from the current balance (credit or debit), adds the deposit amount. + /// If the position has debt, the deposit pays it down and may flip into credit. + /// If the position has credit (or no balance), the deposit increases credit. + access(self) fun trueBalanceAfterDeposit( + balance: FlowALPModels.InternalBalance?, + depositAmount: UFix128, + tokenSnapshot: FlowALPModels.TokenSnapshot + ): FlowALPModels.SignedQuantity { let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit let scaledBalance = balance?.scaledBalance ?? 0.0 - // Compute the new per-token effective collateral and debt after the deposit. - var newEffectiveCollateral: UFix128? = balanceSheet.effectiveCollateralByToken[depositType] - var newEffectiveDebt: UFix128? = balanceSheet.effectiveDebtByToken[depositType] - switch direction { case FlowALPModels.BalanceDirection.Credit: - // No debt for the deposit token — the deposit creates additional collateral. - let additionalCollateral = (depositAmountU * depositPrice) * depositCollateralFactor - newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) + additionalCollateral + // Currently has credit — deposit adds more credit. + let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, interestIndex: tokenSnapshot.creditIndex + ) + return FlowALPModels.SignedQuantity( + direction: FlowALPModels.BalanceDirection.Credit, + quantity: trueCredit + depositAmount + ) case FlowALPModels.BalanceDirection.Debit: - // The user has a debt position in the given token. + // Currently in debt — deposit pays it down, possibly flipping to credit. let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, - interestIndex: depositDebitInterestIndex + scaledBalance, interestIndex: tokenSnapshot.debitIndex ) - if isDebugLogging { - log(" [CONTRACT] trueDebt: \(trueDebt)") - } - - if trueDebt >= depositAmountU { - // Deposit pays down some debt without creating collateral. - let debtDecrease = (depositAmountU * depositPrice) / depositBorrowFactor - newEffectiveDebt = (newEffectiveDebt ?? 0.0) - debtDecrease + if trueDebt >= depositAmount { + return FlowALPModels.SignedQuantity( + direction: FlowALPModels.BalanceDirection.Debit, + quantity: trueDebt - depositAmount + ) } else { - // Deposit wipes out all debt and creates some collateral. - let existingDebt = (trueDebt * depositPrice) / depositBorrowFactor - newEffectiveDebt = (newEffectiveDebt ?? 0.0) - existingDebt - let additionalCollateral = ((depositAmountU - trueDebt) * depositPrice) * depositCollateralFactor - newEffectiveCollateral = (newEffectiveCollateral ?? 0.0) + additionalCollateral + return FlowALPModels.SignedQuantity( + direction: FlowALPModels.BalanceDirection.Credit, + quantity: depositAmount - trueDebt + ) } } - if isDebugLogging { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(newEffectiveCollateral ?? 0.0)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(newEffectiveDebt ?? 0.0)") - } - - // Clean up zero/nil entries - if newEffectiveCollateral == 0.0 { newEffectiveCollateral = nil } - if newEffectiveDebt == 0.0 { newEffectiveDebt = nil } - - return balanceSheet.withUpdatedContributions( - tokenType: depositType, - effectiveCollateral: newEffectiveCollateral, - effectiveDebt: newEffectiveDebt - ) + panic("unreachable") } /// Computes the maximum amount of a given token that can be withdrawn while maintaining a target health. diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 9ea15b8a..9c19f8ed 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -804,22 +804,23 @@ access(all) contract FlowALPv0 { depositType: Type, depositAmount: UFix64 ): FlowALPModels.BalanceSheet { - let depositBalance = position.getBalance(depositType) - var depositDebitInterestIndex: UFix128 = 1.0 - if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { - depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() - } + let tokenState = self._borrowUpdatedTokenState(type: depositType) + let snapshot = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)) + ) + ) return FlowALPHealth.computeAdjustedBalancesAfterDeposit( balanceSheet: balanceSheet, - depositBalance: depositBalance, + depositBalance: position.getBalance(depositType), depositType: depositType, depositAmount: depositAmount, - depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), - depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), - depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), - depositDebitInterestIndex: depositDebitInterestIndex, - isDebugLogging: self.config.isDebugLogging() + tokenSnapshot: snapshot ) } From d8e61b51c09924ec047f95e00069ec1610b92c97 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 11:17:21 -0700 Subject: [PATCH 05/19] Replace inline healthAfterDeposit/healthAfterWithdrawal with shared helpers These ~40-line functions duplicated the adjustment logic. Now they delegate to computeAdjustedBalancesAfter{Deposit,Withdrawal} and return adjusted.health. Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPv0.cdc | 103 +++++--------------------------- 1 file changed, 16 insertions(+), 87 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 9c19f8ed..ccce1fe6 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -852,100 +852,29 @@ access(all) contract FlowALPv0 { access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix128 { let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) let position = self._borrowPosition(pid: pid) - let tokenState = self._borrowUpdatedTokenState(type: type) - - var effectiveCollateralIncrease: UFix128 = 0.0 - var effectiveDebtDecrease: UFix128 = 0.0 - - let amountU = UFix128(amount) - let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) - let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) - let balance = position.getBalance(type) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit - let scaledBalance = balance?.scaledBalance ?? 0.0 - switch direction { - case FlowALPModels.BalanceDirection.Credit: - // Since the user has no debt in the given token, - // we can just compute how much additional collateral this deposit will create. - effectiveCollateralIncrease = (amountU * price) * collateralFactor - - case FlowALPModels.BalanceDirection.Debit: - // The user has a debit position in the given token, - // we need to figure out if this deposit will only pay off some of the debt, - // or if it will also create new collateral. - let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, - interestIndex: tokenState.getDebitInterestIndex() - ) - - if trueDebt >= amountU { - // This deposit will wipe out some or all of the debt, but won't create new collateral, - // we just need to account for the debt decrease. - effectiveDebtDecrease = (amountU * price) / borrowFactor - } else { - // This deposit will wipe out all of the debt, and create new collateral. - effectiveDebtDecrease = (trueDebt * price) / borrowFactor - effectiveCollateralIncrease = (amountU - trueDebt) * price * collateralFactor - } - } - - return FlowALPMath.healthComputation( - effectiveCollateral: balanceSheet.effectiveCollateral + effectiveCollateralIncrease, - effectiveDebt: balanceSheet.effectiveDebt - effectiveDebtDecrease + let adjusted = self.computeAdjustedBalancesAfterDeposit( + balanceSheet: balanceSheet, + position: position, + depositType: type, + depositAmount: amount ) + return adjusted.health } - // Returns health value of this position if the given amount of the specified token were withdrawn without - // using the top up source. - // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates - // that the proposed withdrawal would fail (unless a top up source is available and used). + /// Returns health value of this position if the given amount of the specified token were withdrawn + /// without using the top up source. + /// NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates + /// that the proposed withdrawal would fail (unless a top up source is available and used). access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix128 { let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) let position = self._borrowPosition(pid: pid) - let tokenState = self._borrowUpdatedTokenState(type: type) - - var effectiveCollateralDecrease: UFix128 = 0.0 - var effectiveDebtIncrease: UFix128 = 0.0 - - let amountU = UFix128(amount) - let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) - let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) - let balance = position.getBalance(type) - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit - let scaledBalance = balance?.scaledBalance ?? 0.0 - - switch direction { - case FlowALPModels.BalanceDirection.Debit: - // The user has no credit position in the given token, - // we can just compute how much additional effective debt this withdrawal will create. - effectiveDebtIncrease = (amountU * price) / borrowFactor - - case FlowALPModels.BalanceDirection.Credit: - // The user has a credit position in the given token, - // we need to figure out if this withdrawal will only draw down some of the collateral, - // or if it will also create new debt. - let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, - interestIndex: tokenState.getCreditInterestIndex() - ) - - if trueCredit >= amountU { - // This withdrawal will draw down some collateral, but won't create new debt, - // we just need to account for the collateral decrease. - effectiveCollateralDecrease = (amountU * price) * collateralFactor - } else { - // The withdrawal will wipe out all of the collateral, and create new debt. - effectiveDebtIncrease = ((amountU - trueCredit) * price) / borrowFactor - effectiveCollateralDecrease = (trueCredit * price) * collateralFactor - } - } - - return FlowALPMath.healthComputation( - effectiveCollateral: balanceSheet.effectiveCollateral - effectiveCollateralDecrease, - effectiveDebt: balanceSheet.effectiveDebt + effectiveDebtIncrease + let adjusted = self.computeAdjustedBalancesAfterWithdrawal( + balanceSheet: balanceSheet, + position: position, + withdrawType: type, + withdrawAmount: amount ) + return adjusted.health } /////////////////////////// From 18b9d88d52d6bcef145c186154feb26c0315ad22 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 12:06:19 -0700 Subject: [PATCH 06/19] Restore TODO and documentation comments removed during refactor Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 3 +++ cadence/contracts/FlowALPv0.cdc | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 3629bfec..826d8e23 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -102,6 +102,7 @@ access(all) contract FlowALPHealth { /// Computes the amount of a given token that must be deposited to bring a position to a target health. /// + // TODO(jord): ~100-line function - consider refactoring /// This function handles the case where the deposit token may have an existing debit (debt) balance. /// If so, the deposit first pays down debt before accumulating as collateral. The computation /// determines the minimum deposit required to reach the target health, accounting for both @@ -184,6 +185,7 @@ access(all) contract FlowALPHealth { } let healthChangeU = targetHealth - healthAfterWithdrawal + // TODO: apply the same logic as below to the early return blocks above let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor let collateralTokenCount = requiredEffectiveCollateral / depositPrice @@ -294,6 +296,7 @@ access(all) contract FlowALPHealth { panic("unreachable") } + // TODO(jord): ~100-line function - consider refactoring /// Computes the maximum amount of a given token that can be withdrawn while maintaining a target health. /// /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index ccce1fe6..fd10f0c7 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -693,6 +693,7 @@ access(all) contract FlowALPv0 { ) } + // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, @@ -719,6 +720,8 @@ access(all) contract FlowALPv0 { ) } + // TODO(jord): ~100-line function - consider refactoring + // TODO: documentation access(self) fun computeRequiredDepositForHealth( position: &{FlowALPModels.InternalPosition}, depositType: Type, @@ -798,6 +801,7 @@ access(all) contract FlowALPv0 { ) } + // Helper function to compute balances after deposit access(self) fun computeAdjustedBalancesAfterDeposit( balanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, @@ -824,6 +828,8 @@ access(all) contract FlowALPv0 { ) } + // Helper function to compute available withdrawal + // TODO(jord): ~100-line function - consider refactoring access(self) fun computeAvailableWithdrawal( position: &{FlowALPModels.InternalPosition}, withdrawType: Type, @@ -1919,6 +1925,7 @@ access(all) contract FlowALPv0 { access(self) fun _getUpdatedBalanceSheet(pid: UInt64): FlowALPModels.BalanceSheet { let position = self._borrowPosition(pid: pid) + // Get the position's collateral and debt values in terms of the default token. var effectiveCollateralByToken: {Type: UFix128} = {} var effectiveDebtByToken: {Type: UFix128} = {} From aa4f12e58a14e39b1dedde97be20be8607d7bf35 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 15:30:37 -0700 Subject: [PATCH 07/19] Restore inline comments in computeRequiredDepositForHealth and computeAvailableWithdrawal Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 50 ++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 826d8e23..e4387c44 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -134,12 +134,16 @@ access(all) contract FlowALPHealth { log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") } + // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) + // Now we can figure out how many of the given token would need to be deposited to bring the position + // to the target health value. var healthAfterWithdrawal = adjusted.health if isDebugLogging { log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") } if healthAfterWithdrawal >= targetHealth { + // The position is already at or above the target health, so we don't need to deposit anything. return 0.0 } @@ -148,6 +152,8 @@ access(all) contract FlowALPHealth { var debtTokenCount: UFix128 = 0.0 let maybeBalance = depositBalance if maybeBalance?.direction == FlowALPModels.BalanceDirection.Debit { + // The user has a debt position in the given token, we start by looking at the health impact of paying off + // the entire debt. let debtBalance = maybeBalance!.scaledBalance let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( debtBalance, @@ -155,26 +161,39 @@ access(all) contract FlowALPHealth { ) let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor + // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal, + // it means we can pay off all debt var effectiveDebtAfterPayment: UFix128 = 0.0 if debtEffectiveValue <= effectiveDebtAfterWithdrawal { effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue } + // Check what the new health would be if we paid off all of this debt let potentialHealth = FlowALPMath.healthComputation( effectiveCollateral: effectiveCollateralAfterWithdrawal, effectiveDebt: effectiveDebtAfterPayment ) + // Does paying off all of the debt reach the target health? Then we're done. if potentialHealth >= targetHealth { + // We can reach the target health by paying off some or all of the debt. We can easily + // compute how many units of the token would be needed to reach the target health. let requiredEffectiveDebt = effectiveDebtAfterWithdrawal - (effectiveCollateralAfterWithdrawal / targetHealth) + // The amount of the token to pay back, in units of the token. let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice if isDebugLogging { log(" [CONTRACT] paybackAmount: \(paybackAmount)") } return FlowALPMath.toUFix64RoundUp(paybackAmount) } else { + // We can pay off the entire debt, but we still need to deposit more to reach the target health. + // We have logic below that can determine the collateral deposition required to reach the target health + // from this new health position. Rather than copy that logic here, we fall through into it. But first + // we have to record the amount of tokens that went towards debt payback and adjust the effective + // debt to reflect that it has been paid off. debtTokenCount = trueDebtTokenCount + // Ensure we don't underflow if debtEffectiveValue <= effectiveDebtAfterWithdrawal { effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue } else { @@ -184,10 +203,18 @@ access(all) contract FlowALPHealth { } } + // At this point, we're either dealing with a position that didn't have a debt position in the deposit + // token, or we've accounted for the debt payoff and adjusted the effective debt above. + // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the + // target health. We can rearrange the health equation to solve for the required collateral: + + // We need to increase the effective collateral from its current value to the required value, so we + // multiply the required health change by the effective debt, and turn that into a token amount. let healthChangeU = targetHealth - healthAfterWithdrawal // TODO: apply the same logic as below to the early return blocks above let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor + // The amount of the token to deposit, in units of the token. let collateralTokenCount = requiredEffectiveCollateral / depositPrice if isDebugLogging { log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") @@ -196,6 +223,7 @@ access(all) contract FlowALPHealth { log(" [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)") } + // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) } @@ -327,13 +355,18 @@ access(all) contract FlowALPHealth { } if healthAfterDeposit <= targetHealth { + // The position is already at or below the provided target health, so we can't withdraw anything. return 0.0 } + // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep + // track of the number of tokens that are available from collateral var collateralTokenCount: UFix128 = 0.0 let maybeBalance = withdrawBalance if maybeBalance?.direction == FlowALPModels.BalanceDirection.Credit { + // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all + // of that collateral let creditBalance = maybeBalance!.scaledBalance let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( creditBalance, @@ -341,17 +374,24 @@ access(all) contract FlowALPHealth { ) let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor + // Check what the new health would be if we took out all of this collateral let potentialHealth = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, + effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? effectiveDebt: effectiveDebtAfterDeposit ) + // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. if potentialHealth <= targetHealth { + // We will hit the health target before using up all of the withdraw token credit. We can easily + // compute how many units of the token would bring the position down to the target health. + // We will hit the health target before using up all available withdraw credit. + let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) if isDebugLogging { log(" [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") } + // The amount of the token we can take using that amount of health let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice if isDebugLogging { log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") @@ -359,6 +399,9 @@ access(all) contract FlowALPHealth { return FlowALPMath.toUFix64RoundDown(availableTokenCount) } else { + // We can flip this credit position into a debit position, before hitting the target health. + // We have logic below that can determine health changes for debit positions. We've copied it here + // with an added handling for the case where the health after deposit is an edgecase collateralTokenCount = trueCredit effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue if isDebugLogging { @@ -366,6 +409,7 @@ access(all) contract FlowALPHealth { log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") } + // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice if isDebugLogging { @@ -378,6 +422,10 @@ access(all) contract FlowALPHealth { } } + // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw + // token, or we've accounted for the credit balance and adjusted the effective collateral above. + + // We can calculate the available debt increase that would bring us to the target health let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice if isDebugLogging { From fc1e467797b764bda8a703f1323171f4f1be2ff2 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 16 Mar 2026 19:10:25 -0700 Subject: [PATCH 08/19] Rename SignedQuantity to Balance and refactor InternalBalance to compose it Renames the SignedQuantity struct to Balance (more natural name) and eliminates the redundant direction field from InternalBalance by making its scaledBalance field a Balance (direction + quantity) instead of a bare UFix128. The InternalBalance init signature is preserved to minimize churn at construction sites. Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 32 ++++++------ cadence/contracts/FlowALPModels.cdc | 79 +++++++++++++++++------------ cadence/contracts/FlowALPv0.cdc | 32 ++++++------ 3 files changed, 78 insertions(+), 65 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index e4387c44..3ecd2546 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -65,9 +65,9 @@ access(all) contract FlowALPHealth { balance: FlowALPModels.InternalBalance?, withdrawAmount: UFix128, tokenSnapshot: FlowALPModels.TokenSnapshot - ): FlowALPModels.SignedQuantity { - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Debit - let scaledBalance = balance?.scaledBalance ?? 0.0 + ): FlowALPModels.Balance { + let direction = balance?.scaledBalance?.direction ?? FlowALPModels.BalanceDirection.Debit + let scaledBalance = balance?.scaledBalance?.quantity ?? 0.0 switch direction { case FlowALPModels.BalanceDirection.Debit: @@ -75,7 +75,7 @@ access(all) contract FlowALPHealth { let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, interestIndex: tokenSnapshot.debitIndex ) - return FlowALPModels.SignedQuantity( + return FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Debit, quantity: trueDebt + withdrawAmount ) @@ -86,12 +86,12 @@ access(all) contract FlowALPHealth { scaledBalance, interestIndex: tokenSnapshot.creditIndex ) if trueCredit >= withdrawAmount { - return FlowALPModels.SignedQuantity( + return FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Credit, quantity: trueCredit - withdrawAmount ) } else { - return FlowALPModels.SignedQuantity( + return FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Debit, quantity: withdrawAmount - trueCredit ) @@ -151,10 +151,10 @@ access(all) contract FlowALPHealth { // track of the number of tokens that went towards paying off debt. var debtTokenCount: UFix128 = 0.0 let maybeBalance = depositBalance - if maybeBalance?.direction == FlowALPModels.BalanceDirection.Debit { + if maybeBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Debit { // The user has a debt position in the given token, we start by looking at the health impact of paying off // the entire debt. - let debtBalance = maybeBalance!.scaledBalance + let debtBalance = maybeBalance!.scaledBalance.quantity let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( debtBalance, interestIndex: depositDebitInterestIndex @@ -289,9 +289,9 @@ access(all) contract FlowALPHealth { balance: FlowALPModels.InternalBalance?, depositAmount: UFix128, tokenSnapshot: FlowALPModels.TokenSnapshot - ): FlowALPModels.SignedQuantity { - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit - let scaledBalance = balance?.scaledBalance ?? 0.0 + ): FlowALPModels.Balance { + let direction = balance?.scaledBalance?.direction ?? FlowALPModels.BalanceDirection.Credit + let scaledBalance = balance?.scaledBalance?.quantity ?? 0.0 switch direction { case FlowALPModels.BalanceDirection.Credit: @@ -299,7 +299,7 @@ access(all) contract FlowALPHealth { let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( scaledBalance, interestIndex: tokenSnapshot.creditIndex ) - return FlowALPModels.SignedQuantity( + return FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Credit, quantity: trueCredit + depositAmount ) @@ -310,12 +310,12 @@ access(all) contract FlowALPHealth { scaledBalance, interestIndex: tokenSnapshot.debitIndex ) if trueDebt >= depositAmount { - return FlowALPModels.SignedQuantity( + return FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Debit, quantity: trueDebt - depositAmount ) } else { - return FlowALPModels.SignedQuantity( + return FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Credit, quantity: depositAmount - trueDebt ) @@ -364,10 +364,10 @@ access(all) contract FlowALPHealth { var collateralTokenCount: UFix128 = 0.0 let maybeBalance = withdrawBalance - if maybeBalance?.direction == FlowALPModels.BalanceDirection.Credit { + if maybeBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Credit { // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all // of that collateral - let creditBalance = maybeBalance!.scaledBalance + let creditBalance = maybeBalance!.scaledBalance.quantity let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( creditBalance, interestIndex: withdrawCreditInterestIndex diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 86304f64..fb875bcb 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -72,7 +72,7 @@ access(all) contract FlowALPModels { access(all) case Debit } - access(all) struct SignedQuantity { + access(all) struct Balance { /// The direction (sign) of this quantity. access(all) let direction: BalanceDirection /// The unsigned numeric value. @@ -89,9 +89,6 @@ access(all) contract FlowALPModels { /// A structure used internally to track a position's balance for a particular token access(all) struct InternalBalance { - /// The current direction of the balance - Credit (owed to borrower) or Debit (owed to protocol) - access(all) var direction: BalanceDirection - /// Internally, position balances are tracked using a "scaled balance". /// The "scaled balance" is the actual balance divided by the current interest index for the associated token. /// This means we don't need to update the balance of a position as time passes, even as interest rates change. @@ -100,15 +97,15 @@ access(all) contract FlowALPModels { /// so the scaled balance will be roughly of the same order of magnitude as the actual balance. /// We store the scaled balance as UFix128 to align with UFix128 interest indices /// and to reduce rounding during true ↔ scaled conversions. - access(all) var scaledBalance: UFix128 + /// The Balance includes the direction (Credit or Debit) and the unsigned scaled quantity. + access(all) var scaledBalance: Balance // Single initializer that can handle both cases init( direction: BalanceDirection, scaledBalance: UFix128 ) { - self.direction = direction - self.scaledBalance = scaledBalance + self.scaledBalance = Balance(direction: direction, quantity: scaledBalance) } /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values @@ -122,7 +119,7 @@ access(all) contract FlowALPModels { /// public deposit APIs accept UFix64 and are converted at the boundary. /// access(all) fun recordDeposit(amount: UFix128, tokenState: auth(EImplementation) &{TokenState}) { - switch self.direction { + switch self.scaledBalance.direction { case BalanceDirection.Credit: // Depositing into a credit position just increases the balance. // @@ -138,7 +135,10 @@ access(all) contract FlowALPModels { interestIndex: tokenState.getCreditInterestIndex() ) - self.scaledBalance = self.scaledBalance + scaledDeposit + self.scaledBalance = Balance( + direction: BalanceDirection.Credit, + quantity: self.scaledBalance.quantity + scaledDeposit + ) // Increase the total credit balance for the token tokenState.increaseCreditBalance(by: amount) @@ -148,7 +148,7 @@ access(all) contract FlowALPModels { // to see if this deposit will flip the position from debit to credit. let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - self.scaledBalance, + self.scaledBalance.quantity, interestIndex: tokenState.getDebitInterestIndex() ) @@ -158,9 +158,12 @@ access(all) contract FlowALPModels { // so we just decrement the debt. let updatedBalance = trueBalance - amount - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getDebitInterestIndex() + self.scaledBalance = Balance( + direction: BalanceDirection.Debit, + quantity: FlowALPMath.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.getDebitInterestIndex() + ) ) // Decrease the total debit balance for the token @@ -171,10 +174,12 @@ access(all) contract FlowALPModels { // so we switch to a credit position. let updatedBalance = amount - trueBalance - self.direction = BalanceDirection.Credit - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getCreditInterestIndex() + self.scaledBalance = Balance( + direction: BalanceDirection.Credit, + quantity: FlowALPMath.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.getCreditInterestIndex() + ) ) // Increase the credit balance AND decrease the debit balance @@ -195,7 +200,7 @@ access(all) contract FlowALPModels { /// public withdraw APIs are UFix64 and are converted at the boundary. /// access(all) fun recordWithdrawal(amount: UFix128, tokenState: auth(EImplementation) &{TokenState}) { - switch self.direction { + switch self.scaledBalance.direction { case BalanceDirection.Debit: // Withdrawing from a debit position just increases the debt amount. // @@ -211,7 +216,10 @@ access(all) contract FlowALPModels { interestIndex: tokenState.getDebitInterestIndex() ) - self.scaledBalance = self.scaledBalance + scaledWithdrawal + self.scaledBalance = Balance( + direction: BalanceDirection.Debit, + quantity: self.scaledBalance.quantity + scaledWithdrawal + ) // Increase the total debit balance for the token tokenState.increaseDebitBalance(by: amount) @@ -221,7 +229,7 @@ access(all) contract FlowALPModels { // we first need to compute the true balance // to see if this withdrawal will flip the position from credit to debit. let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - self.scaledBalance, + self.scaledBalance.quantity, interestIndex: tokenState.getCreditInterestIndex() ) @@ -230,9 +238,12 @@ access(all) contract FlowALPModels { // so we just decrement the credit balance. let updatedBalance = trueBalance - amount - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getCreditInterestIndex() + self.scaledBalance = Balance( + direction: BalanceDirection.Credit, + quantity: FlowALPMath.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.getCreditInterestIndex() + ) ) // Decrease the total credit balance for the token @@ -242,10 +253,12 @@ access(all) contract FlowALPModels { // so we switch to a debit position. let updatedBalance = amount - trueBalance - self.direction = BalanceDirection.Debit - self.scaledBalance = FlowALPMath.trueBalanceToScaledBalance( - updatedBalance, - interestIndex: tokenState.getDebitInterestIndex() + self.scaledBalance = Balance( + direction: BalanceDirection.Debit, + quantity: FlowALPMath.trueBalanceToScaledBalance( + updatedBalance, + interestIndex: tokenState.getDebitInterestIndex() + ) ) // Decrease the credit balance AND increase the debit balance @@ -393,13 +406,13 @@ access(all) contract FlowALPModels { access(all) view fun trueBalance(ofToken: Type): UFix128 { if let balance = self.balances[ofToken] { if let tokenSnapshot = self.snapshots[ofToken] { - switch balance.direction { + switch balance.scaledBalance.direction { case BalanceDirection.Debit: return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, interestIndex: tokenSnapshot.getDebitIndex()) + balance.scaledBalance.quantity, interestIndex: tokenSnapshot.getDebitIndex()) case BalanceDirection.Credit: return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, interestIndex: tokenSnapshot.getCreditIndex()) + balance.scaledBalance.quantity, interestIndex: tokenSnapshot.getCreditIndex()) } panic("unreachable") } @@ -418,10 +431,10 @@ access(all) contract FlowALPModels { let balance = view.balances[tokenType]! let snap = view.snapshots[tokenType]! - switch balance.direction { + switch balance.scaledBalance.direction { case BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal @@ -429,7 +442,7 @@ access(all) contract FlowALPModels { case BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index fd10f0c7..b003af02 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -72,10 +72,10 @@ access(all) contract FlowALPv0 { let balance = view.balances[tokenType]! let snap = view.snapshots[tokenType]! - switch balance.direction { + switch balance.scaledBalance.direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal @@ -83,7 +83,7 @@ access(all) contract FlowALPv0 { case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal @@ -94,7 +94,7 @@ access(all) contract FlowALPv0 { let collateralFactor = withdrawSnap.getRisk().getCollateralFactor() let borrowFactor = withdrawSnap.getRisk().getBorrowFactor() - if withdrawBal == nil || withdrawBal!.direction == FlowALPModels.BalanceDirection.Debit { + if withdrawBal == nil || withdrawBal!.scaledBalance.direction == FlowALPModels.BalanceDirection.Debit { // withdrawing increases debt let numerator = effectiveCollateralTotal let denominatorTarget = numerator / targetHealth @@ -105,7 +105,7 @@ access(all) contract FlowALPv0 { } else { // withdrawing reduces collateral let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - withdrawBal!.scaledBalance, + withdrawBal!.scaledBalance.quantity, interestIndex: withdrawSnap.getCreditIndex() ) let maxPossible = trueBalance @@ -434,10 +434,10 @@ access(all) contract FlowALPv0 { let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - switch balance.direction { + switch balance.scaledBalance.direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: tokenState.getCreditInterestIndex() ) @@ -447,7 +447,7 @@ access(all) contract FlowALPv0 { case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: tokenState.getDebitInterestIndex() ) @@ -490,15 +490,15 @@ access(all) contract FlowALPv0 { let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, - interestIndex: balance.direction == FlowALPModels.BalanceDirection.Credit + balance.scaledBalance.quantity, + interestIndex: balance.scaledBalance.direction == FlowALPModels.BalanceDirection.Credit ? tokenState.getCreditInterestIndex() : tokenState.getDebitInterestIndex() ) balances.append(FlowALPModels.PositionBalance( vaultType: type, - direction: balance.direction, + direction: balance.scaledBalance.direction, balance: FlowALPMath.toUFix64Round(trueBalance) )) } @@ -731,7 +731,7 @@ access(all) contract FlowALPv0 { ): UFix64 { let depositBalance = position.getBalance(depositType) var depositDebitInterestIndex: UFix128 = 1.0 - if depositBalance?.direction == FlowALPModels.BalanceDirection.Debit { + if depositBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Debit { depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() } @@ -838,7 +838,7 @@ access(all) contract FlowALPv0 { ): UFix64 { let withdrawBalance = position.getBalance(withdrawType) var withdrawCreditInterestIndex: UFix128 = 1.0 - if withdrawBalance?.direction == FlowALPModels.BalanceDirection.Credit { + if withdrawBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Credit { withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() } @@ -1934,10 +1934,10 @@ access(all) contract FlowALPv0 { let tokenState = self._borrowUpdatedTokenState(type: type) let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - switch balance.direction { + switch balance.scaledBalance.direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: tokenState.getCreditInterestIndex() ) let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) @@ -1945,7 +1945,7 @@ access(all) contract FlowALPv0 { case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.scaledBalance.quantity, interestIndex: tokenState.getDebitInterestIndex() ) let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) From 6de8448aac114e681b33dd538a8600c2257db5c3 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 17 Mar 2026 11:16:08 -0700 Subject: [PATCH 09/19] Add TokenSnapshot.effectiveBalance and BalanceSheet.withReplacedTokenBalance Consolidate the repeated switch-on-direction pattern in FlowALPHealth into two new methods that work with Balance directly, replacing the old withUpdatedContributions which took separate optional collateral/debt values. Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 36 ++++---------------- cadence/contracts/FlowALPModels.cdc | 53 +++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 3ecd2546..dadece7d 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -37,22 +37,10 @@ access(all) contract FlowALPHealth { tokenSnapshot: tokenSnapshot ) - // Compute new per-token effective values from the post-withdrawal true balance. - var newEffectiveCollateral: UFix128? = nil - var newEffectiveDebt: UFix128? = nil - if after.quantity > 0.0 { - switch after.direction { - case FlowALPModels.BalanceDirection.Credit: - newEffectiveCollateral = tokenSnapshot.effectiveCollateral(creditBalance: after.quantity) - case FlowALPModels.BalanceDirection.Debit: - newEffectiveDebt = tokenSnapshot.effectiveDebt(debitBalance: after.quantity) - } - } - - return balanceSheet.withUpdatedContributions( + let effectiveBalance = tokenSnapshot.effectiveBalance(balance: after) + return balanceSheet.withReplacedTokenBalance( tokenType: withdrawType, - effectiveCollateral: newEffectiveCollateral, - effectiveDebt: newEffectiveDebt + effectiveBalance: effectiveBalance ) } @@ -261,22 +249,10 @@ access(all) contract FlowALPHealth { tokenSnapshot: tokenSnapshot ) - // Compute new per-token effective values from the post-deposit true balance. - var newEffectiveCollateral: UFix128? = nil - var newEffectiveDebt: UFix128? = nil - if after.quantity > 0.0 { - switch after.direction { - case FlowALPModels.BalanceDirection.Credit: - newEffectiveCollateral = tokenSnapshot.effectiveCollateral(creditBalance: after.quantity) - case FlowALPModels.BalanceDirection.Debit: - newEffectiveDebt = tokenSnapshot.effectiveDebt(debitBalance: after.quantity) - } - } - - return balanceSheet.withUpdatedContributions( + let effectiveBalance = tokenSnapshot.effectiveBalance(balance: after) + return balanceSheet.withReplacedTokenBalance( tokenType: depositType, - effectiveCollateral: newEffectiveCollateral, - effectiveDebt: newEffectiveDebt + effectiveBalance: effectiveBalance ) } diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index fb875bcb..d0de2791 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -370,6 +370,26 @@ access(all) contract FlowALPModels { access(all) view fun effectiveCollateral(creditBalance: UFix128): UFix128 { return FlowALPMath.effectiveCollateral(credit: creditBalance, price: self.price, collateralFactor: self.risk.getCollateralFactor()) } + + /// Returns the effective value (collateral or debt) for the given balance, based on its direction. + access(all) fun effectiveBalance(balance: Balance): Balance { + if balance.quantity == 0.0 { + return balance + } + switch balance.direction { + case BalanceDirection.Credit: + return Balance( + direction: BalanceDirection.Credit, + quantity: self.effectiveCollateral(creditBalance: balance.quantity) + ) + case BalanceDirection.Debit: + return Balance( + direction: BalanceDirection.Debit, + quantity: self.effectiveDebt(debitBalance: balance.quantity) + ) + } + panic("unreachable") + } } /// Copy-only representation of a position used by pure math (no storage refs) @@ -524,25 +544,30 @@ access(all) contract FlowALPModels { return self.effectiveDebtByToken } - /// Returns a new BalanceSheet with one token's contributions replaced. - /// Pass nil to remove a token's entry from the corresponding map. - access(all) fun withUpdatedContributions( + /// Returns a new BalanceSheet with one token's effective balance replaced. + /// The balance direction determines whether the value goes into the collateral or debt map. + /// A zero-quantity balance removes the token from both maps. + access(all) fun withReplacedTokenBalance( tokenType: Type, - effectiveCollateral: UFix128?, - effectiveDebt: UFix128? + effectiveBalance: Balance ): BalanceSheet { let newCollateral = self.effectiveCollateralByToken let newDebt = self.effectiveDebtByToken - if let coll = effectiveCollateral { - newCollateral[tokenType] = coll - } else { - newCollateral.remove(key: tokenType) - } - if let debt = effectiveDebt { - newDebt[tokenType] = debt - } else { - newDebt.remove(key: tokenType) + + // Remove old entries for this token from both maps + newCollateral.remove(key: tokenType) + newDebt.remove(key: tokenType) + + // Add new entry based on direction (only if non-zero) + if effectiveBalance.quantity > 0.0 { + switch effectiveBalance.direction { + case BalanceDirection.Credit: + newCollateral[tokenType] = effectiveBalance.quantity + case BalanceDirection.Debit: + newDebt[tokenType] = effectiveBalance.quantity + } } + return BalanceSheet( effectiveCollateral: newCollateral, effectiveDebt: newDebt From 4fc16822e502ebaf4e1c474e3182447260434bd5 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 11:25:38 -0700 Subject: [PATCH 10/19] Replace trueBalanceAfterDeposit/Withdrawal with unified trueBalanceAfterDelta Consolidate the two mirror-image functions into a single trueBalanceAfterDelta that accepts a Balance (direction + quantity) as the delta argument. Deposits pass a Credit delta, withdrawals pass a Debit delta. Co-Authored-By: Claude Opus 4.6 --- cadence/contracts/FlowALPHealth.cdc | 137 ++++++++++------------------ cadence/contracts/FlowALPv0.cdc | 1 - 2 files changed, 48 insertions(+), 90 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index dadece7d..4facb654 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -5,11 +5,10 @@ access(all) contract FlowALPHealth { /// Computes adjusted effective collateral and debt after a hypothetical withdrawal. /// - /// Uses a "remove old contribution, add new contribution" approach: - /// 1. Remove the current per-token effective collateral/debt entry - /// 2. Compute the new true balance after the withdrawal - /// 3. Compute the new effective contribution from the post-withdrawal balance - /// 4. Return a new BalanceSheet with the updated per-token entry + /// This function determines how a withdrawal would affect the position's balance sheet, + /// accounting for whether the position holds a credit (collateral) or debit (debt) balance + /// in the withdrawn token. If the position has collateral in the token, the withdrawal may + /// either draw down collateral, or exhaust it entirely and create new debt. /// /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any @@ -31,61 +30,62 @@ access(all) contract FlowALPHealth { let withdrawAmountU = UFix128(withdrawAmount) // Compute the post-withdrawal true balance and direction. - let after = self.trueBalanceAfterWithdrawal( + let balanceAfterWithdrawal = self.trueBalanceAfterDelta( balance: withdrawBalance, - withdrawAmount: withdrawAmountU, + delta: FlowALPModels.Balance( + direction: FlowALPModels.BalanceDirection.Debit, + quantity: withdrawAmountU + ), tokenSnapshot: tokenSnapshot ) - let effectiveBalance = tokenSnapshot.effectiveBalance(balance: after) + let effectiveBalance = tokenSnapshot.effectiveBalance(balance: balanceAfterWithdrawal) return balanceSheet.withReplacedTokenBalance( tokenType: withdrawType, effectiveBalance: effectiveBalance ) } - /// Computes the true balance (direction + amount) after a withdrawal. + /// Computes the true balance after applying a signed delta. /// - /// Starting from the current balance (credit or debit), subtracts the withdrawal amount. - /// If the position has credit, the withdrawal draws it down and may flip into debt. - /// If the position has debt (or no balance), the withdrawal increases debt. - access(self) fun trueBalanceAfterWithdrawal( + /// Starting from the current balance (credit or debit), applies the delta. + /// A Credit delta increases credit / pays down debt; a Debit delta increases debt / draws down credit. + /// The result may flip direction if the delta exceeds the current balance. + access(self) fun trueBalanceAfterDelta( balance: FlowALPModels.InternalBalance?, - withdrawAmount: UFix128, + delta: FlowALPModels.Balance, tokenSnapshot: FlowALPModels.TokenSnapshot ): FlowALPModels.Balance { - let direction = balance?.scaledBalance?.direction ?? FlowALPModels.BalanceDirection.Debit + let currentDirection = balance?.scaledBalance?.direction ?? delta.direction let scaledBalance = balance?.scaledBalance?.quantity ?? 0.0 - switch direction { - case FlowALPModels.BalanceDirection.Debit: - // Currently in debt — withdrawal adds more debt. - let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, interestIndex: tokenSnapshot.debitIndex - ) - return FlowALPModels.Balance( - direction: FlowALPModels.BalanceDirection.Debit, - quantity: trueDebt + withdrawAmount - ) - - case FlowALPModels.BalanceDirection.Credit: - // Currently has credit — withdrawal draws it down, possibly flipping to debt. - let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, interestIndex: tokenSnapshot.creditIndex - ) - if trueCredit >= withdrawAmount { - return FlowALPModels.Balance( - direction: FlowALPModels.BalanceDirection.Credit, - quantity: trueCredit - withdrawAmount - ) - } else { - return FlowALPModels.Balance( - direction: FlowALPModels.BalanceDirection.Debit, - quantity: withdrawAmount - trueCredit - ) - } + let interestIndex = currentDirection == FlowALPModels.BalanceDirection.Credit + ? tokenSnapshot.creditIndex + : tokenSnapshot.debitIndex + let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( + scaledBalance, interestIndex: interestIndex + ) + + if currentDirection == delta.direction { + // Same direction — delta reinforces the current balance. + return FlowALPModels.Balance( + direction: currentDirection, + quantity: trueBalance + delta.quantity + ) + } + + // Opposite direction — delta offsets the current balance, possibly flipping. + if trueBalance >= delta.quantity { + return FlowALPModels.Balance( + direction: currentDirection, + quantity: trueBalance - delta.quantity + ) + } else { + return FlowALPModels.Balance( + direction: delta.direction, + quantity: delta.quantity - trueBalance + ) } - panic("unreachable") } /// Computes the amount of a given token that must be deposited to bring a position to a target health. @@ -243,9 +243,12 @@ access(all) contract FlowALPHealth { let depositAmountU = UFix128(depositAmount) // Compute the post-deposit true balance and direction. - let after = self.trueBalanceAfterDeposit( + let after = self.trueBalanceAfterDelta( balance: depositBalance, - depositAmount: depositAmountU, + delta: FlowALPModels.Balance( + direction: FlowALPModels.BalanceDirection.Credit, + quantity: depositAmountU + ), tokenSnapshot: tokenSnapshot ) @@ -256,50 +259,6 @@ access(all) contract FlowALPHealth { ) } - /// Computes the true balance (direction + amount) after a deposit. - /// - /// Starting from the current balance (credit or debit), adds the deposit amount. - /// If the position has debt, the deposit pays it down and may flip into credit. - /// If the position has credit (or no balance), the deposit increases credit. - access(self) fun trueBalanceAfterDeposit( - balance: FlowALPModels.InternalBalance?, - depositAmount: UFix128, - tokenSnapshot: FlowALPModels.TokenSnapshot - ): FlowALPModels.Balance { - let direction = balance?.scaledBalance?.direction ?? FlowALPModels.BalanceDirection.Credit - let scaledBalance = balance?.scaledBalance?.quantity ?? 0.0 - - switch direction { - case FlowALPModels.BalanceDirection.Credit: - // Currently has credit — deposit adds more credit. - let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, interestIndex: tokenSnapshot.creditIndex - ) - return FlowALPModels.Balance( - direction: FlowALPModels.BalanceDirection.Credit, - quantity: trueCredit + depositAmount - ) - - case FlowALPModels.BalanceDirection.Debit: - // Currently in debt — deposit pays it down, possibly flipping to credit. - let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, interestIndex: tokenSnapshot.debitIndex - ) - if trueDebt >= depositAmount { - return FlowALPModels.Balance( - direction: FlowALPModels.BalanceDirection.Debit, - quantity: trueDebt - depositAmount - ) - } else { - return FlowALPModels.Balance( - direction: FlowALPModels.BalanceDirection.Credit, - quantity: depositAmount - trueDebt - ) - } - } - panic("unreachable") - } - // TODO(jord): ~100-line function - consider refactoring /// Computes the maximum amount of a given token that can be withdrawn while maintaining a target health. /// diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index b003af02..a547b5a8 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -720,7 +720,6 @@ access(all) contract FlowALPv0 { ) } - // TODO(jord): ~100-line function - consider refactoring // TODO: documentation access(self) fun computeRequiredDepositForHealth( position: &{FlowALPModels.InternalPosition}, From 82632c670539b9032c6cbed4e0fde0d4ce47f8cb Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 12:07:24 -0700 Subject: [PATCH 11/19] update docs, enforce credit-dir zero balances --- cadence/contracts/FlowALPHealth.cdc | 30 ++++++++++++++++++++--------- cadence/contracts/FlowALPModels.cdc | 18 +++++++++++++++-- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 4facb654..5d1da3ee 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -30,7 +30,7 @@ access(all) contract FlowALPHealth { let withdrawAmountU = UFix128(withdrawAmount) // Compute the post-withdrawal true balance and direction. - let balanceAfterWithdrawal = self.trueBalanceAfterDelta( + let trueBalanceAfterWithdrawal = self.trueBalanceAfterDelta( balance: withdrawBalance, delta: FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Debit, @@ -39,26 +39,36 @@ access(all) contract FlowALPHealth { tokenSnapshot: tokenSnapshot ) - let effectiveBalance = tokenSnapshot.effectiveBalance(balance: balanceAfterWithdrawal) + // Compute the effective collateral or debt + let effectiveBalance = tokenSnapshot.effectiveBalance(balance: trueBalanceAfterWithdrawal) return balanceSheet.withReplacedTokenBalance( tokenType: withdrawType, effectiveBalance: effectiveBalance ) } - /// Computes the true balance after applying a signed delta. + /// Computes the resulting true balance after applying a signed delta to an InternalBalance. /// - /// Starting from the current balance (credit or debit), applies the delta. - /// A Credit delta increases credit / pays down debt; a Debit delta increases debt / draws down credit. + /// The input balance and delta may have either credit or debit direction. They each may have different directions. + /// A credit-direction delta increases credit / pays down debt. A debit-direction delta increases debt / draws down credit. /// The result may flip direction if the delta exceeds the current balance. + /// + /// @param balance: The initial balance, represented as an InternalBalance (hence, scaled). If nil, considered as zero. + /// @param delta: The deposit or withdrawal to apply to the balance. + /// @param tokenSnapshot: The TokenSnapshot for the token type denominating the balance and delta parameters. + /// @return The true balance after applying the delta. access(self) fun trueBalanceAfterDelta( - balance: FlowALPModels.InternalBalance?, + balance maybeInitialBalance: FlowALPModels.InternalBalance?, delta: FlowALPModels.Balance, tokenSnapshot: FlowALPModels.TokenSnapshot ): FlowALPModels.Balance { - let currentDirection = balance?.scaledBalance?.direction ?? delta.direction - let scaledBalance = balance?.scaledBalance?.quantity ?? 0.0 + // A nil input balance means the initial balance is zero. + let initialBalance = maybeInitialBalance ?? FlowALPModels.makeZeroInternalBalance() + + let currentDirection = initialBalance.scaledBalance.direction + let scaledBalance = initialBalance.scaledBalance.quantity + // Since the initial balance is the internal representation, we scale to true balance first let interestIndex = currentDirection == FlowALPModels.BalanceDirection.Credit ? tokenSnapshot.creditIndex : tokenSnapshot.debitIndex @@ -66,8 +76,8 @@ access(all) contract FlowALPHealth { scaledBalance, interestIndex: interestIndex ) + // Same direction — delta reinforces the current balance. if currentDirection == delta.direction { - // Same direction — delta reinforces the current balance. return FlowALPModels.Balance( direction: currentDirection, quantity: trueBalance + delta.quantity @@ -76,11 +86,13 @@ access(all) contract FlowALPHealth { // Opposite direction — delta offsets the current balance, possibly flipping. if trueBalance >= delta.quantity { + // delta decreases balance but does not flip sign return FlowALPModels.Balance( direction: currentDirection, quantity: trueBalance - delta.quantity ) } else { + // delta flips sign of balance return FlowALPModels.Balance( direction: delta.direction, quantity: delta.quantity - trueBalance diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index d0de2791..b2755fc2 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -72,14 +72,22 @@ access(all) contract FlowALPModels { access(all) case Debit } + /// Balance + /// + /// A structure used to represent a signed numeric value. access(all) struct Balance { - /// The direction (sign) of this quantity. + /// The direction (sign) of this quantity. The sign is always Credit for 0 balances, by convention. access(all) let direction: BalanceDirection /// The unsigned numeric value. access(all) let quantity: UFix128 init(direction: BalanceDirection, quantity: UFix128) { - self.direction = direction + // Enforce 0-balance convention + if quantity == 0.0 { + self.direction = BalanceDirection.Credit + } else { + self.direction = direction + } self.quantity = quantity } } @@ -269,6 +277,12 @@ access(all) contract FlowALPModels { } } + /// Returns a zero Balance instance. + /// By convention, zero balances have BalanceDirection.Credit. + access(all) fun makeZeroBalance(): Balance { + return FlowALPModels.Balance(direction: BalanceDirection.Credit, quantity: 0.0) + } + /// Risk parameters for a token used in effective collateral/debt computations. /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. /// The size of this discount indicates a subjective assessment of risk for the token. From 806756253a0d48d762f39da53bc684c9d9927186 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 12:12:22 -0700 Subject: [PATCH 12/19] improve docs --- cadence/contracts/FlowALPHealth.cdc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 5d1da3ee..4ca4b1ca 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -39,7 +39,7 @@ access(all) contract FlowALPHealth { tokenSnapshot: tokenSnapshot ) - // Compute the effective collateral or debt + // Compute the effective collateral or debt, and return the updated balance sheet. let effectiveBalance = tokenSnapshot.effectiveBalance(balance: trueBalanceAfterWithdrawal) return balanceSheet.withReplacedTokenBalance( tokenType: withdrawType, @@ -229,11 +229,10 @@ access(all) contract FlowALPHealth { /// Computes adjusted effective collateral and debt after a hypothetical deposit. /// - /// Uses a "remove old contribution, add new contribution" approach: - /// 1. Remove the current per-token effective collateral/debt entry - /// 2. Compute the new true balance after the deposit - /// 3. Compute the new effective contribution from the post-deposit balance - /// 4. Return a new BalanceSheet with the updated per-token entry + /// This function determines how a deposit would affect the position's balance sheet, + /// accounting for whether the position holds a credit (collateral) or debit (debt) balance + /// in the deposited token. If the position has debt in the token, the deposit may + /// either pay down debt, or pay it off entirely and create new collateral. /// /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param depositBalance: The position's existing balance for the deposited token, if any @@ -264,6 +263,7 @@ access(all) contract FlowALPHealth { tokenSnapshot: tokenSnapshot ) + // Compute the effective collateral or debt, and return the updated balance sheet. let effectiveBalance = tokenSnapshot.effectiveBalance(balance: after) return balanceSheet.withReplacedTokenBalance( tokenType: depositType, From 8836ef337adc44912336e59b3228edf72c3d3a99 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 12:21:18 -0700 Subject: [PATCH 13/19] make internal balance field access(self) The balance field was globally mutable -- this makes it only mutable through type-defined functions. --- cadence/contracts/FlowALPHealth.cdc | 12 +++++------ cadence/contracts/FlowALPModels.cdc | 22 +++++++++++++------- cadence/contracts/FlowALPv0.cdc | 32 ++++++++++++++--------------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 4ca4b1ca..aaee4a26 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -65,8 +65,8 @@ access(all) contract FlowALPHealth { // A nil input balance means the initial balance is zero. let initialBalance = maybeInitialBalance ?? FlowALPModels.makeZeroInternalBalance() - let currentDirection = initialBalance.scaledBalance.direction - let scaledBalance = initialBalance.scaledBalance.quantity + let currentDirection = initialBalance.getScaledBalance().direction + let scaledBalance = initialBalance.getScaledBalance().quantity // Since the initial balance is the internal representation, we scale to true balance first let interestIndex = currentDirection == FlowALPModels.BalanceDirection.Credit @@ -151,10 +151,10 @@ access(all) contract FlowALPHealth { // track of the number of tokens that went towards paying off debt. var debtTokenCount: UFix128 = 0.0 let maybeBalance = depositBalance - if maybeBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Debit { + if maybeBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Debit { // The user has a debt position in the given token, we start by looking at the health impact of paying off // the entire debt. - let debtBalance = maybeBalance!.scaledBalance.quantity + let debtBalance = maybeBalance!.getScaledBalance().quantity let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( debtBalance, interestIndex: depositDebitInterestIndex @@ -311,10 +311,10 @@ access(all) contract FlowALPHealth { var collateralTokenCount: UFix128 = 0.0 let maybeBalance = withdrawBalance - if maybeBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Credit { + if maybeBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Credit { // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all // of that collateral - let creditBalance = maybeBalance!.scaledBalance.quantity + let creditBalance = maybeBalance!.getScaledBalance().quantity let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( creditBalance, interestIndex: withdrawCreditInterestIndex diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index b2755fc2..2d88fa69 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -106,7 +106,7 @@ access(all) contract FlowALPModels { /// We store the scaled balance as UFix128 to align with UFix128 interest indices /// and to reduce rounding during true ↔ scaled conversions. /// The Balance includes the direction (Credit or Debit) and the unsigned scaled quantity. - access(all) var scaledBalance: Balance + access(self) var scaledBalance: Balance // Single initializer that can handle both cases init( @@ -116,6 +116,10 @@ access(all) contract FlowALPModels { self.scaledBalance = Balance(direction: direction, quantity: scaledBalance) } + access(all) view fun getScaledBalance(): Balance { + return self.scaledBalance + } + /// Records a deposit of the defined amount, updating the inner scaledBalance as well as relevant values /// in the provided TokenState. /// @@ -283,6 +287,10 @@ access(all) contract FlowALPModels { return FlowALPModels.Balance(direction: BalanceDirection.Credit, quantity: 0.0) } + access(all) fun makeZeroInternalBalance(): InternalBalance { + return FlowALPModels.InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0) + } + /// Risk parameters for a token used in effective collateral/debt computations. /// The collateral and borrow factors are fractional values which represent a discount to the "true/market" value of the token. /// The size of this discount indicates a subjective assessment of risk for the token. @@ -440,13 +448,13 @@ access(all) contract FlowALPModels { access(all) view fun trueBalance(ofToken: Type): UFix128 { if let balance = self.balances[ofToken] { if let tokenSnapshot = self.snapshots[ofToken] { - switch balance.scaledBalance.direction { + switch balance.getScaledBalance().direction { case BalanceDirection.Debit: return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, interestIndex: tokenSnapshot.getDebitIndex()) + balance.getScaledBalance().quantity, interestIndex: tokenSnapshot.getDebitIndex()) case BalanceDirection.Credit: return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, interestIndex: tokenSnapshot.getCreditIndex()) + balance.getScaledBalance().quantity, interestIndex: tokenSnapshot.getCreditIndex()) } panic("unreachable") } @@ -465,10 +473,10 @@ access(all) contract FlowALPModels { let balance = view.balances[tokenType]! let snap = view.snapshots[tokenType]! - switch balance.scaledBalance.direction { + switch balance.getScaledBalance().direction { case BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal @@ -476,7 +484,7 @@ access(all) contract FlowALPModels { case BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index a547b5a8..e41e4588 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -72,10 +72,10 @@ access(all) contract FlowALPv0 { let balance = view.balances[tokenType]! let snap = view.snapshots[tokenType]! - switch balance.scaledBalance.direction { + switch balance.getScaledBalance().direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal @@ -83,7 +83,7 @@ access(all) contract FlowALPv0 { case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal @@ -94,7 +94,7 @@ access(all) contract FlowALPv0 { let collateralFactor = withdrawSnap.getRisk().getCollateralFactor() let borrowFactor = withdrawSnap.getRisk().getBorrowFactor() - if withdrawBal == nil || withdrawBal!.scaledBalance.direction == FlowALPModels.BalanceDirection.Debit { + if withdrawBal == nil || withdrawBal!.getScaledBalance().direction == FlowALPModels.BalanceDirection.Debit { // withdrawing increases debt let numerator = effectiveCollateralTotal let denominatorTarget = numerator / targetHealth @@ -105,7 +105,7 @@ access(all) contract FlowALPv0 { } else { // withdrawing reduces collateral let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - withdrawBal!.scaledBalance.quantity, + withdrawBal!.getScaledBalance().quantity, interestIndex: withdrawSnap.getCreditIndex() ) let maxPossible = trueBalance @@ -434,10 +434,10 @@ access(all) contract FlowALPv0 { let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - switch balance.scaledBalance.direction { + switch balance.getScaledBalance().direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: tokenState.getCreditInterestIndex() ) @@ -447,7 +447,7 @@ access(all) contract FlowALPv0 { case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: tokenState.getDebitInterestIndex() ) @@ -490,15 +490,15 @@ access(all) contract FlowALPv0 { let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, - interestIndex: balance.scaledBalance.direction == FlowALPModels.BalanceDirection.Credit + balance.getScaledBalance().quantity, + interestIndex: balance.getScaledBalance().direction == FlowALPModels.BalanceDirection.Credit ? tokenState.getCreditInterestIndex() : tokenState.getDebitInterestIndex() ) balances.append(FlowALPModels.PositionBalance( vaultType: type, - direction: balance.scaledBalance.direction, + direction: balance.getScaledBalance().direction, balance: FlowALPMath.toUFix64Round(trueBalance) )) } @@ -730,7 +730,7 @@ access(all) contract FlowALPv0 { ): UFix64 { let depositBalance = position.getBalance(depositType) var depositDebitInterestIndex: UFix128 = 1.0 - if depositBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Debit { + if depositBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Debit { depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() } @@ -837,7 +837,7 @@ access(all) contract FlowALPv0 { ): UFix64 { let withdrawBalance = position.getBalance(withdrawType) var withdrawCreditInterestIndex: UFix128 = 1.0 - if withdrawBalance?.scaledBalance?.direction == FlowALPModels.BalanceDirection.Credit { + if withdrawBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Credit { withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() } @@ -1933,10 +1933,10 @@ access(all) contract FlowALPv0 { let tokenState = self._borrowUpdatedTokenState(type: type) let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - switch balance.scaledBalance.direction { + switch balance.getScaledBalance().direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: tokenState.getCreditInterestIndex() ) let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) @@ -1944,7 +1944,7 @@ access(all) contract FlowALPv0 { case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance.quantity, + balance.getScaledBalance().quantity, interestIndex: tokenState.getDebitInterestIndex() ) let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) From be3a86f45924a4582cda4fedc943d1d868368d71 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 13:16:43 -0700 Subject: [PATCH 14/19] update balance sheet docs, enforce invariant --- cadence/contracts/FlowALPModels.cdc | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 2d88fa69..d8f0f656 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -519,12 +519,16 @@ access(all) contract FlowALPModels { /// BalanceSheet /// - /// A struct containing a position's overview in terms of its effective collateral and debt + /// A struct containing a position's overview in terms of its per-token effective collateral and debt /// as well as its current health. access(all) struct BalanceSheet { + /// Tracks effective collateral on a per-token basis. + /// A token either has a balance here, or in effectiveDebtByToken, but not both. access(all) let effectiveCollateralByToken: {Type: UFix128} + /// Tracks effective debt on a per-token basis. + /// A token either has a balance here, or in effectiveCollateralByToken, but not both. access(all) let effectiveDebtByToken: {Type: UFix128} /// Aggregate summary of the balance sheet (totals + health). @@ -532,10 +536,12 @@ access(all) contract FlowALPModels { /// Effective collateral is a normalized valuation of collateral deposited into this position, denominated in $. /// In combination with effective debt, this determines how much additional debt can be taken out by this position. + /// This field is the sum of values in effectiveCollateralByToken. access(all) let effectiveCollateral: UFix128 /// Effective debt is a normalized valuation of debt withdrawn against this position, denominated in $. /// In combination with effective collateral, this determines how much additional debt can be taken out by this position. + /// This field is the sum of values in effectiveDebtByToken. access(all) let effectiveDebt: UFix128 /// The health of the related position @@ -545,6 +551,11 @@ access(all) contract FlowALPModels { effectiveCollateral: {Type: UFix128}, effectiveDebt: {Type: UFix128} ) { + // Enforce single balance per token invariant: if a type appears in one map, it must not appear in the other. + for collateralType in effectiveCollateral.keys { + assert(effectiveDebt[collateralType] == nil) + } + self.effectiveCollateralByToken = effectiveCollateral self.effectiveDebtByToken = effectiveDebt self.summary = HealthStatement( @@ -556,16 +567,6 @@ access(all) contract FlowALPModels { self.health = self.summary.health } - /// Returns the per-token effective collateral map. - access(all) view fun getEffectiveCollateralByToken(): {Type: UFix128} { - return self.effectiveCollateralByToken - } - - /// Returns the per-token effective debt map. - access(all) view fun getEffectiveDebtByToken(): {Type: UFix128} { - return self.effectiveDebtByToken - } - /// Returns a new BalanceSheet with one token's effective balance replaced. /// The balance direction determines whether the value goes into the collateral or debt map. /// A zero-quantity balance removes the token from both maps. From 1ec2a4a34aaab886ad0a3707c548922d489beb6a Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 13:51:34 -0700 Subject: [PATCH 15/19] Simplify health computation signatures with TokenSnapshot and shared trueBalance helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add TokenSnapshot.trueBalance helper to deduplicate scaled→true balance conversion. Refactor computeRequiredDepositForHealth and computeAvailableWithdrawal from 8 params to 4 by accepting TokenSnapshot instead of individual scalars. Remove debug logging, rename variables to generic names, and rename balanceSheet→initialBalanceSheet. Co-Authored-By: Claude Opus 4.6 (1M context) --- cadence/contracts/FlowALPHealth.cdc | 202 ++++++++-------------------- cadence/contracts/FlowALPModels.cdc | 23 ++-- cadence/contracts/FlowALPv0.cdc | 81 ++++++----- 3 files changed, 111 insertions(+), 195 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index aaee4a26..1b5c7d32 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -17,14 +17,14 @@ access(all) contract FlowALPHealth { /// @param tokenSnapshot: Snapshot of the withdrawn token's price, interest indices, and risk params /// @return A new BalanceSheet reflecting the effective collateral and debt after the withdrawal access(account) fun computeAdjustedBalancesAfterWithdrawal( - balanceSheet: FlowALPModels.BalanceSheet, + initialBalanceSheet: FlowALPModels.BalanceSheet, withdrawBalance: FlowALPModels.InternalBalance?, withdrawType: Type, withdrawAmount: UFix64, tokenSnapshot: FlowALPModels.TokenSnapshot ): FlowALPModels.BalanceSheet { if withdrawAmount == 0.0 { - return balanceSheet + return initialBalanceSheet } let withdrawAmountU = UFix128(withdrawAmount) @@ -41,7 +41,7 @@ access(all) contract FlowALPHealth { // Compute the effective collateral or debt, and return the updated balance sheet. let effectiveBalance = tokenSnapshot.effectiveBalance(balance: trueBalanceAfterWithdrawal) - return balanceSheet.withReplacedTokenBalance( + return initialBalanceSheet.withReplacedTokenBalance( tokenType: withdrawType, effectiveBalance: effectiveBalance ) @@ -64,85 +64,55 @@ access(all) contract FlowALPHealth { ): FlowALPModels.Balance { // A nil input balance means the initial balance is zero. let initialBalance = maybeInitialBalance ?? FlowALPModels.makeZeroInternalBalance() - - let currentDirection = initialBalance.getScaledBalance().direction - let scaledBalance = initialBalance.getScaledBalance().quantity - - // Since the initial balance is the internal representation, we scale to true balance first - let interestIndex = currentDirection == FlowALPModels.BalanceDirection.Credit - ? tokenSnapshot.creditIndex - : tokenSnapshot.debitIndex - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, interestIndex: interestIndex - ) + let trueBal = tokenSnapshot.trueBalance(balance: initialBalance) // Same direction — delta reinforces the current balance. - if currentDirection == delta.direction { + if trueBal.direction == delta.direction { return FlowALPModels.Balance( - direction: currentDirection, - quantity: trueBalance + delta.quantity + direction: trueBal.direction, + quantity: trueBal.quantity + delta.quantity ) } // Opposite direction — delta offsets the current balance, possibly flipping. - if trueBalance >= delta.quantity { + if trueBal.quantity >= delta.quantity { // delta decreases balance but does not flip sign return FlowALPModels.Balance( - direction: currentDirection, - quantity: trueBalance - delta.quantity + direction: trueBal.direction, + quantity: trueBal.quantity - delta.quantity ) } else { // delta flips sign of balance return FlowALPModels.Balance( direction: delta.direction, - quantity: delta.quantity - trueBalance + quantity: delta.quantity - trueBal.quantity ) } } /// Computes the amount of a given token that must be deposited to bring a position to a target health. /// - // TODO(jord): ~100-line function - consider refactoring /// This function handles the case where the deposit token may have an existing debit (debt) balance. /// If so, the deposit first pays down debt before accumulating as collateral. The computation /// determines the minimum deposit required to reach the target health, accounting for both /// debt repayment and collateral accumulation as needed. /// /// @param depositBalance: The position's existing balance for the deposit token, if any - /// @param depositDebitInterestIndex: The debit interest index for the deposit token - /// @param depositPrice: The oracle price of the deposit token - /// @param depositBorrowFactor: The borrow factor applied to debt in the deposit token - /// @param depositCollateralFactor: The collateral factor applied to collateral in the deposit token - /// @param adjusted: The position's current health statement (post any prior withdrawal) + /// @param depositSnapshot: Snapshot of the deposit token's price, interest indices, and risk params + /// @param initialHealthStatement: The position's current health statement (post any prior withdrawal) /// @param targetHealth: The target health ratio to achieve - /// @param isDebugLogging: Whether to emit debug log messages /// @return The amount of tokens (in UFix64) required to reach the target health access(account) fun computeRequiredDepositForHealth( depositBalance: FlowALPModels.InternalBalance?, - depositDebitInterestIndex: UFix128, - depositPrice: UFix128, - depositBorrowFactor: UFix128, - depositCollateralFactor: UFix128, - adjusted: FlowALPModels.HealthStatement, - targetHealth: UFix128, - isDebugLogging: Bool + depositSnapshot: FlowALPModels.TokenSnapshot, + initialHealthStatement: FlowALPModels.HealthStatement, + targetHealth: UFix128 ): UFix64 { - let effectiveCollateralAfterWithdrawal = adjusted.effectiveCollateral - var effectiveDebtAfterWithdrawal = adjusted.effectiveDebt - if isDebugLogging { - log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") - log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") - } + let initialEffectiveCollateral = initialHealthStatement.effectiveCollateral + var effectiveDebt = initialHealthStatement.effectiveDebt + var health = initialHealthStatement.health - // We now have new effective collateral and debt values that reflect the proposed withdrawal (if any!) - // Now we can figure out how many of the given token would need to be deposited to bring the position - // to the target health value. - var healthAfterWithdrawal = adjusted.health - if isDebugLogging { - log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") - } - - if healthAfterWithdrawal >= targetHealth { + if health >= targetHealth { // The position is already at or above the target health, so we don't need to deposit anything. return 0.0 } @@ -154,23 +124,19 @@ access(all) contract FlowALPHealth { if maybeBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Debit { // The user has a debt position in the given token, we start by looking at the health impact of paying off // the entire debt. - let debtBalance = maybeBalance!.getScaledBalance().quantity - let trueDebtTokenCount = FlowALPMath.scaledBalanceToTrueBalance( - debtBalance, - interestIndex: depositDebitInterestIndex - ) - let debtEffectiveValue = (depositPrice * trueDebtTokenCount) / depositBorrowFactor + let trueDebtTokenCount = depositSnapshot.trueBalance(balance: maybeBalance!).quantity + let debtEffectiveValue = (depositSnapshot.price * trueDebtTokenCount) / depositSnapshot.risk.getBorrowFactor() - // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebtAfterWithdrawal, + // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebt, // it means we can pay off all debt var effectiveDebtAfterPayment: UFix128 = 0.0 - if debtEffectiveValue <= effectiveDebtAfterWithdrawal { - effectiveDebtAfterPayment = effectiveDebtAfterWithdrawal - debtEffectiveValue + if debtEffectiveValue <= effectiveDebt { + effectiveDebtAfterPayment = effectiveDebt - debtEffectiveValue } // Check what the new health would be if we paid off all of this debt let potentialHealth = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterWithdrawal, + effectiveCollateral: initialEffectiveCollateral, effectiveDebt: effectiveDebtAfterPayment ) @@ -178,13 +144,10 @@ access(all) contract FlowALPHealth { if potentialHealth >= targetHealth { // We can reach the target health by paying off some or all of the debt. We can easily // compute how many units of the token would be needed to reach the target health. - let requiredEffectiveDebt = effectiveDebtAfterWithdrawal - - (effectiveCollateralAfterWithdrawal / targetHealth) + let requiredEffectiveDebt = effectiveDebt + - (initialEffectiveCollateral / targetHealth) // The amount of the token to pay back, in units of the token. - let paybackAmount = (requiredEffectiveDebt * depositBorrowFactor) / depositPrice - if isDebugLogging { - log(" [CONTRACT] paybackAmount: \(paybackAmount)") - } + let paybackAmount = (requiredEffectiveDebt * depositSnapshot.risk.getBorrowFactor()) / depositSnapshot.price return FlowALPMath.toUFix64RoundUp(paybackAmount) } else { // We can pay off the entire debt, but we still need to deposit more to reach the target health. @@ -194,12 +157,12 @@ access(all) contract FlowALPHealth { // debt to reflect that it has been paid off. debtTokenCount = trueDebtTokenCount // Ensure we don't underflow - if debtEffectiveValue <= effectiveDebtAfterWithdrawal { - effectiveDebtAfterWithdrawal = effectiveDebtAfterWithdrawal - debtEffectiveValue + if debtEffectiveValue <= effectiveDebt { + effectiveDebt = effectiveDebt - debtEffectiveValue } else { - effectiveDebtAfterWithdrawal = 0.0 + effectiveDebt = 0.0 } - healthAfterWithdrawal = potentialHealth + health = potentialHealth } } @@ -210,18 +173,12 @@ access(all) contract FlowALPHealth { // We need to increase the effective collateral from its current value to the required value, so we // multiply the required health change by the effective debt, and turn that into a token amount. - let healthChangeU = targetHealth - healthAfterWithdrawal + let healthChangeU = targetHealth - health // TODO: apply the same logic as below to the early return blocks above - let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor + let requiredEffectiveCollateral = (healthChangeU * effectiveDebt) / depositSnapshot.risk.getCollateralFactor() // The amount of the token to deposit, in units of the token. - let collateralTokenCount = requiredEffectiveCollateral / depositPrice - if isDebugLogging { - log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") - log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") - log(" [CONTRACT] debtTokenCount: \(debtTokenCount)") - log(" [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)") - } + let collateralTokenCount = requiredEffectiveCollateral / depositSnapshot.price // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) @@ -241,14 +198,14 @@ access(all) contract FlowALPHealth { /// @param tokenSnapshot: Snapshot of the deposited token's price, interest indices, and risk params /// @return A new BalanceSheet reflecting the effective collateral and debt after the deposit access(account) fun computeAdjustedBalancesAfterDeposit( - balanceSheet: FlowALPModels.BalanceSheet, + initialBalanceSheet: FlowALPModels.BalanceSheet, depositBalance: FlowALPModels.InternalBalance?, depositType: Type, depositAmount: UFix64, tokenSnapshot: FlowALPModels.TokenSnapshot ): FlowALPModels.BalanceSheet { if depositAmount == 0.0 { - return balanceSheet + return initialBalanceSheet } let depositAmountU = UFix128(depositAmount) @@ -265,43 +222,30 @@ access(all) contract FlowALPHealth { // Compute the effective collateral or debt, and return the updated balance sheet. let effectiveBalance = tokenSnapshot.effectiveBalance(balance: after) - return balanceSheet.withReplacedTokenBalance( + return initialBalanceSheet.withReplacedTokenBalance( tokenType: depositType, effectiveBalance: effectiveBalance ) } - // TODO(jord): ~100-line function - consider refactoring /// Computes the maximum amount of a given token that can be withdrawn while maintaining a target health. /// /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any - /// @param withdrawCreditInterestIndex: The credit interest index for the withdrawn token - /// @param withdrawPrice: The oracle price of the withdrawn token - /// @param withdrawCollateralFactor: The collateral factor applied to collateral in the withdrawn token - /// @param withdrawBorrowFactor: The borrow factor applied to debt in the withdrawn token - /// @param adjusted: The position's current health statement (post any prior deposit) + /// @param withdrawSnapshot: Snapshot of the withdrawn token's price, interest indices, and risk params + /// @param initialHealthStatement: The position's current health statement (post any prior deposit) /// @param targetHealth: The minimum health ratio to maintain - /// @param isDebugLogging: Whether to emit debug log messages /// @return The maximum amount of tokens (in UFix64) that can be withdrawn access(account) fun computeAvailableWithdrawal( withdrawBalance: FlowALPModels.InternalBalance?, - withdrawCreditInterestIndex: UFix128, - withdrawPrice: UFix128, - withdrawCollateralFactor: UFix128, - withdrawBorrowFactor: UFix128, - adjusted: FlowALPModels.HealthStatement, - targetHealth: UFix128, - isDebugLogging: Bool + withdrawSnapshot: FlowALPModels.TokenSnapshot, + initialHealthStatement: FlowALPModels.HealthStatement, + targetHealth: UFix128 ): UFix64 { - var effectiveCollateralAfterDeposit = adjusted.effectiveCollateral - let effectiveDebtAfterDeposit = adjusted.effectiveDebt + var effectiveCollateral = initialHealthStatement.effectiveCollateral + let effectiveDebt = initialHealthStatement.effectiveDebt + let initialHealth = initialHealthStatement.health - let healthAfterDeposit = adjusted.health - if isDebugLogging { - log(" [CONTRACT] healthAfterDeposit: \(healthAfterDeposit)") - } - - if healthAfterDeposit <= targetHealth { + if initialHealth <= targetHealth { // The position is already at or below the provided target health, so we can't withdraw anything. return 0.0 } @@ -314,56 +258,33 @@ access(all) contract FlowALPHealth { if maybeBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Credit { // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all // of that collateral - let creditBalance = maybeBalance!.getScaledBalance().quantity - let trueCredit = FlowALPMath.scaledBalanceToTrueBalance( - creditBalance, - interestIndex: withdrawCreditInterestIndex - ) - let collateralEffectiveValue = (withdrawPrice * trueCredit) * withdrawCollateralFactor + let trueCredit = withdrawSnapshot.trueBalance(balance: maybeBalance!).quantity + let collateralEffectiveValue = (withdrawSnapshot.price * trueCredit) * withdrawSnapshot.risk.getCollateralFactor() // Check what the new health would be if we took out all of this collateral let potentialHealth = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit - collateralEffectiveValue, // ??? - why subtract? - effectiveDebt: effectiveDebtAfterDeposit + effectiveCollateral: effectiveCollateral - collateralEffectiveValue, + effectiveDebt: effectiveDebt ) // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. if potentialHealth <= targetHealth { // We will hit the health target before using up all of the withdraw token credit. We can easily // compute how many units of the token would bring the position down to the target health. - // We will hit the health target before using up all available withdraw credit. - - let availableEffectiveValue = effectiveCollateralAfterDeposit - (targetHealth * effectiveDebtAfterDeposit) - if isDebugLogging { - log(" [CONTRACT] availableEffectiveValue: \(availableEffectiveValue)") - } + let availableEffectiveValue = effectiveCollateral - (targetHealth * effectiveDebt) // The amount of the token we can take using that amount of health - let availableTokenCount = (availableEffectiveValue / withdrawCollateralFactor) / withdrawPrice - if isDebugLogging { - log(" [CONTRACT] availableTokenCount: \(availableTokenCount)") - } + let availableTokenCount = (availableEffectiveValue / withdrawSnapshot.risk.getCollateralFactor()) / withdrawSnapshot.price return FlowALPMath.toUFix64RoundDown(availableTokenCount) } else { // We can flip this credit position into a debit position, before hitting the target health. - // We have logic below that can determine health changes for debit positions. We've copied it here - // with an added handling for the case where the health after deposit is an edgecase collateralTokenCount = trueCredit - effectiveCollateralAfterDeposit = effectiveCollateralAfterDeposit - collateralEffectiveValue - if isDebugLogging { - log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - } + effectiveCollateral = effectiveCollateral - collateralEffectiveValue // We can calculate the available debt increase that would bring us to the target health - let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit - let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if isDebugLogging { - log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") - log(" [CONTRACT] availableTokens: \(availableTokens)") - log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") - } + let availableDebtIncrease = (effectiveCollateral / targetHealth) - effectiveDebt + let availableTokens = (availableDebtIncrease * withdrawSnapshot.risk.getBorrowFactor()) / withdrawSnapshot.price return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) } @@ -373,13 +294,8 @@ access(all) contract FlowALPHealth { // token, or we've accounted for the credit balance and adjusted the effective collateral above. // We can calculate the available debt increase that would bring us to the target health - let availableDebtIncrease = (effectiveCollateralAfterDeposit / targetHealth) - effectiveDebtAfterDeposit - let availableTokens = (availableDebtIncrease * withdrawBorrowFactor) / withdrawPrice - if isDebugLogging { - log(" [CONTRACT] availableDebtIncrease: \(availableDebtIncrease)") - log(" [CONTRACT] availableTokens: \(availableTokens)") - log(" [CONTRACT] availableTokens + collateralTokenCount: \(availableTokens + collateralTokenCount)") - } + let availableDebtIncrease = (effectiveCollateral / targetHealth) - effectiveDebt + let availableTokens = (availableDebtIncrease * withdrawSnapshot.risk.getBorrowFactor()) / withdrawSnapshot.price return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) } diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index d8f0f656..9238e9bc 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -393,6 +393,16 @@ access(all) contract FlowALPModels { return FlowALPMath.effectiveCollateral(credit: creditBalance, price: self.price, collateralFactor: self.risk.getCollateralFactor()) } + /// Returns the true balance for the given internal (scaled) balance, accounting for accrued interest. + access(all) fun trueBalance(balance: InternalBalance): Balance { + let scaled = balance.getScaledBalance() + let interestIndex = scaled.direction == BalanceDirection.Credit + ? self.creditIndex + : self.debitIndex + let trueQty = FlowALPMath.scaledBalanceToTrueBalance(scaled.quantity, interestIndex: interestIndex) + return Balance(direction: scaled.direction, quantity: trueQty) + } + /// Returns the effective value (collateral or debt) for the given balance, based on its direction. access(all) fun effectiveBalance(balance: Balance): Balance { if balance.quantity == 0.0 { @@ -445,21 +455,12 @@ access(all) contract FlowALPModels { /// Returns the true balance of the given token in this position, accounting for interest. /// Returns balance 0.0 if the position has no balance stored for the given token. - access(all) view fun trueBalance(ofToken: Type): UFix128 { + access(all) fun trueBalance(ofToken: Type): UFix128 { if let balance = self.balances[ofToken] { if let tokenSnapshot = self.snapshots[ofToken] { - switch balance.getScaledBalance().direction { - case BalanceDirection.Debit: - return FlowALPMath.scaledBalanceToTrueBalance( - balance.getScaledBalance().quantity, interestIndex: tokenSnapshot.getDebitIndex()) - case BalanceDirection.Credit: - return FlowALPMath.scaledBalanceToTrueBalance( - balance.getScaledBalance().quantity, interestIndex: tokenSnapshot.getCreditIndex()) - } - panic("unreachable") + return tokenSnapshot.trueBalance(balance: balance).quantity } } - // If the token doesn't exist in the position, the balance is 0 return 0.0 } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index e41e4588..ec250ad5 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -678,7 +678,7 @@ access(all) contract FlowALPv0 { let position = self._borrowPosition(pid: pid) let adjusted = self.computeAdjustedBalancesAfterWithdrawal( - balanceSheet: balanceSheet, + initialBalanceSheet: balanceSheet, position: position, withdrawType: withdrawType, withdrawAmount: withdrawAmount @@ -687,15 +687,14 @@ access(all) contract FlowALPv0 { return self.computeRequiredDepositForHealth( position: position, depositType: depositType, - withdrawType: withdrawType, - adjusted: adjusted.summary, + initialHealthStatement: adjusted.summary, targetHealth: targetHealth ) } // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( - balanceSheet: FlowALPModels.BalanceSheet, + initialBalanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, withdrawType: Type, withdrawAmount: UFix64 @@ -712,7 +711,7 @@ access(all) contract FlowALPv0 { ) return FlowALPHealth.computeAdjustedBalancesAfterWithdrawal( - balanceSheet: balanceSheet, + initialBalanceSheet: initialBalanceSheet, withdrawBalance: position.getBalance(withdrawType), withdrawType: withdrawType, withdrawAmount: withdrawAmount, @@ -724,25 +723,25 @@ access(all) contract FlowALPv0 { access(self) fun computeRequiredDepositForHealth( position: &{FlowALPModels.InternalPosition}, depositType: Type, - withdrawType: Type, - adjusted: FlowALPModels.HealthStatement, + initialHealthStatement: FlowALPModels.HealthStatement, targetHealth: UFix128 ): UFix64 { - let depositBalance = position.getBalance(depositType) - var depositDebitInterestIndex: UFix128 = 1.0 - if depositBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Debit { - depositDebitInterestIndex = self._borrowUpdatedTokenState(type: depositType).getDebitInterestIndex() - } + let tokenState = self._borrowUpdatedTokenState(type: depositType) + let depositSnapshot = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)) + ) + ) return FlowALPHealth.computeRequiredDepositForHealth( - depositBalance: depositBalance, - depositDebitInterestIndex: depositDebitInterestIndex, - depositPrice: UFix128(self.config.getPriceOracle().price(ofToken: depositType)!), - depositBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: depositType)), - depositCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: depositType)), - adjusted: adjusted, - targetHealth: targetHealth, - isDebugLogging: self.config.isDebugLogging() + depositBalance: position.getBalance(depositType), + depositSnapshot: depositSnapshot, + initialHealthStatement: initialHealthStatement, + targetHealth: targetHealth ) } @@ -786,7 +785,7 @@ access(all) contract FlowALPv0 { let position = self._borrowPosition(pid: pid) let adjusted = self.computeAdjustedBalancesAfterDeposit( - balanceSheet: balanceSheet, + initialBalanceSheet: balanceSheet, position: position, depositType: depositType, depositAmount: depositAmount @@ -795,14 +794,14 @@ access(all) contract FlowALPv0 { return self.computeAvailableWithdrawal( position: position, withdrawType: withdrawType, - adjusted: adjusted.summary, + initialHealthStatement: adjusted.summary, targetHealth: targetHealth ) } // Helper function to compute balances after deposit access(self) fun computeAdjustedBalancesAfterDeposit( - balanceSheet: FlowALPModels.BalanceSheet, + initialBalanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, depositType: Type, depositAmount: UFix64 @@ -819,7 +818,7 @@ access(all) contract FlowALPv0 { ) return FlowALPHealth.computeAdjustedBalancesAfterDeposit( - balanceSheet: balanceSheet, + initialBalanceSheet: initialBalanceSheet, depositBalance: position.getBalance(depositType), depositType: depositType, depositAmount: depositAmount, @@ -828,28 +827,28 @@ access(all) contract FlowALPv0 { } // Helper function to compute available withdrawal - // TODO(jord): ~100-line function - consider refactoring access(self) fun computeAvailableWithdrawal( position: &{FlowALPModels.InternalPosition}, withdrawType: Type, - adjusted: FlowALPModels.HealthStatement, + initialHealthStatement: FlowALPModels.HealthStatement, targetHealth: UFix128 ): UFix64 { - let withdrawBalance = position.getBalance(withdrawType) - var withdrawCreditInterestIndex: UFix128 = 1.0 - if withdrawBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Credit { - withdrawCreditInterestIndex = self._borrowUpdatedTokenState(type: withdrawType).getCreditInterestIndex() - } + let tokenState = self._borrowUpdatedTokenState(type: withdrawType) + let withdrawSnapshot = FlowALPModels.TokenSnapshot( + price: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), + credit: tokenState.getCreditInterestIndex(), + debit: tokenState.getDebitInterestIndex(), + risk: FlowALPModels.RiskParamsImplv1( + collateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), + borrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)) + ) + ) return FlowALPHealth.computeAvailableWithdrawal( - withdrawBalance: withdrawBalance, - withdrawCreditInterestIndex: withdrawCreditInterestIndex, - withdrawPrice: UFix128(self.config.getPriceOracle().price(ofToken: withdrawType)!), - withdrawCollateralFactor: UFix128(self.config.getCollateralFactor(tokenType: withdrawType)), - withdrawBorrowFactor: UFix128(self.config.getBorrowFactor(tokenType: withdrawType)), - adjusted: adjusted, - targetHealth: targetHealth, - isDebugLogging: self.config.isDebugLogging() + withdrawBalance: position.getBalance(withdrawType), + withdrawSnapshot: withdrawSnapshot, + initialHealthStatement: initialHealthStatement, + targetHealth: targetHealth ) } @@ -858,7 +857,7 @@ access(all) contract FlowALPv0 { let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) let position = self._borrowPosition(pid: pid) let adjusted = self.computeAdjustedBalancesAfterDeposit( - balanceSheet: balanceSheet, + initialBalanceSheet: balanceSheet, position: position, depositType: type, depositAmount: amount @@ -874,7 +873,7 @@ access(all) contract FlowALPv0 { let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) let position = self._borrowPosition(pid: pid) let adjusted = self.computeAdjustedBalancesAfterWithdrawal( - balanceSheet: balanceSheet, + initialBalanceSheet: balanceSheet, position: position, withdrawType: type, withdrawAmount: amount From 3dd2c2b8b22765398b8b8658e379f48a96cc7951 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 17:30:36 -0700 Subject: [PATCH 16/19] refactor "delta for target health" functions --- cadence/contracts/FlowALPHealth.cdc | 313 ++++++++++++++++------------ cadence/contracts/FlowALPv0.cdc | 16 +- cadence/lib/FlowALPMath.cdc | 2 +- 3 files changed, 185 insertions(+), 146 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 1b5c7d32..1728f346 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -90,98 +90,170 @@ access(all) contract FlowALPHealth { } } - /// Computes the amount of a given token that must be deposited to bring a position to a target health. + /// Computes the minimum true balance of a given token T required for a balance sheet to achieve a target health. /// - /// This function handles the case where the deposit token may have an existing debit (debt) balance. - /// If so, the deposit first pays down debt before accumulating as collateral. The computation - /// determines the minimum deposit required to reach the target health, accounting for both - /// debt repayment and collateral accumulation as needed. + /// The result is the minimum-magnitude balance (which may be credit or debit direction) such that, + /// if the token's effective contribution were recomputed from this balance, the overall health + /// would equal the target. When the rest of the balance sheet already exceeds the target health + /// without any contribution from this token, the result is a debit (debt) balance representing the + /// maximum debt the token can carry. Otherwise, it is a credit (collateral) balance representing + /// the minimum collateral needed. /// - /// @param depositBalance: The position's existing balance for the deposit token, if any - /// @param depositSnapshot: Snapshot of the deposit token's price, interest indices, and risk params - /// @param initialHealthStatement: The position's current health statement (post any prior withdrawal) - /// @param targetHealth: The target health ratio to achieve - /// @return The amount of tokens (in UFix64) required to reach the target health - access(account) fun computeRequiredDepositForHealth( - depositBalance: FlowALPModels.InternalBalance?, - depositSnapshot: FlowALPModels.TokenSnapshot, - initialHealthStatement: FlowALPModels.HealthStatement, + /// @param tokenType: The type of the token to solve for (T). + /// @param tokenSnapshot: Snapshot of the token's price, interest indices, and risk params. + /// @param balanceSheet: The position's current balance sheet. + /// @param targetHealth: The target health ratio to achieve. + /// @return The minimum true balance of the token required to achieve the target health. + access(self) fun requiredBalanceForTargetHealth( + tokenType: Type, + tokenSnapshot: FlowALPModels.TokenSnapshot, + balanceSheet: FlowALPModels.BalanceSheet, targetHealth: UFix128 - ): UFix64 { - let initialEffectiveCollateral = initialHealthStatement.effectiveCollateral - var effectiveDebt = initialHealthStatement.effectiveDebt - var health = initialHealthStatement.health + ): FlowALPModels.Balance { + let tokenEffectiveCollateral = balanceSheet.effectiveCollateralByToken[tokenType] ?? 0.0 + let tokenEffectiveDebt = balanceSheet.effectiveDebtByToken[tokenType] ?? 0.0 + // Remove the token's current contribution to isolate everything else. + let Ce_others = balanceSheet.effectiveCollateral - tokenEffectiveCollateral + let De_others = balanceSheet.effectiveDebt - tokenEffectiveDebt + + let price = tokenSnapshot.price + let CF = tokenSnapshot.risk.getCollateralFactor() + let BF = tokenSnapshot.risk.getBorrowFactor() + + // Determine whether the token needs to be collateral or can carry debt. + // H = Ce/De + // H = (Ce_others + Ce_token)/De + // Required credit = (H * edOther - ecOther) / (P * CF) + // If this is negative (or edOther == 0), the token can carry a debit balance instead. + + // Given the health formula H = Ce/De, we find the value for Ce needed for the target health, + // given the effective debt without T's contribution. + let requiredEffectiveCollateral = targetHealth * De_others + if requiredEffectiveCollateral > Ce_others { + // The rest of the balance sheet does not reach target health, so T must have a credit balance + + // The required contribution of T to overall effective collateral (denominated in $) + let targetTokenEffectiveCollateral = requiredEffectiveCollateral - Ce_others + // The required credit balance to achieve this contribution (denominated in T) + // Re-arrange the effective collateral formula Ce=(Nc)(Pc)(Fc) -> Nc=Ce/(Pc*Fc) + let minCredit = targetTokenEffectiveCollateral / (price * CF) + return FlowALPModels.Balance( + direction: FlowALPModels.BalanceDirection.Credit, + quantity: minCredit + ) + } else { + // The rest of the balance sheet already exceeds the target health, leaving room for T-denominated debt + + // The required contribution of T to overall effective debt (denominated in $) + // H = Ce_others/(De_others+De_T) -> solve for De_T + let targetTokenEffectiveDebt = (Ce_others / targetHealth) - De_others + // The required credit balance to achieve this contribution (denominated in T) + // Re-arrange the effective debt formula De=(Nd)(Pd)/(Fd) -> Nd=(De*Fc)/Pc + let maxDebt = targetTokenEffectiveDebt * BF / price + return FlowALPModels.Balance( + direction: FlowALPModels.BalanceDirection.Debit, + quantity: maxDebt + ) + } + } - if health >= targetHealth { - // The position is already at or above the target health, so we don't need to deposit anything. + /// Computes the minimum deposit to bring the initial balance to a target balance. + /// + /// Returns the magnitude of the deposit needed to move from the initial balance to the target balance. + /// If initial is already greater than or equal to target, returns 0. + /// + /// @param initial: The current true balance. + /// @param target: The target true balance. + /// @return The deposit size (always >= 0). + access(self) fun minDepositForTargetBalance( + initial: FlowALPModels.Balance, + target: FlowALPModels.Balance + ): UFix128 { + let Credit = FlowALPModels.BalanceDirection.Credit + let Debit = FlowALPModels.BalanceDirection.Debit + + if target.direction == Credit && initial.direction == Credit { + // Both credit: deposit needed only if target exceeds initial. + return target.quantity > initial.quantity ? target.quantity - initial.quantity : 0.0 + } else if target.direction == Credit && initial.direction == Debit { + // Initial is debit, target is credit: delta must cross zero. + return initial.quantity + target.quantity + } else if target.direction == Debit && initial.direction == Credit { + // Initial already more favorable (credit) than target (debit): no deposit needed. return 0.0 + } else if target.direction == Debit && initial.direction == Debit { + // Both debit: deposit needed only if initial debt exceeds target debt. + return initial.quantity > target.quantity ? initial.quantity - target.quantity : 0.0 } + panic("unreachable") + } - // For situations where the required deposit will BOTH pay off debt and accumulate collateral, we keep - // track of the number of tokens that went towards paying off debt. - var debtTokenCount: UFix128 = 0.0 - let maybeBalance = depositBalance - if maybeBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Debit { - // The user has a debt position in the given token, we start by looking at the health impact of paying off - // the entire debt. - let trueDebtTokenCount = depositSnapshot.trueBalance(balance: maybeBalance!).quantity - let debtEffectiveValue = (depositSnapshot.price * trueDebtTokenCount) / depositSnapshot.risk.getBorrowFactor() - - // Ensure we don't underflow - if debtEffectiveValue is greater than effectiveDebt, - // it means we can pay off all debt - var effectiveDebtAfterPayment: UFix128 = 0.0 - if debtEffectiveValue <= effectiveDebt { - effectiveDebtAfterPayment = effectiveDebt - debtEffectiveValue - } - - // Check what the new health would be if we paid off all of this debt - let potentialHealth = FlowALPMath.healthComputation( - effectiveCollateral: initialEffectiveCollateral, - effectiveDebt: effectiveDebtAfterPayment - ) - - // Does paying off all of the debt reach the target health? Then we're done. - if potentialHealth >= targetHealth { - // We can reach the target health by paying off some or all of the debt. We can easily - // compute how many units of the token would be needed to reach the target health. - let requiredEffectiveDebt = effectiveDebt - - (initialEffectiveCollateral / targetHealth) - // The amount of the token to pay back, in units of the token. - let paybackAmount = (requiredEffectiveDebt * depositSnapshot.risk.getBorrowFactor()) / depositSnapshot.price - return FlowALPMath.toUFix64RoundUp(paybackAmount) - } else { - // We can pay off the entire debt, but we still need to deposit more to reach the target health. - // We have logic below that can determine the collateral deposition required to reach the target health - // from this new health position. Rather than copy that logic here, we fall through into it. But first - // we have to record the amount of tokens that went towards debt payback and adjust the effective - // debt to reflect that it has been paid off. - debtTokenCount = trueDebtTokenCount - // Ensure we don't underflow - if debtEffectiveValue <= effectiveDebt { - effectiveDebt = effectiveDebt - debtEffectiveValue - } else { - effectiveDebt = 0.0 - } - health = potentialHealth - } + /// Computes the maximum withdrawal to bring the initial balance to a target balance. + /// + /// Returns the magnitude of the withdrawal needed to move from the initial balance to the target balance. + /// If initial is already less than or equal to target, returns 0. + /// + /// @param initial: The current true balance. + /// @param target: The target true balance. + /// @return The withdrawal size (always >= 0). + access(self) fun maxWithdrawalForTargetBalance( + initial: FlowALPModels.Balance, + target: FlowALPModels.Balance + ): UFix128 { + let Credit = FlowALPModels.BalanceDirection.Credit + let Debit = FlowALPModels.BalanceDirection.Debit + + if target.direction == Debit && initial.direction == Debit { + // Both debit: withdrawal available only if target debt exceeds initial. + return target.quantity > initial.quantity ? target.quantity - initial.quantity : 0.0 + } else if target.direction == Debit && initial.direction == Credit { + // Initial is credit, target is debit: delta must cross zero. + return initial.quantity + target.quantity + } else if target.direction == Credit && initial.direction == Debit { + // Initial already more unfavorable (debit) than target (credit): no withdrawal available. + return 0.0 + } else if target.direction == Credit && initial.direction == Credit { + // Both credit: withdrawal available only if initial credit exceeds target. + return initial.quantity > target.quantity ? initial.quantity - target.quantity : 0.0 } + panic("unreachable") + } - // At this point, we're either dealing with a position that didn't have a debt position in the deposit - // token, or we've accounted for the debt payoff and adjusted the effective debt above. - // Now we need to figure out how many tokens would need to be deposited (as collateral) to reach the - // target health. We can rearrange the health equation to solve for the required collateral: + /// Computes the amount of a given token that must be deposited to bring a position to a target health. + /// + /// Determines the minimum true balance the token must have to achieve the target health, + /// then computes the credit-direction delta from the current balance to that target balance. + /// The delta represents the required deposit amount. + /// + /// @param initialBalance: The position's existing (scaled) balance for the deposit token, if any. If nil, considered as zero. + /// @param depositType: The type of token being deposited. + /// @param depositSnapshot: Snapshot of the deposit token's price, interest indices, and risk params. + /// @param initialBalanceSheet: The position's current balance sheet. + /// @param targetHealth: The target health ratio to achieve. + /// @return The amount of tokens (in UFix64) required to reach the target health. + access(account) fun computeRequiredDepositForHealth( + initialBalance maybeInitialBalance: FlowALPModels.InternalBalance?, + depositType: Type, + depositSnapshot: FlowALPModels.TokenSnapshot, + initialBalanceSheet: FlowALPModels.BalanceSheet, + targetHealth: UFix128 + ): UFix64 { + if initialBalanceSheet.health >= targetHealth { + return 0.0 + } - // We need to increase the effective collateral from its current value to the required value, so we - // multiply the required health change by the effective debt, and turn that into a token amount. - let healthChangeU = targetHealth - health - // TODO: apply the same logic as below to the early return blocks above - let requiredEffectiveCollateral = (healthChangeU * effectiveDebt) / depositSnapshot.risk.getCollateralFactor() + let requiredBalance = self.requiredBalanceForTargetHealth( + tokenType: depositType, + tokenSnapshot: depositSnapshot, + balanceSheet: initialBalanceSheet, + targetHealth: targetHealth + ) - // The amount of the token to deposit, in units of the token. - let collateralTokenCount = requiredEffectiveCollateral / depositSnapshot.price + let initialBalance = maybeInitialBalance ?? FlowALPModels.makeZeroInternalBalance() + let currentTrueBalance = depositSnapshot.trueBalance(balance: initialBalance) - // debtTokenCount is the number of tokens that went towards debt, zero if there was no debt. - return FlowALPMath.toUFix64Round(collateralTokenCount + debtTokenCount) + let delta = self.minDepositForTargetBalance(initial: currentTrueBalance, target: requiredBalance) + return FlowALPMath.toUFix64RoundUp(delta) } /// Computes adjusted effective collateral and debt after a hypothetical deposit. @@ -230,73 +302,38 @@ access(all) contract FlowALPHealth { /// Computes the maximum amount of a given token that can be withdrawn while maintaining a target health. /// - /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any - /// @param withdrawSnapshot: Snapshot of the withdrawn token's price, interest indices, and risk params - /// @param initialHealthStatement: The position's current health statement (post any prior deposit) - /// @param targetHealth: The minimum health ratio to maintain - /// @return The maximum amount of tokens (in UFix64) that can be withdrawn + /// Determines the minimum true balance the token must have to maintain the target health, + /// then computes the debit-direction delta from the current balance to that target balance. + /// The delta represents the maximum available withdrawal amount. + /// + /// @param withdrawBalance: The position's existing (scaled) balance for the withdrawn token, if any. If nil, considered as zero. + /// @param withdrawType: The type of token being withdrawn. + /// @param withdrawSnapshot: Snapshot of the withdrawn token's price, interest indices, and risk params. + /// @param initialBalanceSheet: The position's current balance sheet. + /// @param targetHealth: The minimum health ratio to maintain. + /// @return The maximum amount of tokens (in UFix64) that can be withdrawn. access(account) fun computeAvailableWithdrawal( withdrawBalance: FlowALPModels.InternalBalance?, + withdrawType: Type, withdrawSnapshot: FlowALPModels.TokenSnapshot, - initialHealthStatement: FlowALPModels.HealthStatement, + initialBalanceSheet: FlowALPModels.BalanceSheet, targetHealth: UFix128 ): UFix64 { - var effectiveCollateral = initialHealthStatement.effectiveCollateral - let effectiveDebt = initialHealthStatement.effectiveDebt - let initialHealth = initialHealthStatement.health - - if initialHealth <= targetHealth { - // The position is already at or below the provided target health, so we can't withdraw anything. + if initialBalanceSheet.health <= targetHealth { return 0.0 } - // For situations where the available withdrawal will BOTH draw down collateral and create debt, we keep - // track of the number of tokens that are available from collateral - var collateralTokenCount: UFix128 = 0.0 - - let maybeBalance = withdrawBalance - if maybeBalance?.getScaledBalance()?.direction == FlowALPModels.BalanceDirection.Credit { - // The user has a credit position in the withdraw token, we start by looking at the health impact of pulling out all - // of that collateral - let trueCredit = withdrawSnapshot.trueBalance(balance: maybeBalance!).quantity - let collateralEffectiveValue = (withdrawSnapshot.price * trueCredit) * withdrawSnapshot.risk.getCollateralFactor() - - // Check what the new health would be if we took out all of this collateral - let potentialHealth = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateral - collateralEffectiveValue, - effectiveDebt: effectiveDebt - ) - - // Does drawing down all of the collateral go below the target health? Then the max withdrawal comes from collateral only. - if potentialHealth <= targetHealth { - // We will hit the health target before using up all of the withdraw token credit. We can easily - // compute how many units of the token would bring the position down to the target health. - let availableEffectiveValue = effectiveCollateral - (targetHealth * effectiveDebt) - - // The amount of the token we can take using that amount of health - let availableTokenCount = (availableEffectiveValue / withdrawSnapshot.risk.getCollateralFactor()) / withdrawSnapshot.price - - return FlowALPMath.toUFix64RoundDown(availableTokenCount) - } else { - // We can flip this credit position into a debit position, before hitting the target health. - collateralTokenCount = trueCredit - effectiveCollateral = effectiveCollateral - collateralEffectiveValue - - // We can calculate the available debt increase that would bring us to the target health - let availableDebtIncrease = (effectiveCollateral / targetHealth) - effectiveDebt - let availableTokens = (availableDebtIncrease * withdrawSnapshot.risk.getBorrowFactor()) / withdrawSnapshot.price - - return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) - } - } - - // At this point, we're either dealing with a position that didn't have a credit balance in the withdraw - // token, or we've accounted for the credit balance and adjusted the effective collateral above. + let requiredBalance = self.requiredBalanceForTargetHealth( + tokenType: withdrawType, + tokenSnapshot: withdrawSnapshot, + balanceSheet: initialBalanceSheet, + targetHealth: targetHealth + ) - // We can calculate the available debt increase that would bring us to the target health - let availableDebtIncrease = (effectiveCollateral / targetHealth) - effectiveDebt - let availableTokens = (availableDebtIncrease * withdrawSnapshot.risk.getBorrowFactor()) / withdrawSnapshot.price + let initialBalance = withdrawBalance ?? FlowALPModels.makeZeroInternalBalance() + let currentTrueBalance = withdrawSnapshot.trueBalance(balance: initialBalance) - return FlowALPMath.toUFix64RoundDown(availableTokens + collateralTokenCount) + let delta = self.maxWithdrawalForTargetBalance(initial: currentTrueBalance, target: requiredBalance) + return FlowALPMath.toUFix64RoundDown(delta) } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index ec250ad5..86e6e403 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -687,7 +687,7 @@ access(all) contract FlowALPv0 { return self.computeRequiredDepositForHealth( position: position, depositType: depositType, - initialHealthStatement: adjusted.summary, + initialBalanceSheet: adjusted, targetHealth: targetHealth ) } @@ -723,7 +723,7 @@ access(all) contract FlowALPv0 { access(self) fun computeRequiredDepositForHealth( position: &{FlowALPModels.InternalPosition}, depositType: Type, - initialHealthStatement: FlowALPModels.HealthStatement, + initialBalanceSheet: FlowALPModels.BalanceSheet, targetHealth: UFix128 ): UFix64 { let tokenState = self._borrowUpdatedTokenState(type: depositType) @@ -738,9 +738,10 @@ access(all) contract FlowALPv0 { ) return FlowALPHealth.computeRequiredDepositForHealth( - depositBalance: position.getBalance(depositType), + initialBalance: position.getBalance(depositType), + depositType: depositType, depositSnapshot: depositSnapshot, - initialHealthStatement: initialHealthStatement, + initialBalanceSheet: initialBalanceSheet, targetHealth: targetHealth ) } @@ -794,7 +795,7 @@ access(all) contract FlowALPv0 { return self.computeAvailableWithdrawal( position: position, withdrawType: withdrawType, - initialHealthStatement: adjusted.summary, + initialBalanceSheet: adjusted, targetHealth: targetHealth ) } @@ -830,7 +831,7 @@ access(all) contract FlowALPv0 { access(self) fun computeAvailableWithdrawal( position: &{FlowALPModels.InternalPosition}, withdrawType: Type, - initialHealthStatement: FlowALPModels.HealthStatement, + initialBalanceSheet: FlowALPModels.BalanceSheet, targetHealth: UFix128 ): UFix64 { let tokenState = self._borrowUpdatedTokenState(type: withdrawType) @@ -846,8 +847,9 @@ access(all) contract FlowALPv0 { return FlowALPHealth.computeAvailableWithdrawal( withdrawBalance: position.getBalance(withdrawType), + withdrawType: withdrawType, withdrawSnapshot: withdrawSnapshot, - initialHealthStatement: initialHealthStatement, + initialBalanceSheet: initialBalanceSheet, targetHealth: targetHealth ) } diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 56369085..df23979f 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -169,7 +169,7 @@ access(all) contract FlowALPMath { /// Returns the effective debt (denominated in $) for the given debit balance of some token T. /// Effective Debt is defined: - /// De = (Nd)(Pd)(Fd) + /// De = (Nd)(Pd)/(Fd) /// Where: /// De = Effective Debt /// Nd = Number of Debt Tokens From f400d29ad00fe8737f697da7020b3f8e4136c0f6 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 18 Mar 2026 17:32:21 -0700 Subject: [PATCH 17/19] rm comments --- cadence/contracts/FlowALPHealth.cdc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index 1728f346..f074d05e 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -120,12 +120,6 @@ access(all) contract FlowALPHealth { let CF = tokenSnapshot.risk.getCollateralFactor() let BF = tokenSnapshot.risk.getBorrowFactor() - // Determine whether the token needs to be collateral or can carry debt. - // H = Ce/De - // H = (Ce_others + Ce_token)/De - // Required credit = (H * edOther - ecOther) / (P * CF) - // If this is negative (or edOther == 0), the token can carry a debit balance instead. - // Given the health formula H = Ce/De, we find the value for Ce needed for the target health, // given the effective debt without T's contribution. let requiredEffectiveCollateral = targetHealth * De_others From 8f264e25f847470318eac9cae450ddfaa1de01f4 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Mon, 23 Mar 2026 12:57:57 -0700 Subject: [PATCH 18/19] cherry-pick docs from other branch --- cadence/contracts/FlowALPv0.cdc | 135 +++++++++++++++++++------------- 1 file changed, 80 insertions(+), 55 deletions(-) diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 86e6e403..8154ba8c 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -419,49 +419,9 @@ access(all) contract FlowALPv0 { /// to its debt as denominated in the Pool's default token. /// "Effective collateral" means the value of each credit balance times the liquidation threshold /// for that token, i.e. the maximum borrowable amount - // TODO: make this output enumeration of effective debts/collaterals (or provide option that does) access(all) fun positionHealth(pid: UInt64): UFix128 { - let position = self._borrowPosition(pid: pid) - - // Get the position's collateral and debt values in terms of the default token. - var effectiveCollateral: UFix128 = 0.0 - var effectiveDebt: UFix128 = 0.0 - - for type in position.getBalanceKeys() { - let balance = position.getBalance(type)! - let tokenState = self._borrowUpdatedTokenState(type: type) - - let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type)) - let borrowFactor = UFix128(self.config.getBorrowFactor(tokenType: type)) - let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) - switch balance.getScaledBalance().direction { - case FlowALPModels.BalanceDirection.Credit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.getScaledBalance().quantity, - interestIndex: tokenState.getCreditInterestIndex() - ) - - let value = price * trueBalance - let effectiveCollateralValue = value * collateralFactor - effectiveCollateral = effectiveCollateral + effectiveCollateralValue - - case FlowALPModels.BalanceDirection.Debit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.getScaledBalance().quantity, - interestIndex: tokenState.getDebitInterestIndex() - ) - - let value = price * trueBalance - let effectiveDebtValue = value / borrowFactor - effectiveDebt = effectiveDebt + effectiveDebtValue - } - } - - // Calculate the health as the ratio of collateral to debt. - return FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt - ) + let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) + return balanceSheet.health } /// Returns the quantity of funds of a specified token which would need to be deposited @@ -544,7 +504,7 @@ access(all) contract FlowALPv0 { self.isTokenSupported(tokenType: debtType): "Debt token type unsupported: \(debtType.identifier)" self.isTokenSupported(tokenType: seizeType): "Collateral token type unsupported: \(seizeType.identifier)" debtType == repayment.getType(): "Repayment vault does not match debt type: \(debtType.identifier)!=\(repayment.getType().identifier)" - // TODO(jord): liquidation paused / post-pause warm + debtType != seizeType: "Debt and seize types must be different" } post { !self.state.isPositionLocked(pid): "Position is not unlocked" @@ -654,11 +614,18 @@ access(all) contract FlowALPv0 { } /// Returns the quantity of funds of a specified token which would need to be deposited - /// in order to bring the position to the target health - /// assuming we also withdraw a specified amount of another token. + /// in order to bring the position to the target health, assuming we also withdraw a + /// specified amount of another token. /// - /// This function will return 0.0 if the position would already be at or over the target health value + /// Returns 0.0 if the position would already be at or above the target health /// after the proposed withdrawal. + /// + /// @param pid The position ID. + /// @param depositType The token type that would be deposited to restore health. + /// @param targetHealth The desired health to reach (must be >= 1.0). + /// @param withdrawType The token type being withdrawn. + /// @param withdrawAmount The amount of withdrawType being withdrawn. + /// @return The amount of depositType needed to reach targetHealth, or 0.0 if already healthy enough. access(all) fun fundsRequiredForTargetHealthAfterWithdrawing( pid: UInt64, depositType: Type, @@ -692,7 +659,14 @@ access(all) contract FlowALPv0 { ) } - // TODO: documentation + /// Computes the effective collateral and debt after a hypothetical withdrawal, + /// accounting for whether the withdrawal reduces credit or increases debt. + /// + /// @param balanceSheet The position's current balance sheet. + /// @param position The position reference. + /// @param withdrawType The token type being withdrawn. + /// @param withdrawAmount The amount being withdrawn. + /// @return An adjusted BalanceSheet reflecting post-withdrawal effective values. access(self) fun computeAdjustedBalancesAfterWithdrawal( initialBalanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, @@ -719,7 +693,17 @@ access(all) contract FlowALPv0 { ) } - // TODO: documentation + /// Computes the deposit amount needed to bring effective values to the target health. + /// Accounts for whether the deposit reduces debt or adds collateral based on the + /// position's current balance direction for the deposit token. + /// + /// @param position The position reference. + /// @param depositType The token type being deposited. + /// @param withdrawType The token type being withdrawn (used for context). + /// @param effectiveCollateral The effective collateral after the proposed withdrawal. + /// @param effectiveDebt The effective debt after the proposed withdrawal. + /// @param targetHealth The desired health to achieve. + /// @return The amount of depositType needed, or 0.0 if already at or above target. access(self) fun computeRequiredDepositForHealth( position: &{FlowALPModels.InternalPosition}, depositType: Type, @@ -748,6 +732,12 @@ access(all) contract FlowALPv0 { /// Returns the quantity of the specified token that could be withdrawn /// while still keeping the position's health at or above the provided target. + /// Equivalent to fundsAvailableAboveTargetHealthAfterDepositing with depositAmount=0. + /// + /// @param pid The position ID. + /// @param type The token type to compute available withdrawal for. + /// @param targetHealth The minimum health to maintain after withdrawal. + /// @return The maximum withdrawable amount of the given token type. access(all) fun fundsAvailableAboveTargetHealth(pid: UInt64, type: Type, targetHealth: UFix128): UFix64 { return self.fundsAvailableAboveTargetHealthAfterDepositing( pid: pid, @@ -761,6 +751,13 @@ access(all) contract FlowALPv0 { /// Returns the quantity of the specified token that could be withdrawn /// while still keeping the position's health at or above the provided target, /// assuming we also deposit a specified amount of another token. + /// + /// @param pid The position ID. + /// @param withdrawType The token type to compute available withdrawal for. + /// @param targetHealth The minimum health to maintain after the withdrawal. + /// @param depositType The token type being deposited alongside the withdrawal. + /// @param depositAmount The amount of depositType being deposited. + /// @return The maximum withdrawable amount of withdrawType. access(all) fun fundsAvailableAboveTargetHealthAfterDepositing( pid: UInt64, withdrawType: Type, @@ -800,7 +797,14 @@ access(all) contract FlowALPv0 { ) } - // Helper function to compute balances after deposit + /// Computes the effective collateral and debt after a hypothetical deposit, + /// accounting for whether the deposit adds collateral or reduces debt. + /// + /// @param balanceSheet The position's current balance sheet. + /// @param position The position reference. + /// @param depositType The token type being deposited. + /// @param depositAmount The amount being deposited. + /// @return An adjusted BalanceSheet reflecting post-deposit effective values. access(self) fun computeAdjustedBalancesAfterDeposit( initialBalanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, @@ -827,7 +831,15 @@ access(all) contract FlowALPv0 { ) } - // Helper function to compute available withdrawal + /// Computes the maximum amount of a token that can be withdrawn while maintaining + /// the target health, given pre-adjusted effective collateral and debt values. + /// + /// @param position The position reference. + /// @param withdrawType The token type being withdrawn. + /// @param effectiveCollateral The effective collateral (already adjusted for any prior deposit). + /// @param effectiveDebt The effective debt (already adjusted for any prior deposit). + /// @param targetHealth The minimum health to maintain after withdrawal. + /// @return The maximum withdrawable amount of withdrawType. access(self) fun computeAvailableWithdrawal( position: &{FlowALPModels.InternalPosition}, withdrawType: Type, @@ -854,7 +866,13 @@ access(all) contract FlowALPv0 { ) } - /// Returns the position's health if the given amount of the specified token were deposited + /// Returns the position's health if the given amount of the specified token were deposited. + /// Accounts for whether the deposit reduces existing debt or adds new collateral. + /// + /// @param pid The position ID. + /// @param type The token type being deposited. + /// @param amount The amount to deposit. + /// @return The projected health after the deposit. access(all) fun healthAfterDeposit(pid: UInt64, type: Type, amount: UFix64): UFix128 { let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) let position = self._borrowPosition(pid: pid) @@ -867,10 +885,17 @@ access(all) contract FlowALPv0 { return adjusted.health } - /// Returns health value of this position if the given amount of the specified token were withdrawn - /// without using the top up source. - /// NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates - /// that the proposed withdrawal would fail (unless a top up source is available and used). + /// Returns the position's health if the given amount of the specified token were withdrawn + /// without using the top-up source. Accounts for whether the withdrawal reduces existing + /// collateral or creates new debt. + /// + /// NOTE: Can return values below 1.0, indicating the withdrawal would fail unless a + /// top-up source is available and used. + /// + /// @param pid The position ID. + /// @param type The token type being withdrawn. + /// @param amount The amount to withdraw. + /// @return The projected health after the withdrawal. access(all) fun healthAfterWithdrawal(pid: UInt64, type: Type, amount: UFix64): UFix128 { let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) let position = self._borrowPosition(pid: pid) From ba9aa2a381a387b67f7b8d11f800b6cee67eb636 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Thu, 26 Mar 2026 17:09:09 -0700 Subject: [PATCH 19/19] Apply suggestions from code review Co-authored-by: Ardit Marku Co-authored-by: Jordan Schalm --- cadence/contracts/FlowALPHealth.cdc | 8 ++++---- cadence/contracts/FlowALPModels.cdc | 2 +- cadence/contracts/FlowALPv0.cdc | 17 +++++++++-------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/cadence/contracts/FlowALPHealth.cdc b/cadence/contracts/FlowALPHealth.cdc index f074d05e..6fdca711 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -10,7 +10,7 @@ access(all) contract FlowALPHealth { /// in the withdrawn token. If the position has collateral in the token, the withdrawal may /// either draw down collateral, or exhaust it entirely and create new debt. /// - /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) + /// @param initialBalanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param withdrawBalance: The position's existing balance for the withdrawn token, if any /// @param withdrawType: The type of token being withdrawn /// @param withdrawAmount: The amount of tokens to withdraw @@ -142,8 +142,8 @@ access(all) contract FlowALPHealth { // H = Ce_others/(De_others+De_T) -> solve for De_T let targetTokenEffectiveDebt = (Ce_others / targetHealth) - De_others // The required credit balance to achieve this contribution (denominated in T) - // Re-arrange the effective debt formula De=(Nd)(Pd)/(Fd) -> Nd=(De*Fc)/Pc - let maxDebt = targetTokenEffectiveDebt * BF / price + // Re-arrange the effective debt formula De=(Nd)(Pd)/(Fd) -> Nd=(De*Fd)/Pd + let maxDebt = (targetTokenEffectiveDebt * BF) / price return FlowALPModels.Balance( direction: FlowALPModels.BalanceDirection.Debit, quantity: maxDebt @@ -257,7 +257,7 @@ access(all) contract FlowALPHealth { /// in the deposited token. If the position has debt in the token, the deposit may /// either pay down debt, or pay it off entirely and create new collateral. /// - /// @param balanceSheet: The position's current effective collateral and debt (with per-token maps) + /// @param initialBalanceSheet: The position's current effective collateral and debt (with per-token maps) /// @param depositBalance: The position's existing balance for the deposited token, if any /// @param depositType: The type of token being deposited /// @param depositAmount: The amount of tokens to deposit diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 61b568ff..108bb624 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -554,7 +554,7 @@ access(all) contract FlowALPModels { ) { // Enforce single balance per token invariant: if a type appears in one map, it must not appear in the other. for collateralType in effectiveCollateral.keys { - assert(effectiveDebt[collateralType] == nil) + assert(effectiveDebt[collateralType] == nil, message: "cannot construct BalanceShet: observed both credit and debit balance for \(collateralType)") } self.effectiveCollateralByToken = effectiveCollateral diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 2d28e136..a06b596a 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -692,7 +692,7 @@ access(all) contract FlowALPv0 { /// Computes the effective collateral and debt after a hypothetical withdrawal, /// accounting for whether the withdrawal reduces credit or increases debt. /// - /// @param balanceSheet The position's current balance sheet. + /// @param initialBalanceSheet The position's current balance sheet. /// @param position The position reference. /// @param withdrawType The token type being withdrawn. /// @param withdrawAmount The amount being withdrawn. @@ -729,9 +729,7 @@ access(all) contract FlowALPv0 { /// /// @param position The position reference. /// @param depositType The token type being deposited. - /// @param withdrawType The token type being withdrawn (used for context). - /// @param effectiveCollateral The effective collateral after the proposed withdrawal. - /// @param effectiveDebt The effective debt after the proposed withdrawal. + /// @param initialBalanceSheet The position's balance sheet prior to the deposit /// @param targetHealth The desired health to achieve. /// @return The amount of depositType needed, or 0.0 if already at or above target. access(self) fun computeRequiredDepositForHealth( @@ -830,7 +828,7 @@ access(all) contract FlowALPv0 { /// Computes the effective collateral and debt after a hypothetical deposit, /// accounting for whether the deposit adds collateral or reduces debt. /// - /// @param balanceSheet The position's current balance sheet. + /// @param initialBalanceSheet The position's current balance sheet. /// @param position The position reference. /// @param depositType The token type being deposited. /// @param depositAmount The amount being deposited. @@ -865,9 +863,7 @@ access(all) contract FlowALPv0 { /// the target health, given pre-adjusted effective collateral and debt values. /// /// @param position The position reference. - /// @param withdrawType The token type being withdrawn. - /// @param effectiveCollateral The effective collateral (already adjusted for any prior deposit). - /// @param effectiveDebt The effective debt (already adjusted for any prior deposit). + /// @param initialBalanceSheet The position's current balance sheet /// @param targetHealth The minimum health to maintain after withdrawal. /// @return The maximum withdrawable amount of withdrawType. access(self) fun computeAvailableWithdrawal( @@ -2020,6 +2016,11 @@ access(all) contract FlowALPv0 { let balance = position.getBalance(type)! let tokenState = self._borrowUpdatedTokenState(type: type) let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!) + let oracle = self.config.getPriceOracle() + for type in position.getBalanceKeys() { + let balance = position.getBalance(type)! + let tokenState = self._borrowUpdatedTokenState(type: type) + let price = UFix128(oracle.price(ofToken: type)!) switch balance.getScaledBalance().direction { case FlowALPModels.BalanceDirection.Credit: