diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 62133be4..8b6950bc 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -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() { + let fixedCurve = curve as! FlowALPInterestRates.FixedCurve + params["yearlyRate"] = fixedCurve.yearlyRate + return params + } + + if curve.getType() == Type() { + 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. diff --git a/cadence/scripts/flow-alp/get_interest_curve_params.cdc b/cadence/scripts/flow-alp/get_interest_curve_params.cdc new file mode 100644 index 00000000..e98e695b --- /dev/null +++ b/cadence/scripts/flow-alp/get_interest_curve_params.cdc @@ -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) +} diff --git a/docs/interest_rate_and_protocol_fees.md b/docs/interest_rate_and_protocol_fees.md index 95173312..51b4877c 100644 --- a/docs/interest_rate_and_protocol_fees.md +++ b/docs/interest_rate_and_protocol_fees.md @@ -34,27 +34,101 @@ 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 @@ -62,6 +136,8 @@ 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 @@ -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