From 690cb99c906c260a4518559cd986c86c330745f2 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 09:25:12 -0800 Subject: [PATCH 1/5] Add _borrowOrCreateReserveVault helper and simplify reserve access This change introduces the _borrowOrCreateReserveVault helper function from the jord/liquidation branch, which simplifies reserve vault access patterns in the codebase. Changes: - Add _borrowOrCreateReserveVault helper to create reserves on-demand - Use helper in liquidateRepayForSeize for debt reserve deposits - Use helper in internalRepay for debt reserve deposits - Remove redundant reserve creation in liquidateViaDex (handled by internal functions) Co-Authored-By: Claude Sonnet 4.5 --- cadence/contracts/FlowCreditMarket.cdc | 33 +++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 80c5384..30c55a5 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1282,6 +1282,19 @@ access(all) contract FlowCreditMarket { return vaultRef?.balance ?? 0.0 } + /// Returns a reference to the reserve vault for the given type, if the token type is supported. + /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created. + access(self) fun _borrowOrCreateReserveVault(type: Type): &{FungibleToken.Vault} { + pre { + self.isTokenSupported(tokenType: type) + } + if self.reserves[type] == nil { + self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type) + } + let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? + return vaultRef! + } + /// Returns a position's balance available for withdrawal of a given Vault type. /// Phase 0 refactor: compute via pure helpers using a PositionView and TokenSnapshot for the base path. /// When `pullFromTopUpSource` is true and a topUpSource exists, preserve deposit-assisted semantics. @@ -1813,13 +1826,10 @@ access(all) contract FlowCreditMarket { message: "Seize amount below minimum" ) - // Ensure internal reserves exist for seizeType and debtType + // Ensure internal reserve exists for seizeType if self.reserves[seizeType] == nil { self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType) } - if self.reserves[debtType] == nil { - self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) - } // Move repay tokens into reserves (repay vault must exactly match requiredRepay) assert( @@ -1831,7 +1841,7 @@ access(all) contract FlowCreditMarket { message: "Repay vault balance must be at least requiredRepay" ) let toUse <- from.withdraw(amount: quote.requiredRepay) - let debtReserveRef = (&self.reserves[debtType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let debtReserveRef = self._borrowOrCreateReserveVault(type: debtType) debtReserveRef.deposit(from: <-toUse) // Reduce borrower's debt position by repayAmount @@ -1907,14 +1917,6 @@ access(all) contract FlowCreditMarket { } self._assertLiquidationsActive() - // Ensure reserve vaults exist for both tokens - if self.reserves[seizeType] == nil { - self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType) - } - if self.reserves[debtType] == nil { - self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) - } - // Validate position is liquidatable let health = self.positionHealth(pid: pid) assert( @@ -2055,12 +2057,9 @@ access(all) contract FlowCreditMarket { access(self) fun internalRepay(pid: UInt64, from: @{FungibleToken.Vault}): UFix64 { let debtType = from.getType() - if self.reserves[debtType] == nil { - self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) - } let toDeposit <- from let amount = toDeposit.balance - let reserveRef = (&self.reserves[debtType] as &{FungibleToken.Vault}?)! + let reserveRef = self._borrowOrCreateReserveVault(type: debtType) reserveRef.deposit(from: <-toDeposit) let position = self._borrowPosition(pid: pid) let debtState = self._borrowUpdatedTokenState(type: debtType) From c4641775481ebf36132eb2580ff12cd6f757ac5e Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 09:35:14 -0800 Subject: [PATCH 2/5] Enhance documentation and add TODO comments This commit adds comprehensive documentation improvements and TODO comments throughout the codebase to better track technical debt and clarify functionality. Changes: - Enhanced InternalPosition field documentation (targetHealth, minHealth, maxHealth) - Added TODO comments to setDrawDownSink and setTopUpSource - Improved InterestCurve interface documentation with parameter descriptions - Enhanced TokenState totalCreditBalance and totalDebitBalance documentation - Added blank line after balance update helpers comment block - Added TODO comments to unused functions (updateCreditBalance, updateDebitBalance) - Updated updateInterestIndices comment - Added TODO for InterestCurve abstraction improvement - Enhanced PositionView struct documentation for balances and snapshots fields - Added TODO comments to healthFactor and maxWithdraw functions - Added documentation TODOs to helper functions (computeAdjustedBalancesAfterWithdrawal, computeRequiredDepositForHealth, computeAvailableWithdrawal) - Added permission system TODO to createPosition - Added refactoring TODOs to large functions (depositAndPush, withdrawAndPull) - Enhanced rebalancePosition documentation with detailed behavior description - Added TODO for MOET type enforcement in rebalance logic Co-Authored-By: Claude Sonnet 4.5 --- cadence/contracts/FlowCreditMarket.cdc | 62 +++++++++++++++++++++----- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 30c55a5..158e2c5 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -398,13 +398,18 @@ access(all) contract FlowCreditMarket { /// An internal resource used to track deposits, withdrawals, balances, and queued deposits to an open position. access(all) resource InternalPosition { - /// The target health of the position + /// The position-specific target health, for auto-balancing purposes. + /// When the position health moves outside the range [minHealth, maxHealth], the balancing operation + /// should result in a position health of targetHealth. access(EImplementation) var targetHealth: UFix128 - /// The minimum health of the position, below which a position is considered undercollateralized + /// The position-specific minimum health threshold, below which a position is considered undercollateralized. + /// When a position is under-collateralized, it is eligible for rebalancing. + /// NOTE: An under-collateralized position is distinct from an unhealthy position, and cannot be liquidated access(EImplementation) var minHealth: UFix128 - /// The maximum health of the position, above which a position is considered overcollateralized + /// The position-specific maximum health threshold, above which a position is considered overcollateralized. + /// When a position is over-collateralized, it is eligible for rebalancing. access(EImplementation) var maxHealth: UFix128 /// The balances of deposited and withdrawn token types @@ -457,6 +462,7 @@ access(all) contract FlowCreditMarket { /// the position exceeds its maximum health. /// /// NOTE: If a non-nil value is provided, the Sink MUST accept MOET deposits or the operation will revert. + /// TODO(jord): precondition assumes Pool's default token is MOET, however Pool has option to specify default token in constructor. access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) { pre { sink == nil || sink!.getSinkType() == Type<@MOET.Vault>(): @@ -468,14 +474,19 @@ access(all) contract FlowCreditMarket { /// Sets the InternalPosition's topUpSource. If `nil`, the Pool will not be able to pull underflown value when /// the position falls below its minimum health which may result in liquidation. access(EImplementation) fun setTopUpSource(_ source: {DeFiActions.Source}?) { + /// TODO(jord): User can provide top-up source containing unsupported token type. Then later rebalances will revert. + /// Possibly an attack vector on automated rebalancing, if multiple positions are rebalanced in the same transaction. self.topUpSource = source } } /// InterestCurve /// - /// A simple interface to calculate interest rate + /// A simple interface to calculate interest rate for a token type. access(all) struct interface InterestCurve { + /// Returns the annual interest rate for the given credit and debit balance, for some token T. + /// @param creditBalance The credit (deposit) balance of token T + /// @param debitBalance The debit (withdrawal) balance of token T access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { post { // Max rate is 400% (4.0) to accommodate high-utilization scenarios @@ -606,10 +617,14 @@ access(all) contract FlowCreditMarket { /// The timestamp at which the TokenState was last updated access(EImplementation) var lastUpdate: UFix64 - /// The total credit balance of the related Token across the whole Pool in which this TokenState resides + /// The total credit balance for this token, in a specific Pool. + /// The total credit balance is the sum of balances of all positions with a credit balance (ie. they have lent this token). + /// In other words, it is the the sum of net deposits among positions which are net creditors in this token. access(EImplementation) var totalCreditBalance: UFix128 - /// The total debit balance of the related Token across the whole Pool in which this TokenState resides + /// The total debit balance for this token, in a specific Pool. + /// The total debit balance is the sum of balances of all positions with a debit balance (ie. they have borrowed this token). + /// In other words, it is the the sum of net withdrawals among positions which are net debtors in this token. access(EImplementation) var totalDebitBalance: UFix128 /// The index of the credit interest for the related token. @@ -758,6 +773,7 @@ access(all) contract FlowCreditMarket { /// which recalculates interest rates based on the new utilization ratio. /// This ensures rates always reflect the current state of the pool /// without requiring manual rate update calls. + access(EImplementation) fun increaseCreditBalance(by amount: UFix128) { self.totalCreditBalance = self.totalCreditBalance + amount self.updateForUtilizationChange() @@ -787,6 +803,7 @@ access(all) contract FlowCreditMarket { } /// Updates the totalCreditBalance by the provided amount + /// TODO(jord): unused access(EImplementation) fun updateCreditBalance(amount: Int256) { // temporary cast the credit balance to a signed value so we can add/subtract let adjustedBalance = Int256(self.totalCreditBalance) + amount @@ -799,6 +816,7 @@ access(all) contract FlowCreditMarket { self.updateForUtilizationChange() } + /// TODO(jord): unused access(EImplementation) fun updateDebitBalance(amount: Int256) { // temporary cast the debit balance to a signed value so we can add/subtract let adjustedBalance = Int256(self.totalDebitBalance) + amount @@ -811,7 +829,7 @@ access(all) contract FlowCreditMarket { self.updateForUtilizationChange() } - // Enhanced updateInterestIndices with deposit capacity update + // Updates the credit and debit interest index for this token, accounting for time since the last update. access(EImplementation) fun updateInterestIndices() { let currentTime = getCurrentBlock().timestamp let dt = currentTime - self.lastUpdate @@ -903,6 +921,7 @@ access(all) contract FlowCreditMarket { // Used for stable assets like MOET where rates are governance-controlled // 2. KinkInterestCurve (and others): reserve factor model // Insurance is a percentage of interest income, not a fixed spread + // TODO(jord): seems like InterestCurve abstraction could be improved if we need to check specific types here. if self.interestCurve.getType() == Type() { // FixedRate path: creditRate = debitRate - insuranceRate // This provides a fixed, predictable spread between borrower and lender rates @@ -971,7 +990,11 @@ access(all) contract FlowCreditMarket { /// Copy-only representation of a position used by pure math (no storage refs) access(all) struct PositionView { + /// Set of all non-zero balances in the position. + /// If the position does not have a balance for a supported token, no entry for that token exists in this map. access(all) let balances: {Type: InternalBalance} + /// Set of all token snapshots for which this position has a non-zero balance. + /// If the position does not have a balance for a supported token, no entry for that token exists in this map. access(all) let snapshots: {Type: TokenSnapshot} access(all) let defaultToken: Type access(all) let minHealth: UFix128 @@ -1004,6 +1027,8 @@ access(all) contract FlowCreditMarket { /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0) access(all) view fun healthFactor(view: PositionView): UFix128 { + // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet + // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations. var effectiveCollateralTotal: UFix128 = 0.0 var effectiveDebtTotal: UFix128 = 0.0 @@ -1047,6 +1072,8 @@ access(all) contract FlowCreditMarket { return 0.0 } + // TODO: this logic partly duplicates BalanceSheet construction in _getUpdatedBalanceSheet + // This function differs in that it does not read any data from a Pool resource. Consider consolidating the two implementations. var effectiveCollateralTotal: UFix128 = 0.0 var effectiveDebtTotal: UFix128 = 0.0 @@ -2077,6 +2104,7 @@ access(all) contract FlowCreditMarket { return amount } + // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: BalanceSheet, position: &InternalPosition, @@ -2138,6 +2166,8 @@ access(all) contract FlowCreditMarket { ) } + // TODO(jord): ~100-line function - consider refactoring + // TODO: documentation access(self) fun computeRequiredDepositForHealth( position: &InternalPosition, depositType: Type, @@ -2394,7 +2424,7 @@ access(all) contract FlowCreditMarket { } // Helper function to compute available withdrawal - // Helper function to compute available withdrawal + // TODO(jord): ~100-line function - consider refactoring access(self) fun computeAvailableWithdrawal( position: &InternalPosition, withdrawType: Type, @@ -2607,6 +2637,7 @@ access(all) contract FlowCreditMarket { /// depositing the loaned amount to the given Sink. /// If a Source is provided, the position will be configured to pull loan repayment /// when the loan becomes undercollateralized, preferring repayment to outright liquidation. + /// TODO(jord): it does not seem like there is any permission system for positions. Anyone with (auth EPosition) &Pool can operate on any position. access(EParticipant) fun createPosition( funds: @{FungibleToken.Vault}, issuanceSink: {DeFiActions.Sink}, @@ -2616,6 +2647,7 @@ access(all) contract FlowCreditMarket { pre { self.globalLedger[funds.getType()] != nil: "Invalid token type \(funds.getType().identifier) - not supported by this Pool" + // TODO(jord): Sink/source should be valid } // construct a new InternalPosition, assigning it the current position ID let id = self.nextPositionID @@ -2657,6 +2689,7 @@ access(all) contract FlowCreditMarket { /// Deposits the provided funds to the specified position with the configurable `pushToDrawDownSink` option. /// If `pushToDrawDownSink` is true, excess value putting the position above its max health /// is pushed to the position's configured `drawDownSink`. + /// TODO(jord): ~100-line function - consider refactoring. access(EPosition) fun depositAndPush( pid: UInt64, from: @{FungibleToken.Vault}, @@ -2786,6 +2819,7 @@ access(all) contract FlowCreditMarket { /// /// If `pullFromTopUpSource` is true, deficient value putting the position below its min health /// is pulled from the position's configured `topUpSource`. + /// TODO(jord): ~150-line function - consider refactoring. access(EPosition) fun withdrawAndPull( pid: UInt64, type: Type, @@ -3220,9 +3254,14 @@ access(all) contract FlowCreditMarket { self.debugLogging = enabled } - /// Rebalances the position to the target health value. - /// If `force` is `true`, the position will be rebalanced even if it is currently healthy. - /// Otherwise, this function will do nothing if the position is within the min/max health bounds. + /// Rebalances the position to the target health value, if the position is under- or over-collateralized, + /// as defined by the position-specific min/max health thresholds. + /// If force=true, the position will be rebalanced regardless of its current health. + /// + /// When rebalancing, funds are withdrawn from the position's topUpSource or deposited to its drawDownSink. + /// Rebalancing is done on a best effort basis (even when force=true). If the position has no sink/source, + /// of either cannot accept/provide sufficient funds for rebalancing, the rebalance will still occur but will + /// not cause the position to reach its target health. access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) { if self.debugLogging { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") @@ -3284,6 +3323,7 @@ access(all) contract FlowCreditMarket { let sinkCapacity = drawDownSink.minimumCapacity() let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal + // TODO(jord): we enforce in setDrawDownSink that the type is MOET -> we should panic here if that does not hold (currently silently fail) if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>()) if position.balances[Type<@MOET.Vault>()] == nil { From 9d2f45eb8c3e6734be7c93ace8fd6d49d960fedc Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 09:40:57 -0800 Subject: [PATCH 3/5] Revert liquidation function changes Remove modifications to liquidateRepayForSeize, liquidateViaDex, and internalRepay as these functions will be removed in the future. Changes reverted: - liquidateRepayForSeize: Restore inline reserve creation for both seizeType and debtType - liquidateViaDex: Restore reserve vault creation for both tokens - internalRepay: Restore inline reserve creation and reference pattern Co-Authored-By: Claude Sonnet 4.5 --- cadence/contracts/FlowCreditMarket.cdc | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 158e2c5..30980f9 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1853,10 +1853,13 @@ access(all) contract FlowCreditMarket { message: "Seize amount below minimum" ) - // Ensure internal reserve exists for seizeType + // Ensure internal reserves exist for seizeType and debtType if self.reserves[seizeType] == nil { self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType) } + if self.reserves[debtType] == nil { + self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) + } // Move repay tokens into reserves (repay vault must exactly match requiredRepay) assert( @@ -1868,7 +1871,7 @@ access(all) contract FlowCreditMarket { message: "Repay vault balance must be at least requiredRepay" ) let toUse <- from.withdraw(amount: quote.requiredRepay) - let debtReserveRef = self._borrowOrCreateReserveVault(type: debtType) + let debtReserveRef = (&self.reserves[debtType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! debtReserveRef.deposit(from: <-toUse) // Reduce borrower's debt position by repayAmount @@ -1944,6 +1947,14 @@ access(all) contract FlowCreditMarket { } self._assertLiquidationsActive() + // Ensure reserve vaults exist for both tokens + if self.reserves[seizeType] == nil { + self.reserves[seizeType] <-! DeFiActionsUtils.getEmptyVault(seizeType) + } + if self.reserves[debtType] == nil { + self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) + } + // Validate position is liquidatable let health = self.positionHealth(pid: pid) assert( @@ -2084,9 +2095,12 @@ access(all) contract FlowCreditMarket { access(self) fun internalRepay(pid: UInt64, from: @{FungibleToken.Vault}): UFix64 { let debtType = from.getType() + if self.reserves[debtType] == nil { + self.reserves[debtType] <-! DeFiActionsUtils.getEmptyVault(debtType) + } let toDeposit <- from let amount = toDeposit.balance - let reserveRef = self._borrowOrCreateReserveVault(type: debtType) + let reserveRef = (&self.reserves[debtType] as &{FungibleToken.Vault}?)! reserveRef.deposit(from: <-toDeposit) let position = self._borrowPosition(pid: pid) let debtState = self._borrowUpdatedTokenState(type: debtType) From 9731f7f6546d162b35f521d65b3217e0eebe42e1 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 09:43:18 -0800 Subject: [PATCH 4/5] Update cadence/contracts/FlowCreditMarket.cdc --- cadence/contracts/FlowCreditMarket.cdc | 1 - 1 file changed, 1 deletion(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 30980f9..811c3ce 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -773,7 +773,6 @@ access(all) contract FlowCreditMarket { /// which recalculates interest rates based on the new utilization ratio. /// This ensures rates always reflect the current state of the pool /// without requiring manual rate update calls. - access(EImplementation) fun increaseCreditBalance(by amount: UFix128) { self.totalCreditBalance = self.totalCreditBalance + amount self.updateForUtilizationChange() From d6012c1e0ef27abfba8a8d6653f30dd26aba3bc9 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 13:20:19 -0800 Subject: [PATCH 5/5] Apply suggestion from @jordanschalm --- cadence/contracts/FlowCreditMarket.cdc | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 811c3ce..e54c371 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1308,19 +1308,6 @@ access(all) contract FlowCreditMarket { return vaultRef?.balance ?? 0.0 } - /// Returns a reference to the reserve vault for the given type, if the token type is supported. - /// If no reserve vault exists yet, and the token type is supported, the reserve vault is created. - access(self) fun _borrowOrCreateReserveVault(type: Type): &{FungibleToken.Vault} { - pre { - self.isTokenSupported(tokenType: type) - } - if self.reserves[type] == nil { - self.reserves[type] <-! DeFiActionsUtils.getEmptyVault(type) - } - let vaultRef = &self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}? - return vaultRef! - } - /// Returns a position's balance available for withdrawal of a given Vault type. /// Phase 0 refactor: compute via pure helpers using a PositionView and TokenSnapshot for the base path. /// When `pullFromTopUpSource` is true and a topUpSource exists, preserve deposit-assisted semantics.