Skip to content
Merged
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
46 changes: 46 additions & 0 deletions cadence/contracts/FlowALPv0.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,52 @@ access(all) contract FlowALPv0 {
return nil
}

/// Returns interest curve parameters and current per-second rates for a given token type.
/// Returns nil if the token type is not supported.
///
/// Always returned:
/// - curveType
/// - currentDebitRatePerSecond
/// - currentCreditRatePerSecond
///
/// FixedCurve fields:
/// - yearlyRate
///
/// KinkCurve fields:
/// - optimalUtilization
/// - baseRate
/// - slope1
/// - slope2
access(all) view fun getInterestCurveParams(tokenType: Type): {String: AnyStruct}? {
if let tokenState = self.state.getTokenState(tokenType) {
let curve = tokenState.getInterestCurve()
var params = {
"curveType": curve.getType().identifier,
"currentDebitRatePerSecond": tokenState.getCurrentDebitRate(),
"currentCreditRatePerSecond": tokenState.getCurrentCreditRate()
}

if curve.getType() == Type<FlowALPInterestRates.FixedCurve>() {
let fixedCurve = curve as! FlowALPInterestRates.FixedCurve
params["yearlyRate"] = fixedCurve.yearlyRate
return params
}

if curve.getType() == Type<FlowALPInterestRates.KinkCurve>() {
let kinkCurve = curve as! FlowALPInterestRates.KinkCurve
params["optimalUtilization"] = kinkCurve.optimalUtilization
params["baseRate"] = kinkCurve.baseRate
params["slope1"] = kinkCurve.slope1
params["slope2"] = kinkCurve.slope2
return params
}

return params
}

return nil
}

/// 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.
Expand Down
30 changes: 30 additions & 0 deletions cadence/scripts/flow-alp/get_interest_curve_params.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import "FlowALPv0"

/// Returns interest curve parameters for the specified token type.
///
/// Always returns:
/// - curveType
/// - currentDebitRatePerSecond
/// - currentCreditRatePerSecond
///
/// For FixedCurve, also returns:
/// - yearlyRate
///
/// For KinkCurve, also returns:
/// - optimalUtilization
/// - baseRate
/// - slope1
/// - slope2
///
/// @param tokenTypeIdentifier: The Type identifier of the token vault (e.g., "A.0x07.MOET.Vault")
/// @return A map of curve parameters, or nil if the token type is not supported
access(all) fun main(tokenTypeIdentifier: String): {String: AnyStruct}? {
let tokenType = CompositeType(tokenTypeIdentifier)
?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)")

let protocolAddress = Type<@FlowALPv0.Pool>().address!
let pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath)
?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)")

return pool.getInterestCurveParams(tokenType: tokenType)
}
96 changes: 91 additions & 5 deletions docs/interest_rate_and_protocol_fees.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,110 @@ Both fees are deducted from the interest income that would otherwise go to lende

### 1. Debit Rate Calculation

The debit rate is determined by an interest curve that takes into account the utilization ratio of the pool:
For each token, the protocol stores one interest curve. The debit rate (borrow APY) is computed from that curve and the current pool utilization:

```
utilization = totalDebitBalance / (totalCreditBalance + totalDebitBalance)

debitRate = interestCurve.interestRate(
creditBalance: totalCreditBalance,
debitBalance: totalDebitBalance
)
```

The interest curve typically increases the rate as utilization increases, incentivizing borrowers to repay when the pool is highly utilized and encouraging lenders to supply liquidity when rates are high.
Utilization in this model is:
- `0%` when there is no debt
- `50%` when debit and credit balances are equal
- near `100%` when most liquidity is borrowed

### FixedCurve (constant APY)

For `FixedCurve`, debit APY is constant regardless of utilization:

```
debitRate = yearlyRate
```

Example:
- `yearlyRate = 0.05` (5% APY)
- debit APY stays at 5% whether utilization is 10% or 95%

### KinkCurve (utilization-based APY)

For `KinkCurve`, debit APY follows a two-segment curve:
- below `optimalUtilization` ("before the kink"), rates rise gently
- above `optimalUtilization` ("after the kink"), rates rise steeply

Definitions:

```
u = utilization
u* = optimalUtilization
```

If `u <= u*`:

```
debitRate = baseRate + slope1 * (u / u*)
```

If `u > u*`:

```
debitRate = baseRate + slope1 + slope2 * ((u - u*) / (1 - u*))
```

At full utilization (`u = 100%`), the rate is:

```
maxDebitRate = baseRate + slope1 + slope2
```

#### Example profile (Aave v3 "Volatile One" style)

Reference values discussed for volatile assets:
- Source: https://github.com/onflow/FlowYieldVaults/pull/108#discussion_r2688322723
- `optimalUtilization = 45%` (`0.45`)
- `baseRate = 0%` (`0.0`)
- `slope1 = 4%` (`0.04`)
- `slope2 = 300%` (`3.0`)

Interpretation:
- at or below 45% utilization, borrowers see relatively low/gradual APY increases
- above 45%, APY increases very aggressively to push utilization back down
- theoretical max debit APY at 100% utilization is `304%` (`0% + 4% + 300%`)

This is the mechanism that helps protect withdrawal liquidity under stress.

### 2. Credit Rate Calculation

The credit rate is derived from the total debit interest income, with insurance and stability fees applied proportionally as a percentage of the interest generated.
The credit rate (deposit APY) is derived from debit-side income after protocol fees.

Shared definitions:

For **FixedRateInterestCurve** (used for stable assets like MOET):
```
protocolFeeRate = insuranceRate + stabilityFeeRate
```

and `protocolFeeRate` must be `< 1.0`.

For **FixedCurve** (used for stable assets like MOET):
```
creditRate = debitRate * (1 - protocolFeeRate)
```

For **KinkInterestCurve** and other curves:
This gives a simple spread model between borrow APY and lend APY.

For **KinkCurve** and other non-fixed curves:
```
debitIncome = totalDebitBalance * debitRate
protocolFeeRate = insuranceRate + stabilityFeeRate
protocolFeeAmount = debitIncome * protocolFeeRate
creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance
```

This computes lender yield from actual debit-side income, after reserve deductions.

**Important**: The combined `insuranceRate + stabilityFeeRate` must be less than 1.0 to avoid underflow in credit rate calculation. This is enforced by preconditions when setting either rate.

### 3. Per-Second Rate Conversion
Expand All @@ -76,6 +152,16 @@ Where `secondsInYear = 31_557_600` (365.25 days × 24 hours × 60 minutes × 60

This conversion allows for continuous compounding of interest over time.

### 4. Querying Curve Parameters On-Chain

The pool exposes `getInterestCurveParams(tokenType)` and the repo includes script:
- `cadence/scripts/flow-alp/get_interest_curve_params.cdc`

Returned fields:
- Always: `curveType`, `currentDebitRatePerSecond`, `currentCreditRatePerSecond`
- FixedCurve: `yearlyRate`
- KinkCurve: `optimalUtilization`, `baseRate`, `slope1`, `slope2`

## Interest Accrual Mechanism

### Interest Indices
Expand Down
Loading