Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 55 additions & 22 deletions cadence/contracts/FlowCreditMarket.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -433,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>():
Expand All @@ -444,14 +450,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
Expand Down Expand Up @@ -582,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.
Expand Down Expand Up @@ -736,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()
Expand Down Expand Up @@ -765,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
Expand All @@ -777,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
Expand All @@ -789,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
Expand Down Expand Up @@ -881,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<FlowCreditMarket.FixedRateInterestCurve>() {
// FixedRate path: creditRate = debitRate - insuranceRate
// This provides a fixed, predictable spread between borrower and lender rates
Expand Down Expand Up @@ -956,7 +975,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
Expand Down Expand Up @@ -1008,8 +1031,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

Expand Down Expand Up @@ -1053,7 +1077,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

Expand Down Expand Up @@ -1305,7 +1330,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)
}
Expand Down Expand Up @@ -1372,7 +1397,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)

Expand Down Expand Up @@ -1582,7 +1606,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)
Expand Down Expand Up @@ -1630,6 +1654,7 @@ access(all) contract FlowCreditMarket {
)
}

// TODO: documentation
access(self) fun computeAdjustedBalancesAfterWithdrawal(
balanceSheet: BalanceSheet,
position: &InternalPosition,
Expand Down Expand Up @@ -1691,6 +1716,8 @@ access(all) contract FlowCreditMarket {
)
}

// TODO(jord): ~100-line function - consider refactoring
// TODO: documentation
access(self) fun computeRequiredDepositForHealth(
position: &InternalPosition,
depositType: Type,
Expand Down Expand Up @@ -1947,7 +1974,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,
Expand Down Expand Up @@ -2160,6 +2187,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},
Expand All @@ -2169,6 +2197,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
Expand Down Expand Up @@ -2210,6 +2239,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},
Expand Down Expand Up @@ -2281,10 +2311,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.
//
Expand Down Expand Up @@ -2339,6 +2366,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,
Expand Down Expand Up @@ -2446,7 +2474,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)
Expand Down Expand Up @@ -2767,9 +2795,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))")
Expand Down Expand Up @@ -2831,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 {
Expand Down Expand Up @@ -2954,7 +2988,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)

Expand Down