Skip to content

Commit 1663ffe

Browse files
authored
Merge branch 'main' into claude/update-agents-guidelines-IPuNw
2 parents 35ec5c7 + 7869867 commit 1663ffe

14 files changed

Lines changed: 173 additions & 108 deletions

cadence/contracts/FlowALPEvents.cdc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ access(all) contract FlowALPEvents {
141141
)
142142

143143
/// Emitted when the insurance rate for a token is updated by governance.
144-
/// The insurance rate is an annual fraction of debit interest diverted to the insurance fund.
144+
/// The insurance rate is a fee of accrued debit interest diverted to the insurance fund.
145145
///
146146
/// @param poolUUID the UUID of the pool containing the token
147147
/// @param tokenType the type identifier string of the token whose rate changed
148-
/// @param insuranceRate the new annual insurance rate (e.g. 0.001 for 0.1%)
148+
/// @param insuranceRate the new insurance fee (e.g. 0.001 for 0.1%)
149149
access(all) event InsuranceRateUpdated(
150150
poolUUID: UInt64,
151151
tokenType: String,
@@ -167,11 +167,11 @@ access(all) contract FlowALPEvents {
167167
)
168168

169169
/// Emitted when the stability fee rate for a token is updated by governance.
170-
/// The stability fee rate is an annual fraction of debit interest diverted to the stability fund.
170+
/// The stability fee rate is a fee of accrued debit interest diverted to the stability fund.
171171
///
172172
/// @param poolUUID the UUID of the pool containing the token
173173
/// @param tokenType the type identifier string of the token whose rate changed
174-
/// @param stabilityFeeRate the new annual stability fee rate (e.g. 0.05 for 5%)
174+
/// @param stabilityFeeRate the new stability fee (e.g. 0.05 for 5%)
175175
access(all) event StabilityFeeRateUpdated(
176176
poolUUID: UInt64,
177177
tokenType: String,

cadence/contracts/FlowALPInterestRates.cdc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ access(all) contract FlowALPInterestRates {
44
///
55
/// A simple interface to calculate interest rate for a token type.
66
access(all) struct interface InterestCurve {
7-
/// Returns the annual interest rate for the given credit and debit balance, for some token T.
7+
/// Returns the annual nominal interest rate for the given credit and debit balance, for some token T.
88
/// @param creditBalance The credit (deposit) balance of token T
99
/// @param debitBalance The debit (withdrawal) balance of token T
1010
access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 {
@@ -19,10 +19,10 @@ access(all) contract FlowALPInterestRates {
1919

2020
/// FixedCurve
2121
///
22-
/// A fixed-rate interest curve implementation that returns a constant yearly interest rate
22+
/// A fixed-rate interest curve implementation that returns a constant nominal yearly interest rate
2323
/// regardless of utilization. This is suitable for stable assets like MOET where predictable
2424
/// rates are desired.
25-
/// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY)
25+
/// @param yearlyRate The fixed yearly nominal rate as a UFix128 (e.g., 0.05 for a 5% nominal yearly rate)
2626
access(all) struct FixedCurve: InterestCurve {
2727

2828
access(all) let yearlyRate: UFix128
@@ -64,7 +64,7 @@ access(all) contract FlowALPInterestRates {
6464
/// This matches the live TokenState accounting used by FlowALP.
6565
///
6666
/// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%)
67-
/// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY)
67+
/// @param baseRate The minimum yearly nominal rate (e.g., 0.01 for a 1% nominal yearly rate)
6868
/// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%)
6969
/// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%)
7070
access(all) struct KinkCurve: InterestCurve {

cadence/lib/FlowALPMath.cdc

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,11 @@ access(all) contract FlowALPMath {
9999
return diffBps <= maxDeviationBps
100100
}
101101

102-
/// Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point
103-
/// number with 18 decimal places). The input to this function will be just the relative annual interest rate
104-
/// (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001).
102+
/// Converts a nominal yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed
103+
/// point number with 18 decimal places). The input to this function is the relative nominal annual rate
104+
/// (e.g. 0.05 for a 5% nominal yearly rate), and the result is the per-second multiplier
105+
/// (e.g. 1.000000000001). For positive rates, the effective one-year growth will be slightly higher than the
106+
/// nominal rate because interest compounds over time.
105107
access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 {
106108
let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0
107109
assert(
@@ -111,6 +113,19 @@ access(all) contract FlowALPMath {
111113
return perSecondScaledValue + 1.0
112114
}
113115

116+
/// Returns the effective annual yield (EAY) for a given nominal yearly rate, assuming discrete per-second compounding.
117+
///
118+
/// Formula: EAY = (1 + nominalRate / secondsPerYear) ^ secondsPerYear - 1
119+
///
120+
/// For example, a nominal rate of 100% (1.0) produces an effective rate of about 171.8281776413%
121+
/// under discrete per-second compounding: (1 + 1 / 31_557_600) ^ 31_557_600 - 1.
122+
/// This is extremely close to the continuous-compounding limit of e - 1.
123+
access(all) view fun effectiveYearlyRate(nominalYearlyRate: UFix128): UFix128 {
124+
let perSecondRate = FlowALPMath.perSecondInterestRate(yearlyRate: nominalYearlyRate)
125+
let compounded = FlowALPMath.powUFix128(perSecondRate, 31_557_600.0)
126+
return compounded - 1.0
127+
}
128+
114129
/// Returns the compounded interest index reflecting the passage of time
115130
/// The result is: newIndex = oldIndex * perSecondRate ^ seconds
116131
access(all) view fun compoundInterestIndex(

cadence/tests/TEST_COVERAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ The `test_helpers.cdc` file provides:
204204
3. **FLOW Debit Interest**
205205
- KinkCurve-based interest rates
206206
- Variable rates based on utilization
207-
- Interest compounds continuously
207+
- Interest compounds via discrete per-second updates
208208

209209
4. **FLOW Credit Interest**
210210
- LP earnings with insurance spread
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Test
2+
import BlockchainHelpers
3+
4+
import "FlowALPMath"
5+
import "test_helpers.cdc"
6+
7+
access(all)
8+
fun setup() {
9+
let err = Test.deployContract(
10+
name: "FlowALPMath",
11+
path: "../lib/FlowALPMath.cdc",
12+
arguments: []
13+
)
14+
Test.expect(err, Test.beNil())
15+
}
16+
17+
access(all) struct TestCase {
18+
access(all) let nominal: UFix128
19+
access(all) let expected: UFix128
20+
21+
init(nominal: UFix128, expected: UFix128) {
22+
self.nominal = nominal
23+
self.expected = expected
24+
}
25+
}
26+
27+
access(all)
28+
fun test_effectiveYearlyRate() {
29+
let delta: UFix128 = 0.0001
30+
let testCases = [
31+
TestCase(nominal: 0.01, expected: 0.01005016708), // ≈ e^0.01 - 1
32+
TestCase(nominal: 0.02, expected: 0.02020134003), // ≈ e^0.02 - 1
33+
TestCase(nominal: 0.05, expected: 0.05127109638), // ≈ e^0.05 - 1
34+
TestCase(nominal: 0.50, expected: 0.6487212707), // ≈ e^0.5 - 1
35+
TestCase(nominal: 1.0, expected: 1.7182818285), // ≈ e^1 - 1
36+
TestCase(nominal: 4.0, expected: 53.5981500331) // ≈ e^4 - 1
37+
]
38+
for testCase in testCases {
39+
let effective = FlowALPMath.effectiveYearlyRate(nominalYearlyRate: testCase.nominal)
40+
let diff = effective > testCase.expected ? effective - testCase.expected : testCase.expected - effective
41+
Test.assert(
42+
diff <= delta,
43+
message: "effectiveYearlyRate(\(testCase.nominal.toString())) expected ~\(testCase.expected.toString()), got \(effective.toString()), diff \(diff.toString()) exceeds delta \(delta.toString())"
44+
)
45+
}
46+
}

cadence/tests/interest_accrual_integration_test.cdc

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import "test_helpers.cdc"
2828
// - Focuses on protocol solvency and insurance mechanics
2929
//
3030
// Interest Rate Configuration:
31-
// - MOET: FixedCurve at 4% APY (rate independent of utilization)
31+
// - MOET: FixedCurve at a 4% nominal yearly rate (rate independent of utilization)
3232
// - Flow: KinkCurve with Aave v3 Volatile One parameters
3333
// (45% optimal utilization, 0% base, 4% slope1, 300% slope2)
3434
// =============================================================================
@@ -40,18 +40,19 @@ access(all) var snapshot: UInt64 = 0
4040
// Interest Rate Parameters
4141
// =============================================================================
4242

43-
// MOET: FixedCurve (Spread Model)
43+
// MOET: FixedCurve (Protocol-Fee Spread Model)
4444
// -----------------------------------------------------------------------------
45-
// In the spread model, the curve defines the DEBIT rate (what borrowers pay).
46-
// The CREDIT rate is derived as: creditRate = debitRate - insuranceRate
45+
// In the fixed-curve path, the curve defines the DEBIT rate (what borrowers pay).
46+
// The CREDIT rate is derived from the debit rate after protocol fees.
4747
// This ensures lenders always earn less than borrowers pay, with the
48-
// difference going to the insurance pool for protocol solvency.
48+
// difference allocated by the configured protocol fee settings.
4949
//
50-
// Example at 4% debit rate with 0.1% insurance:
51-
// - Borrowers pay: 4.0% APY
52-
// - Lenders earn: 3.9% APY
53-
// - Insurance: 0.1% APY (collected by protocol)
54-
access(all) let moetFixedRate: UFix128 = 0.04 // 4% APY debit rate
50+
// Example at a 4% nominal yearly debit rate:
51+
// - Borrowers pay: 4.0% nominal yearly debit rate
52+
// - Lenders earn: a lower nominal yearly credit rate after protocol fees
53+
// - Protocol Fees are comprised of two parts -
54+
// - Insurance/Stability: configurable fees of accrued debit interest
55+
access(all) let moetFixedRate: UFix128 = 0.04 // 4% nominal yearly debit rate
5556

5657
// FlowToken: KinkCurve (Aave v3 Volatile One Parameters)
5758
// -----------------------------------------------------------------------------
@@ -64,10 +65,10 @@ access(all) let moetFixedRate: UFix128 = 0.04 // 4% APY debit rate
6465
// - If utilization > optimal: rate = baseRate + slope1 + ((util-optimal)/(1-optimal)) × slope2
6566
//
6667
// At 40% utilization (below 45% optimal):
67-
// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% APY
68+
// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% nominal yearly rate
6869
//
6970
// At 80% utilization (above 45% optimal):
70-
// - Rate = 0% + 4% + ((80%-45%)/(100%-45%)) × 300% ≈ 195% APY
71+
// - Rate = 0% + 4% + ((80%-45%)/(100%-45%)) × 300% ≈ 195% nominal yearly rate
7172
access(all) let flowOptimalUtilization: UFix128 = 0.45 // 45% kink point
7273
access(all) let flowBaseRate: UFix128 = 0.0 // 0% base rate
7374
access(all) let flowSlope1: UFix128 = 0.04 // 4% slope below kink
@@ -160,15 +161,15 @@ fun test_moet_debit_accrues_interest() {
160161
// -------------------------------------------------------------------------
161162
// STEP 4: Configure MOET Interest Rate
162163
// -------------------------------------------------------------------------
163-
// Set MOET to use a FixedCurve at 4% APY.
164+
// Set MOET to use a FixedCurve at a 4% nominal yearly rate.
164165
// This rate is independent of utilization - borrowers always pay 4%.
165166
// Note: Interest curve must be set AFTER LP deposit to ensure credit exists.
166167
setInterestCurveFixed(
167168
signer: PROTOCOL_ACCOUNT,
168169
tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER,
169170
yearlyRate: moetFixedRate
170171
)
171-
log("Set MOET interest rate to 4% APY (after LP deposit)")
172+
log("Set MOET interest rate to 4% nominal yearly rate (after LP deposit)")
172173

173174
let res = setInsuranceSwapper(
174175
signer: PROTOCOL_ACCOUNT,
@@ -305,7 +306,7 @@ fun test_moet_debit_accrues_interest() {
305306
// Expected Growth Calculation
306307
// -------------------------------------------------------------------------
307308
// Per-second compounding: (1 + r / 31_557_600) ^ seconds - 1
308-
// At 4% APY for 30 days (2,592,000 seconds):
309+
// At a 4% nominal yearly rate for 30 days (2,592,000 seconds):
309310
// Growth = (1 + 0.04 / 31_557_600) ^ 2_592_000 - 1 ≈ 0.328%
310311
//
311312
// We use a wide tolerance range because:
@@ -337,10 +338,10 @@ fun test_moet_debit_accrues_interest() {
337338
// - Time advances 30 days
338339
// - Verify: LP credit increased, growth rate is in expected range
339340
//
340-
// Key Insight (FixedCurve Spread Model):
341-
// - debitRate = 4.0% (what borrowers pay, defined by curve)
342-
// - insuranceRate = 0.1% (protocol reserve)
343-
// - creditRate = debitRate - insuranceRate = 3.9% (what lenders earn)
341+
// Key Insight (FixedCurve Protocol-Fee Spread):
342+
// - debitRate is defined by the curve
343+
// - creditRate is the debit rate after protocol fees
344+
// - creditRate remains below debitRate
344345
// =============================================================================
345346
access(all)
346347
fun test_moet_credit_accrues_interest_with_insurance() {
@@ -394,7 +395,7 @@ fun test_moet_credit_accrues_interest_with_insurance() {
394395
// -------------------------------------------------------------------------
395396
// STEP 4: Configure MOET Interest Rate
396397
// -------------------------------------------------------------------------
397-
// Set 4% APY debit rate. Credit rate will be ~3.9% after insurance deduction.
398+
// Set a 4% nominal yearly debit rate. Credit rate will be lower after protocol fees.
398399
setInterestCurveFixed(
399400
signer: PROTOCOL_ACCOUNT,
400401
tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER,
@@ -485,9 +486,9 @@ fun test_moet_credit_accrues_interest_with_insurance() {
485486
// -------------------------------------------------------------------------
486487
// Expected Credit Growth Calculation
487488
// -------------------------------------------------------------------------
488-
// Debit rate: 4% APY (what borrowers pay)
489-
// Insurance: 0.1% APY (protocol reserve)
490-
// Credit rate: 4% - 0.1% = 3.9% APY (what LPs earn)
489+
// Debit rate: 4% nominal yearly rate (what borrowers pay)
490+
// Protocol fees: configured insurance plus stability fee fractions
491+
// Credit rate: lower than the debit rate after protocol fees
491492
//
492493
// 30-day credit growth ≈ 3.9% × (30/365) ≈ 0.32%
493494
//
@@ -523,7 +524,7 @@ fun test_moet_credit_accrues_interest_with_insurance() {
523524
// Key Insight (KinkCurve):
524525
// At 40% utilization (below 45% optimal kink):
525526
// - Rate = baseRate + (utilization/optimal) × slope1
526-
// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% APY
527+
// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% nominal yearly rate
527528
// =============================================================================
528529
access(all)
529530
fun test_flow_debit_accrues_interest() {
@@ -685,7 +686,7 @@ fun test_flow_debit_accrues_interest() {
685686
// -------------------------------------------------------------------------
686687
// Utilization = 4,000 / 10,000 = 40% (below 45% optimal)
687688
// Rate = baseRate + (util/optimal) × slope1
688-
// = 0% + (40%/45%) × 4% ≈ 3.56% APY
689+
// = 0% + (40%/45%) × 4% ≈ 3.56% nominal yearly rate
689690
//
690691
// 30-day growth ≈ 3.56% × (30/365) ≈ 0.29%
691692
let minExpectedDebtGrowth: UFix64 = 0.002 // 0.2%
@@ -891,15 +892,15 @@ fun test_flow_credit_accrues_interest_with_insurance() {
891892
// - LP deposits 10,000 MOET
892893
// - Borrower deposits 10,000 FLOW and borrows MOET
893894
// - Insurance rate set to 1% (higher than default 0.1% for visibility)
894-
// - Debit rate set to 10% APY
895+
// - Debit rate set to a 10% nominal yearly rate
895896
// - Time advances 1 YEAR
896897
// - Verify: Insurance spread ≈ 1% (debit rate - credit rate)
897898
//
898-
// Key Insight (FixedCurve Spread Model):
899-
// - debitRate = 10% (what borrowers pay)
900-
// - insuranceRate = 1% (protocol reserve)
901-
// - creditRate = debitRate - insuranceRate = 9% (what LPs earn)
902-
// - Spread = debitRate - creditRate = 1%
899+
// Key Insight (FixedCurve Protocol-Fee Spread):
900+
// - debitRate is set by the fixed curve
901+
// - insurance/stability remain configured fee parameters
902+
// - creditRate is reduced relative to debitRate by those protocol fees
903+
// - the realized spread shows up as a lower lender growth rate than borrower growth rate
903904
// =============================================================================
904905
access(all)
905906
fun test_insurance_deduction_verification() {
@@ -952,7 +953,7 @@ fun test_insurance_deduction_verification() {
952953
//
953954
// Insurance Rate: 1% (vs default 0.1%)
954955
// Debit Rate: 10% (vs default 4%)
955-
// Expected Credit Rate: 10% - 1% = 9%
956+
// Expected Credit Rate: lower than 10% after protocol fees
956957
let res = setInsuranceSwapper(
957958
signer: PROTOCOL_ACCOUNT,
958959
tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER,
@@ -1012,8 +1013,8 @@ fun test_insurance_deduction_verification() {
10121013
// =========================================================================
10131014
// Using 1 year (31,557,600 seconds for 365.25 days) makes the percentage calculations
10141015
// straightforward. With per-second discrete compounding:
1015-
// - 10% APY → (1 + 0.10 / 31_557_600) ^ 31_557_600 - 1 ≈ 10.52% effective rate
1016-
// - 9% APY → (1 + 0.09 / 31_557_600) ^ 31_557_600 - 1 ≈ 9.42% effective rate
1016+
// - 10% nominal yearly rate → (1 + 0.10 / 31_557_600) ^ 31_557_600 - 1 ≈ 10.52% effective rate
1017+
// - 9% nominal yearly rate → (1 + 0.09 / 31_557_600) ^ 31_557_600 - 1 ≈ 9.42% effective rate
10171018
// - Spread should be approximately 1%
10181019
Test.moveTime(by: ONE_YEAR)
10191020
Test.commitBlock()
@@ -1049,9 +1050,10 @@ fun test_insurance_deduction_verification() {
10491050
// =========================================================================
10501051
// ASSERTION: Verify Insurance Spread
10511052
// =========================================================================
1052-
// For FixedCurve (spread model):
1053-
// - debitRate = creditRate + insuranceRate
1054-
// - insuranceSpread = debitRate - creditRate ≈ insuranceRate
1053+
// For FixedCurve:
1054+
// - debitRate is the curve-defined nominal yearly rate
1055+
// - creditRate is the debit rate after protocol fees
1056+
// - insuranceSpread = actualDebtRate - actualCreditRate
10551057
//
10561058
// With 10% debit and 1% insurance, spread should be ~1%
10571059
// (Slight variation due to per-second compounding effects)
@@ -1158,12 +1160,12 @@ fun test_combined_all_interest_scenarios() {
11581160
// -------------------------------------------------------------------------
11591161
// STEP 5: Configure Interest Curves for Both Tokens
11601162
// -------------------------------------------------------------------------
1161-
// MOET: FixedCurve at 4% APY (spread model)
1163+
// MOET: FixedCurve at a 4% nominal yearly rate (fixed-curve spread model)
11621164
// Flow: KinkCurve with Aave v3 Volatile One parameters
11631165
setInterestCurveFixed(
11641166
signer: PROTOCOL_ACCOUNT,
11651167
tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER,
1166-
yearlyRate: moetFixedRate // 4% APY
1168+
yearlyRate: moetFixedRate // 4% nominal yearly rate
11671169
)
11681170
setInterestCurveKink(
11691171
signer: PROTOCOL_ACCOUNT,
@@ -1324,14 +1326,14 @@ fun test_combined_all_interest_scenarios() {
13241326
// Assertion Group 2: Health Factor Changes
13251327
// -------------------------------------------------------------------------
13261328
// Borrower1 (Flow collateral, MOET debt):
1327-
// - MOET debit rate: 4% APY
1329+
// - MOET debit rate: 4% nominal yearly rate
13281330
// - Flow credit rate: lower than Flow debit rate due to insurance spread
13291331
// - Net effect: Debt grows faster than collateral → Health DECREASES
13301332
Test.assert(b1HealthAfter < b1HealthBefore, message: "Borrower1 health should decrease")
13311333

13321334
// Borrower2 (MOET collateral, Flow debt):
1333-
// - MOET credit rate: ~3.9% APY (4% debit - 0.1% insurance)
1334-
// - Flow debit rate: ~2.5% APY (at 28.6% utilization)
1335+
// - MOET credit rate: lower than the MOET debit rate after protocol fees
1336+
// - Flow debit rate: ~2.5% nominal yearly rate (at 28.6% utilization)
13351337
// - Collateral (3,000 MOET) earning more absolute interest than debt (2,000 Flow)
13361338
// - Net effect: Health INCREASES
13371339
Test.assert(b2HealthAfter > b2HealthBefore, message: "Borrower2 health should increase (collateral interest > debt interest)")

0 commit comments

Comments
 (0)