From 6db24381c395dc9e187a53a008d73e2bb772a781 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 14 Jan 2026 13:59:04 -0800 Subject: [PATCH 1/6] update submodule version --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index 1254f6e..53cf15e 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 1254f6e94fe23e27490d9df042de186b29e5e4cc +Subproject commit 53cf15e7967fbff77e46cb0fed9981d665cba354 From 992b211228c42f0f6cd6047bd8d423bfebed8251 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 14 Jan 2026 14:41:36 -0800 Subject: [PATCH 2/6] doc updates, use safe getter for reserve vault --- cadence/contracts/FlowCreditMarket.cdc | 39 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 1f5d779..ff52bf9 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -374,13 +374,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 @@ -444,14 +449,18 @@ 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): must be a supported token type? 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 @@ -956,7 +965,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 @@ -1008,8 +1021,9 @@ access(all) contract FlowCreditMarket { } /// Computes health = totalEffectiveCollateral / totalEffectiveDebt (∞ when debt == 0) - // TODO: return BalanceSheet, this seems like a dupe of _getUpdatedBalanceSheet 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 @@ -1053,7 +1067,8 @@ access(all) contract FlowCreditMarket { return 0.0 } - // TODO(jord): this logic duplicates BalanceSheet construction + // 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 @@ -1305,7 +1320,7 @@ access(all) contract FlowCreditMarket { /// 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} { + access(self) fun _borrowOrCreateReserveVault(type: Type): auth(FungibleToken.Withdraw) &{FungibleToken.Vault} { pre { self.isTokenSupported(tokenType: type) } @@ -1582,7 +1597,7 @@ access(all) contract FlowCreditMarket { position.balances[seizeType] = InternalBalance(direction: BalanceDirection.Credit, scaledBalance: 0.0) } position.balances[seizeType]!.recordWithdrawal(amount: UFix128(seizeAmount), tokenState: seizeState) - let seizeReserveRef = (&self.reserves[seizeType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let seizeReserveRef = self._borrowOrCreateReserveVault(type: seizeType) let seizedCollateral <- seizeReserveRef.withdraw(amount: seizeAmount) let newHealth = self.positionHealth(pid: pid) @@ -2281,10 +2296,7 @@ access(all) contract FlowCreditMarket { } // Create vault if it doesn't exist yet - if self.reserves[type] == nil { - self.reserves[type] <-! from.createEmptyVault() - } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveVault = self._borrowOrCreateReserveVault(type: type) // Reflect the deposit in the position's balance. // @@ -2446,7 +2458,7 @@ access(all) contract FlowCreditMarket { ) } - let reserveVault = (&self.reserves[type] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! + let reserveVault = self._borrowOrCreateReserveVault(type: type) // Reflect the withdrawal in the position's balance let uintAmount = UFix128(amount) @@ -2954,7 +2966,6 @@ access(all) contract FlowCreditMarket { } /// Returns a position's BalanceSheet containing its effective collateral and debt as well as its current health - /// TODO(jord): in all cases callers already are calling _borrowPosition, more efficient to pass in PositionView? access(self) fun _getUpdatedBalanceSheet(pid: UInt64): BalanceSheet { let position = self._borrowPosition(pid: pid) From 7f6734e85984129e841ddc2e655908b4d3068f0f Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Wed, 14 Jan 2026 14:46:13 -0800 Subject: [PATCH 3/6] remove todos --- cadence/contracts/FlowCreditMarket.cdc | 2 -- 1 file changed, 2 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index ff52bf9..06b7cef 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -449,7 +449,6 @@ 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): must be a supported token type? self.topUpSource = source } } @@ -1387,7 +1386,6 @@ access(all) contract FlowCreditMarket { /// 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) From 7328c9781263b24a755e4438cb90423886aa0144 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Thu, 15 Jan 2026 08:56:18 -0800 Subject: [PATCH 4/6] add docs, todos --- cadence/contracts/FlowCreditMarket.cdc | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 06b7cef..4dabb8e 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -438,6 +438,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>(): @@ -449,6 +450,8 @@ 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 } } @@ -1643,6 +1646,7 @@ access(all) contract FlowCreditMarket { ) } + // TODO: documentation access(self) fun computeAdjustedBalancesAfterWithdrawal( balanceSheet: BalanceSheet, position: &InternalPosition, @@ -1704,6 +1708,8 @@ access(all) contract FlowCreditMarket { ) } + // TODO(jord): ~100-line function - consider refactoring + // TODO: documentation access(self) fun computeRequiredDepositForHealth( position: &InternalPosition, depositType: Type, @@ -1960,7 +1966,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, @@ -2173,6 +2179,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}, @@ -2182,6 +2189,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 @@ -2223,6 +2231,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}, @@ -2349,6 +2358,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, @@ -2777,9 +2787,12 @@ 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 access(EPosition) fun rebalancePosition(pid: UInt64, force: Bool) { if self.debugLogging { log(" [CONTRACT] rebalancePosition(pid: \(pid), force: \(force))") From 043f27cd11147dfff1d41f975bd71acf29f3ec23 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Thu, 15 Jan 2026 13:43:39 -0800 Subject: [PATCH 5/6] expand rebalance docs --- cadence/contracts/FlowCreditMarket.cdc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 4dabb8e..70b92d5 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -2792,7 +2792,9 @@ access(all) contract FlowCreditMarket { /// 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 + /// 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))") From e8e9aef8e07937ccbfbf61e6aa0c519d1a2283c7 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Thu, 15 Jan 2026 15:54:47 -0800 Subject: [PATCH 6/6] add docs, todos --- cadence/contracts/FlowCreditMarket.cdc | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 70b92d5..4be638b 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -593,10 +593,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. @@ -747,6 +751,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() @@ -776,6 +781,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 @@ -788,6 +794,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 @@ -800,7 +807,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 @@ -892,6 +899,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 @@ -2856,6 +2864,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 {