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..6fdca711 100644 --- a/cadence/contracts/FlowALPHealth.cdc +++ b/cadence/contracts/FlowALPHealth.cdc @@ -10,432 +10,324 @@ 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 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 - /// @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 + /// @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, - withdrawPrice: UFix128, - withdrawBorrowFactor: UFix128, - withdrawCollateralFactor: UFix128, - withdrawCreditInterestIndex: UFix128, - isDebugLogging: Bool + tokenSnapshot: FlowALPModels.TokenSnapshot ): FlowALPModels.BalanceSheet { - var effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - var effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt - if withdrawAmount == 0.0 { - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal - ) - } - if isDebugLogging { - log(" [CONTRACT] effectiveCollateralAfterWithdrawal: \(effectiveCollateralAfterWithdrawal)") - log(" [CONTRACT] effectiveDebtAfterWithdrawal: \(effectiveDebtAfterWithdrawal)") + return initialBalanceSheet } 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 - - 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 * withdrawPrice2) / withdrawBorrowFactor2 - 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. - 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 * withdrawPrice2) * collateralFactor - } else { - // The withdrawal will wipe out all of the collateral, and create some debt. - effectiveDebtAfterWithdrawal = balanceSheet.effectiveDebt + - ((withdrawAmountU - trueCollateral) * withdrawPrice2) / withdrawBorrowFactor2 - effectiveCollateralAfterWithdrawal = balanceSheet.effectiveCollateral - - (trueCollateral * withdrawPrice2) * collateralFactor - } - } + // Compute the post-withdrawal true balance and direction. + let trueBalanceAfterWithdrawal = self.trueBalanceAfterDelta( + balance: withdrawBalance, + delta: FlowALPModels.Balance( + direction: FlowALPModels.BalanceDirection.Debit, + quantity: withdrawAmountU + ), + tokenSnapshot: tokenSnapshot + ) - return FlowALPModels.BalanceSheet( - effectiveCollateral: effectiveCollateralAfterWithdrawal, - effectiveDebt: effectiveDebtAfterWithdrawal + // Compute the effective collateral or debt, and return the updated balance sheet. + let effectiveBalance = tokenSnapshot.effectiveBalance(balance: trueBalanceAfterWithdrawal) + return initialBalanceSheet.withReplacedTokenBalance( + tokenType: withdrawType, + effectiveBalance: effectiveBalance ) } - /// Computes the amount of a given token that must be deposited to bring a position to a target health. + /// Computes the resulting true balance after applying a signed delta to an InternalBalance. /// - /// 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 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 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 effectiveCollateral: The position's current effective collateral (post any prior withdrawal) - /// @param effectiveDebt: The position's current effective debt (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, - targetHealth: UFix128, - isDebugLogging: Bool - ): UFix64 { - let effectiveCollateralAfterWithdrawal = effectiveCollateral - var effectiveDebtAfterWithdrawal = 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 - ) - if isDebugLogging { - log(" [CONTRACT] healthAfterWithdrawal: \(healthAfterWithdrawal)") + /// @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 maybeInitialBalance: FlowALPModels.InternalBalance?, + delta: FlowALPModels.Balance, + tokenSnapshot: FlowALPModels.TokenSnapshot + ): FlowALPModels.Balance { + // A nil input balance means the initial balance is zero. + let initialBalance = maybeInitialBalance ?? FlowALPModels.makeZeroInternalBalance() + let trueBal = tokenSnapshot.trueBalance(balance: initialBalance) + + // Same direction — delta reinforces the current balance. + if trueBal.direction == delta.direction { + return FlowALPModels.Balance( + direction: trueBal.direction, + quantity: trueBal.quantity + delta.quantity + ) } - if healthAfterWithdrawal >= targetHealth { - // The position is already at or above the target health, so we don't need to deposit anything. - return 0.0 + // Opposite direction — delta offsets the current balance, possibly flipping. + if trueBal.quantity >= delta.quantity { + // delta decreases balance but does not flip sign + return FlowALPModels.Balance( + direction: trueBal.direction, + quantity: trueBal.quantity - delta.quantity + ) + } else { + // delta flips sign of balance + return FlowALPModels.Balance( + direction: delta.direction, + quantity: delta.quantity - trueBal.quantity + ) } + } - // 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?.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, - interestIndex: depositDebitInterestIndex + /// Computes the minimum true balance of a given token T required for a balance sheet to achieve a target health. + /// + /// 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 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 + ): 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() + + // 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 ) - 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 + } 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*Fd)/Pd + let maxDebt = (targetTokenEffectiveDebt * BF) / price + return FlowALPModels.Balance( + direction: FlowALPModels.BalanceDirection.Debit, + quantity: maxDebt ) + } + } - // 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)") - } + /// 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") + } - 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 { - effectiveDebtAfterWithdrawal = 0.0 - } - healthAfterWithdrawal = 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 - healthAfterWithdrawal - // TODO: apply the same logic as below to the early return blocks above - let requiredEffectiveCollateral = (healthChangeU * effectiveDebtAfterWithdrawal) / depositCollateralFactor + 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 / depositPrice - if isDebugLogging { - log(" [CONTRACT] requiredEffectiveCollateral: \(requiredEffectiveCollateral)") - log(" [CONTRACT] collateralTokenCount: \(collateralTokenCount)") - log(" [CONTRACT] debtTokenCount: \(debtTokenCount)") - log(" [CONTRACT] collateralTokenCount + debtTokenCount: \(collateralTokenCount) + \(debtTokenCount) = \(collateralTokenCount + debtTokenCount)") - } + 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. /// /// 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. + /// 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 + /// @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 - /// @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 + /// @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, - depositPrice: UFix128, - depositBorrowFactor: UFix128, - depositCollateralFactor: UFix128, - depositDebitInterestIndex: UFix128, - isDebugLogging: Bool + tokenSnapshot: FlowALPModels.TokenSnapshot ): 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 initialBalanceSheet } - let depositAmountCasted = UFix128(depositAmount) - let depositPriceCasted = depositPrice - let depositBorrowFactorCasted = depositBorrowFactor - let depositCollateralFactorCasted = depositCollateralFactor - let balance = depositBalance - let direction = balance?.direction ?? FlowALPModels.BalanceDirection.Credit - let scaledBalance = balance?.scaledBalance ?? 0.0 - - 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 - - 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. - let trueDebt = FlowALPMath.scaledBalanceToTrueBalance( - scaledBalance, - interestIndex: depositDebitInterestIndex - ) - if isDebugLogging { - log(" [CONTRACT] trueDebt: \(trueDebt)") - } + let depositAmountU = UFix128(depositAmount) - 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 - } 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 - } - } - if isDebugLogging { - log(" [CONTRACT] effectiveCollateralAfterDeposit: \(effectiveCollateralAfterDeposit)") - log(" [CONTRACT] effectiveDebtAfterDeposit: \(effectiveDebtAfterDeposit)") - } + // Compute the post-deposit true balance and direction. + let after = self.trueBalanceAfterDelta( + balance: depositBalance, + delta: FlowALPModels.Balance( + direction: FlowALPModels.BalanceDirection.Credit, + quantity: depositAmountU + ), + tokenSnapshot: tokenSnapshot + ) - // 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 + // Compute the effective collateral or debt, and return the updated balance sheet. + let effectiveBalance = tokenSnapshot.effectiveBalance(balance: after) + return initialBalanceSheet.withReplacedTokenBalance( + tokenType: depositType, + effectiveBalance: effectiveBalance ) } /// 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. + /// 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 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 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 + /// @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?, - withdrawCreditInterestIndex: UFix128, - withdrawPrice: UFix128, - withdrawCollateralFactor: UFix128, - withdrawBorrowFactor: UFix128, - effectiveCollateral: UFix128, - effectiveDebt: UFix128, - targetHealth: UFix128, - isDebugLogging: Bool + withdrawType: Type, + withdrawSnapshot: FlowALPModels.TokenSnapshot, + initialBalanceSheet: FlowALPModels.BalanceSheet, + targetHealth: UFix128 ): UFix64 { - var effectiveCollateralAfterDeposit = effectiveCollateral - let effectiveDebtAfterDeposit = effectiveDebt - - let healthAfterDeposit = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateralAfterDeposit, - effectiveDebt: effectiveDebtAfterDeposit - ) - 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. + 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?.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, - interestIndex: withdrawCreditInterestIndex - ) - 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? - 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)") - } - - 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)") - } - - // 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)") - } - - 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 = (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 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/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 42f60eb5..108bb624 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -72,14 +72,31 @@ 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 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) { + // Enforce 0-balance convention + if quantity == 0.0 { + self.direction = BalanceDirection.Credit + } else { + self.direction = direction + } + self.quantity = quantity + } + } + /// InternalBalance /// /// 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. @@ -88,15 +105,19 @@ 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(self) 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) + } + + 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 @@ -110,7 +131,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. // @@ -126,7 +147,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) @@ -136,7 +160,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() ) @@ -146,9 +170,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 @@ -159,10 +186,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 @@ -183,7 +212,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. // @@ -199,7 +228,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) @@ -209,7 +241,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() ) @@ -218,9 +250,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 @@ -230,10 +265,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 @@ -244,6 +281,16 @@ 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) + } + + 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. @@ -345,6 +392,36 @@ 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 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 { + 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) @@ -378,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.direction { - case BalanceDirection.Debit: - return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, interestIndex: tokenSnapshot.getDebitIndex()) - case BalanceDirection.Credit: - return FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, 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 } } @@ -406,10 +474,10 @@ access(all) contract FlowALPModels { let balance = view.balances[tokenType]! let snap = view.snapshots[tokenType]! - switch balance.direction { + switch balance.getScaledBalance().direction { case BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.getScaledBalance().quantity, interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal @@ -417,7 +485,7 @@ access(all) contract FlowALPModels { case BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.getScaledBalance().quantity, interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal @@ -430,32 +498,103 @@ access(all) contract FlowALPModels { ) } + /// 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 /// - /// 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). + 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. + /// 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 access(all) let health: UFix128 init( - effectiveCollateral: UFix128, - effectiveDebt: UFix128 + effectiveCollateral: {Type: UFix128}, + effectiveDebt: {Type: UFix128} ) { - self.effectiveCollateral = effectiveCollateral - self.effectiveDebt = effectiveDebt - self.health = FlowALPMath.healthComputation( - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt + // 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, message: "cannot construct BalanceShet: observed both credit and debit balance for \(collateralType)") + } + + self.effectiveCollateralByToken = effectiveCollateral + self.effectiveDebtByToken = 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 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, + effectiveBalance: Balance + ): BalanceSheet { + let newCollateral = self.effectiveCollateralByToken + let newDebt = self.effectiveDebtByToken + + // 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 ) } } diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 224bf8e1..a06b596a 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -79,10 +79,10 @@ access(all) contract FlowALPv0 { let balance = view.balances[tokenType]! let snap = view.snapshots[tokenType]! - switch balance.direction { + switch balance.getScaledBalance().direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.getScaledBalance().quantity, interestIndex: snap.getCreditIndex() ) effectiveCollateralTotal = effectiveCollateralTotal @@ -90,7 +90,7 @@ access(all) contract FlowALPv0 { case FlowALPModels.BalanceDirection.Debit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.getScaledBalance().quantity, interestIndex: snap.getDebitIndex() ) effectiveDebtTotal = effectiveDebtTotal @@ -101,7 +101,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!.getScaledBalance().direction == FlowALPModels.BalanceDirection.Debit { // withdrawing increases debt let numerator = effectiveCollateralTotal let denominatorTarget = numerator / targetHealth @@ -112,7 +112,7 @@ access(all) contract FlowALPv0 { } else { // withdrawing reduces collateral (and may flip into debt beyond zero) let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - withdrawBal!.scaledBalance, + withdrawBal!.getScaledBalance().quantity, interestIndex: withdrawSnap.getCreditIndex() ) let requiredCollateral = effectiveDebtTotal * targetHealth @@ -437,49 +437,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.direction { - case FlowALPModels.BalanceDirection.Credit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, - interestIndex: tokenState.getCreditInterestIndex() - ) - - let value = price * trueBalance - let effectiveCollateralValue = value * collateralFactor - effectiveCollateral = effectiveCollateral + effectiveCollateralValue - - case FlowALPModels.BalanceDirection.Debit: - let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, - 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 @@ -520,15 +480,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.getScaledBalance().quantity, + interestIndex: balance.getScaledBalance().direction == FlowALPModels.BalanceDirection.Credit ? tokenState.getCreditInterestIndex() : tokenState.getDebitInterestIndex() ) balances.append(FlowALPModels.PositionBalance( vaultType: type, - direction: balance.direction, + direction: balance.getScaledBalance().direction, balance: FlowALPMath.toUFix64Round(trueBalance) )) } @@ -575,7 +535,6 @@ access(all) contract FlowALPv0 { 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)" debtType != seizeType: "Debt and seize types must be different" - // TODO(jord): liquidation paused / post-pause warm } post { !self.state.isPositionLocked(pid): "Position is not unlocked" @@ -685,11 +644,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, @@ -709,7 +675,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 @@ -718,69 +684,88 @@ access(all) contract FlowALPv0 { return self.computeRequiredDepositForHealth( position: position, depositType: depositType, - withdrawType: withdrawType, - effectiveCollateral: adjusted.effectiveCollateral, - effectiveDebt: adjusted.effectiveDebt, + initialBalanceSheet: adjusted, targetHealth: targetHealth ) } - // TODO: documentation + /// Computes the effective collateral and debt after a hypothetical withdrawal, + /// accounting for whether the withdrawal reduces credit or increases debt. + /// + /// @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. + /// @return An adjusted BalanceSheet reflecting post-withdrawal effective values. access(self) fun computeAdjustedBalancesAfterWithdrawal( - balanceSheet: FlowALPModels.BalanceSheet, + initialBalanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, 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, + initialBalanceSheet: initialBalanceSheet, + 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, - isDebugLogging: self.config.isDebugLogging() + tokenSnapshot: snapshot ) } - // TODO(jord): ~100-line function - consider refactoring - // TODO: documentation - access(self) fun computeRequiredDepositForHealth( + /// 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 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( position: &{FlowALPModels.InternalPosition}, depositType: Type, - withdrawType: Type, - effectiveCollateral: UFix128, - effectiveDebt: UFix128, + initialBalanceSheet: FlowALPModels.BalanceSheet, targetHealth: UFix128 ): UFix64 { - 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 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)), - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt, - targetHealth: targetHealth, - isDebugLogging: self.config.isDebugLogging() + initialBalance: position.getBalance(depositType), + depositType: depositType, + depositSnapshot: depositSnapshot, + initialBalanceSheet: initialBalanceSheet, + targetHealth: targetHealth ) } /// 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, @@ -794,6 +779,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, @@ -819,7 +811,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 @@ -828,163 +820,118 @@ access(all) contract FlowALPv0 { return self.computeAvailableWithdrawal( position: position, withdrawType: withdrawType, - effectiveCollateral: adjusted.effectiveCollateral, - effectiveDebt: adjusted.effectiveDebt, + initialBalanceSheet: adjusted, targetHealth: targetHealth ) } - // 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 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. + /// @return An adjusted BalanceSheet reflecting post-deposit effective values. access(self) fun computeAdjustedBalancesAfterDeposit( - balanceSheet: FlowALPModels.BalanceSheet, + initialBalanceSheet: FlowALPModels.BalanceSheet, position: &{FlowALPModels.InternalPosition}, 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, + initialBalanceSheet: initialBalanceSheet, + 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 ) } - // Helper function to compute available withdrawal - // TODO(jord): ~100-line function - consider refactoring + /// 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 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( position: &{FlowALPModels.InternalPosition}, withdrawType: Type, - effectiveCollateral: UFix128, - effectiveDebt: UFix128, + initialBalanceSheet: FlowALPModels.BalanceSheet, targetHealth: UFix128 ): UFix64 { - let withdrawBalance = position.getBalance(withdrawType) - var withdrawCreditInterestIndex: UFix128 = 1.0 - if withdrawBalance?.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)), - effectiveCollateral: effectiveCollateral, - effectiveDebt: effectiveDebt, - targetHealth: targetHealth, - isDebugLogging: self.config.isDebugLogging() + withdrawBalance: position.getBalance(withdrawType), + withdrawType: withdrawType, + withdrawSnapshot: withdrawSnapshot, + initialBalanceSheet: initialBalanceSheet, + targetHealth: targetHealth ) } - /// 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) - 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( + initialBalanceSheet: 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 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) - 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( + initialBalanceSheet: balanceSheet, + position: position, + withdrawType: type, + withdrawAmount: amount ) + return adjusted.health } /////////////////////////// @@ -2062,44 +2009,41 @@ access(all) contract FlowALPv0 { 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)!) + 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.direction { + switch balance.getScaledBalance().direction { case FlowALPModels.BalanceDirection.Credit: let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( - balance.scaledBalance, + balance.getScaledBalance().quantity, 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, + balance.getScaledBalance().quantity, 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 ) } diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 1c8fde8d..f65a3330 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) @@ -175,7 +184,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