From cc903f598fa5d79e6a8ffed427ff20efd9ca1329 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Thu, 26 Feb 2026 15:45:32 +0200 Subject: [PATCH 1/6] Refactored withdraw from position tx, updated test helper, updated usages --- ...rsarial_recursive_withdraw_source_test.cdc | 12 ++++--- .../tests/adversarial_type_spoofing_test.cdc | 11 +++--- cadence/tests/pool_pause_test.cdc | 35 +++++++++++-------- .../position_health_constraints_test.cdc | 11 +++--- .../tests/position_lifecycle_unhappy_test.cdc | 24 +++++++------ cadence/tests/test_helpers.cdc | 7 ++-- .../withdraw_from_position.cdc | 27 ++++---------- 7 files changed, 68 insertions(+), 59 deletions(-) diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index bdf9e099..d4cd85e9 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -119,10 +119,14 @@ fun testRecursiveWithdrawSource() { // // In this test, the topUpSource behavior is adversarial: it attempts to re-enter // the pool during the pull/deposit flow. We expect the transaction to fail. - let withdrawRes = executeTransaction( - "./transactions/flow-alp/pool-management/withdraw_from_position.cdc", - [positionID, flowTokenIdentifier, 1500.0, true], // pullFromTopUpSource: true - userAccount + let withdrawRes = withdrawFromPosition( + signer: userAccount, + positionId: positionID, + tokenTypeIdentifier: flowTokenIdentifier, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 1500.0, + pullFromTopUpSource: true + ) Test.expect(withdrawRes, Test.beFailed()) diff --git a/cadence/tests/adversarial_type_spoofing_test.cdc b/cadence/tests/adversarial_type_spoofing_test.cdc index aa7d497e..d31f33be 100644 --- a/cadence/tests/adversarial_type_spoofing_test.cdc +++ b/cadence/tests/adversarial_type_spoofing_test.cdc @@ -59,10 +59,13 @@ fun testMaliciousSource() { Test.expect(openRes, Test.beSucceeded()) // withdraw 1337 Flow from the position - let withdrawRes = executeTransaction( - "./transactions/flow-alp/pool-management/withdraw_from_position.cdc", - [1 as UInt64, flowTokenIdentifier, 1337.0, true], - hackerAccount + let withdrawRes = withdrawFromPosition( + signer: hackerAccount, + positionId: 1, + tokenTypeIdentifier: flowTokenIdentifier, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 1337.0, + pullFromTopUpSource: true ) Test.expect(withdrawRes, Test.beFailed()) diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index 0a2fa85e..2e4749cc 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -69,10 +69,13 @@ fun test_pool_pause_deposit_withdrawal() { Test.expect(depositRes, Test.beFailed()) // Can't withdraw from existing position - let withdrawRes = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [0, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], - user1 + let withdrawRes = withdrawFromPosition( + signer: user1, + positionId: 0, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: initialDepositAmount/2.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes, Test.beFailed()) @@ -101,10 +104,13 @@ fun test_pool_pause_deposit_withdrawal() { Test.expect(depositRes2, Test.beSucceeded()) // Withdrawing from position should still fail during warmup period - let withdrawRes2 = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [0 as UInt64, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], - user1 + let withdrawRes2 = withdrawFromPosition( + signer: user1, + positionId: 0, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: initialDepositAmount/2.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes2, Test.beFailed()) @@ -121,12 +127,13 @@ fun test_pool_pause_deposit_withdrawal() { // --------------------------------------------------------- // Withdrawing from position should now succeed - let withdrawRes3 = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [0 as UInt64, FLOW_TOKEN_IDENTIFIER, initialDepositAmount/2.0, false], - user1 + let withdrawRes3 = withdrawFromPosition( + signer: user1, + positionId: 0, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: initialDepositAmount/2.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes3, Test.beSucceeded()) - - } diff --git a/cadence/tests/position_health_constraints_test.cdc b/cadence/tests/position_health_constraints_test.cdc index 702b0f8e..c80a2bb4 100644 --- a/cadence/tests/position_health_constraints_test.cdc +++ b/cadence/tests/position_health_constraints_test.cdc @@ -190,10 +190,13 @@ fun test_withdraw_fails_when_health_drops_below_one() { // health = 600 / 615.38 ~ 0.975, well below 1.0. // The preflight check enforces that withdrawals cannot reduce health below minHealth, // which prevents health from ever reaching 1.0. - let withdrawRes = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, FLOW_TOKEN_IDENTIFIER, 250.0, false], - user + let withdrawRes = withdrawFromPosition( + signer: user, + positionId: positionId, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 250.0, + pullFromTopUpSource: false ) Test.expect(withdrawRes, Test.beFailed()) Test.assertError(withdrawRes, errorMessage: "Insufficient funds for withdrawal") diff --git a/cadence/tests/position_lifecycle_unhappy_test.cdc b/cadence/tests/position_lifecycle_unhappy_test.cdc index bc56b949..ad4c79e5 100644 --- a/cadence/tests/position_lifecycle_unhappy_test.cdc +++ b/cadence/tests/position_lifecycle_unhappy_test.cdc @@ -72,20 +72,24 @@ fun testPositionLifecycleBelowMinimumDeposit() { Test.expect(openRes, Test.beSucceeded()) // Attempt to withdraw the exact amount above the minimum - let withdrawResSuccess = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, FLOW_TOKEN_IDENTIFIER, amountAboveMin, true], - user + let withdrawResSuccess = withdrawFromPosition( + signer: user, + positionId: positionId, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: amountAboveMin, + pullFromTopUpSource: true ) - Test.expect(withdrawResSuccess, Test.beSucceeded()) // Amount should now be exactly the minimum, so withdrawal should fail - let withdrawResFail = _executeTransaction( - "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, FLOW_TOKEN_IDENTIFIER, minimum/2.0, true], - user + let withdrawResFail = withdrawFromPosition( + signer: user, + positionId: positionId, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: minimum/2.0, + pullFromTopUpSource: true ) - Test.expect(withdrawResFail, Test.beFailed()) } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index e7e77b63..1a3fb991 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -50,6 +50,7 @@ access(all) let MAINNET_PROTOCOL_ACCOUNT_ADDRESS: Address = 0x6b00ff876c299c61 access(all) let MAINNET_USDF_HOLDER_ADDRESS: Address = 0xf18b50870aed46ad access(all) let MAINNET_WETH_HOLDER_ADDRESS: Address = 0xf62e3381a164f993 access(all) let MAINNET_WBTC_HOLDER_ADDRESS: Address = 0x47f544294e3b7656 +access(all) let MAINNET_FLOW_HOLDER_ADDRESS: Address = 0x92674150c9213fc9 /* --- Test execution helpers --- */ @@ -552,13 +553,13 @@ fun borrowFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeId } access(all) -fun withdrawFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeIdentifier: String, amount: UFix64, pullFromTopUpSource: Bool) { +fun withdrawFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeIdentifier: String, receiverVaultStoragePath: StoragePath, amount: UFix64, pullFromTopUpSource: Bool): Test.TransactionResult{ let withdrawRes = _executeTransaction( "./transactions/position-manager/withdraw_from_position.cdc", - [positionId, tokenTypeIdentifier, amount, pullFromTopUpSource], + [positionId, tokenTypeIdentifier, receiverVaultStoragePath, amount, pullFromTopUpSource], signer ) - Test.expect(withdrawRes, Test.beSucceeded()) + return withdrawRes } access(all) diff --git a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc index 75bbe4b9..3756cdc8 100644 --- a/cadence/tests/transactions/position-manager/withdraw_from_position.cdc +++ b/cadence/tests/transactions/position-manager/withdraw_from_position.cdc @@ -11,6 +11,7 @@ import "FlowALPv0" transaction( positionId: UInt64, tokenTypeIdentifier: String, + receiverVaultStoragePath: StoragePath, amount: UFix64, pullFromTopUpSource: Bool ) { @@ -18,12 +19,11 @@ transaction( let tokenType: Type let receiverVault: &{FungibleToken.Receiver} - prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + prepare(signer: auth(BorrowValue) &Account) { // Borrow the PositionManager from constant storage path let manager = signer.storage.borrow( - from: FlowALPv0.PositionStoragePath - ) - ?? panic("Could not find PositionManager in signer's storage") + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not find PositionManager in signer's storage") // Borrow the position with withdraw entitlement self.position = manager.borrowAuthorizedPosition(pid: positionId) @@ -32,21 +32,8 @@ transaction( self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier: \(tokenTypeIdentifier)") - // Ensure signer has a FlowToken vault to receive withdrawn tokens - if signer.storage.type(at: /storage/flowTokenVault) == nil { - signer.storage.save(<-FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()), to: /storage/flowTokenVault) - } - - // Get receiver for the specific token type - // For FlowToken, use the standard path - if tokenTypeIdentifier == "A.0000000000000003.FlowToken.Vault" { - self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: /storage/flowTokenVault) - ?? panic("Could not borrow FlowToken vault receiver") - } else { - // For other tokens, try to find a matching vault - // This is a simplified approach for testing - panic("Unsupported token type for withdrawal: \(tokenTypeIdentifier)") - } + self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: receiverVaultStoragePath) + ?? panic("Could not borrow receiver vault at \(receiverVaultStoragePath)") } execute { @@ -60,4 +47,4 @@ transaction( // Deposit the withdrawn tokens to the signer's vault self.receiverVault.deposit(from: <-withdrawnVault) } -} +} \ No newline at end of file From 078850982f7d37deafb853649776b9da77ff1c2c Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Thu, 26 Feb 2026 15:48:00 +0200 Subject: [PATCH 2/6] Refactored setInsuranceSwapper test helper, updated usages --- cadence/tests/governance_parameters_test.cdc | 3 +- .../insurance_collection_formula_test.cdc | 7 ++- cadence/tests/insurance_collection_test.cdc | 56 ++++++++++++++++--- cadence/tests/insurance_rate_test.cdc | 22 +++++--- cadence/tests/insurance_swapper_test.cdc | 24 +++++--- .../interest_accrual_integration_test.cdc | 18 ++++-- .../tests/interest_curve_advanced_test.cdc | 5 +- cadence/tests/test_helpers.cdc | 5 +- 8 files changed, 104 insertions(+), 36 deletions(-) diff --git a/cadence/tests/governance_parameters_test.cdc b/cadence/tests/governance_parameters_test.cdc index bdc79d18..53803433 100644 --- a/cadence/tests/governance_parameters_test.cdc +++ b/cadence/tests/governance_parameters_test.cdc @@ -17,7 +17,8 @@ fun test_setGovernanceParams_and_exercise_paths() { // 1) Set insurance swapper let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/insurance_collection_formula_test.cdc b/cadence/tests/insurance_collection_formula_test.cdc index 27c7cc39..bb0b475c 100644 --- a/cadence/tests/insurance_collection_formula_test.cdc +++ b/cadence/tests/insurance_collection_formula_test.cdc @@ -58,7 +58,12 @@ fun test_collectInsurance_success_fullAmount() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 10% annual debit rate diff --git a/cadence/tests/insurance_collection_test.cdc b/cadence/tests/insurance_collection_test.cdc index 87aa8d96..f64a683f 100644 --- a/cadence/tests/insurance_collection_test.cdc +++ b/cadence/tests/insurance_collection_test.cdc @@ -80,7 +80,12 @@ fun test_collectInsurance_zeroDebitBalance_returnsNil() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // verify initial insurance fund balance is 0 @@ -129,7 +134,12 @@ fun test_collectInsurance_partialReserves_collectsAvailable() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 90% annual debit rate @@ -182,7 +192,12 @@ fun test_collectInsurance_tinyAmount_roundsToZero_returnsNil() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper with very low rate - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set a very low insurance rate @@ -232,7 +247,12 @@ fun test_collectInsurance_success_fullAmount() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) // configure insurance swapper (1:1 ratio) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 10% annual debit rate @@ -318,10 +338,20 @@ fun test_collectInsurance_multipleTokens() { mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 20000.0, beFailed: false) // configure insurance swappers for both tokens (both swap to MOET at 1:1) - let moetSwapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0) + let moetSwapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(moetSwapperResult, Test.beSucceeded()) - let flowSwapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 1.0) + let flowSwapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(flowSwapperResult, Test.beSucceeded()) // set 10% annual debit rates @@ -421,7 +451,12 @@ fun test_collectInsurance_dexOracleSlippageProtection() { // Oracle says FLOW = 1.0 MOET (already set in setup()) // Configure insurance swapper with price ratio = 0.5 (50% deviation from oracle) - let swapperResult = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 0.5) + let swapperResult = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 0.5, + ) Test.expect(swapperResult, Test.beSucceeded()) // set 10% annual debit rate and 10% insurance rate @@ -441,7 +476,12 @@ fun test_collectInsurance_dexOracleSlippageProtection() { Test.assertEqual(0.0, balanceAfterFailure) // Now reconfigure swapper with price ratio = 1.0 (matches oracle, 0% deviation) - let swapperResult2 = setInsuranceSwapper(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, priceRatio: 1.0) + let swapperResult2 = setInsuranceSwapper( + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, + priceRatio: 1.0, + ) Test.expect(swapperResult2, Test.beSucceeded()) // collect insurance for FLOW - should SUCCEED now diff --git a/cadence/tests/insurance_rate_test.cdc b/cadence/tests/insurance_rate_test.cdc index 50b52cbb..5830b14e 100644 --- a/cadence/tests/insurance_rate_test.cdc +++ b/cadence/tests/insurance_rate_test.cdc @@ -11,7 +11,6 @@ access(all) fun setup() { deployContracts() createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) - // take snapshot first, then advance time so reset() target is always lower than current height snapshot = getCurrentBlockHeight() // move time by 1 second so Test.reset() works properly before each test @@ -32,7 +31,8 @@ fun test_setInsuranceRate_withoutEGovernanceEntitlement() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -56,7 +56,8 @@ fun test_setInsuranceRate_withEGovernanceEntitlement() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -104,7 +105,8 @@ fun test_setInsuranceRate_rateGreaterThanOne_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -131,7 +133,8 @@ fun test_setInsuranceRate_combinedRateExceedsOne_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -164,7 +167,8 @@ fun test_setStabilityFeeRate_combinedRateExceedsOne_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -197,7 +201,8 @@ fun test_setInsuranceRate_rateLessThanZero_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -223,7 +228,8 @@ fun test_setInsuranceRate_invalidTokenType_fails() { // set insurance swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/insurance_swapper_test.cdc b/cadence/tests/insurance_swapper_test.cdc index 8885b6a6..3a124a6c 100644 --- a/cadence/tests/insurance_swapper_test.cdc +++ b/cadence/tests/insurance_swapper_test.cdc @@ -20,7 +20,8 @@ access(all) fun test_setInsuranceSwapper_success() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -39,7 +40,8 @@ fun test_setInsuranceSwapper_updateExistingSwapper_success() { let initialPriceRatio = 1.0 let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: initialPriceRatio, ) Test.expect(res, Test.beSucceeded()) @@ -48,7 +50,8 @@ fun test_setInsuranceSwapper_updateExistingSwapper_success() { let updatedPriceRatio = 2.0 let updatedRes = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: updatedPriceRatio, ) Test.expect(updatedRes, Test.beSucceeded()) @@ -66,7 +69,8 @@ fun test_removeInsuranceSwapper_success() { // set a swapper let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -94,7 +98,8 @@ fun test_remove_insuranceSwapper_failed() { // set a swapper var res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -130,7 +135,8 @@ access(all) fun test_setInsuranceSwapper_withoutEGovernanceEntitlement_fails() { let res = setInsuranceSwapper( signer: alice, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) @@ -148,7 +154,8 @@ fun test_setInsuranceSwapper_invalidTokenTypeIdentifier_fails() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: invalidTokenIdentifier, + swapperInTypeIdentifier: invalidTokenIdentifier, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) @@ -166,7 +173,8 @@ fun test_setInsuranceSwapper_emptyTokenTypeIdentifier_fails() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: emptyTokenIdentifier, + swapperInTypeIdentifier: emptyTokenIdentifier, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index d8d86a78..f93f7630 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -172,7 +172,8 @@ fun test_moet_debit_accrues_interest() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -403,7 +404,8 @@ fun test_moet_credit_accrues_interest_with_insurance() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -589,7 +591,8 @@ fun test_flow_debit_accrues_interest() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -777,7 +780,8 @@ fun test_flow_credit_accrues_interest_with_insurance() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -955,7 +959,8 @@ fun test_insurance_deduction_verification() { // Expected Credit Rate: 10% - 1% = 9% let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) @@ -1176,7 +1181,8 @@ fun test_combined_all_interest_scenarios() { let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/interest_curve_advanced_test.cdc b/cadence/tests/interest_curve_advanced_test.cdc index 09355ddb..866b614e 100644 --- a/cadence/tests/interest_curve_advanced_test.cdc +++ b/cadence/tests/interest_curve_advanced_test.cdc @@ -118,8 +118,9 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // set insurance swapper let res = setInsuranceSwapper( - signer: PROTOCOL_ACCOUNT, - tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, + signer: PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MOET_TOKEN_IDENTIFIER, + swapperOutTypeIdentifier: MOET_TOKEN_IDENTIFIER, priceRatio: 1.0, ) Test.expect(res, Test.beSucceeded()) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 1a3fb991..7e625ad6 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -631,12 +631,13 @@ fun setInsuranceRate( access(all) fun setInsuranceSwapper( signer: Test.TestAccount, - tokenTypeIdentifier: String, + swapperInTypeIdentifier: String, + swapperOutTypeIdentifier: String, priceRatio: UFix64, ): Test.TransactionResult { let res = _executeTransaction( "./transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc", - [ tokenTypeIdentifier, priceRatio, tokenTypeIdentifier, MOET_TOKEN_IDENTIFIER], + [ swapperInTypeIdentifier, priceRatio, swapperInTypeIdentifier, swapperOutTypeIdentifier], signer ) return res From d120405fb40d1ccea363ec1bf8540dadf06f14d7 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Mon, 9 Mar 2026 12:38:05 +0200 Subject: [PATCH 3/6] Added boundary conditions tests for interest rate --- ...rsarial_recursive_withdraw_source_test.cdc | 1 - cadence/tests/fork_interest_rate_test.cdc | 598 ++++++++++++++++++ .../interest_accrual_integration_test.cdc | 2 +- 3 files changed, 599 insertions(+), 2 deletions(-) create mode 100644 cadence/tests/fork_interest_rate_test.cdc diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index d4cd85e9..7a2315bb 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -126,7 +126,6 @@ fun testRecursiveWithdrawSource() { receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1500.0, pullFromTopUpSource: true - ) Test.expect(withdrawRes, Test.beFailed()) diff --git a/cadence/tests/fork_interest_rate_test.cdc b/cadence/tests/fork_interest_rate_test.cdc new file mode 100644 index 00000000..8d90b96e --- /dev/null +++ b/cadence/tests/fork_interest_rate_test.cdc @@ -0,0 +1,598 @@ +#test_fork(network: "mainnet", height: 142528994) + +import Test +import BlockchainHelpers + +import "FlowToken" +import "FungibleToken" +import "MOET" +import "FlowALPv0" + +import "test_helpers.cdc" + +access(all) let MAINNET_PROTOCOL_ACCOUNT = Test.getAccount(MAINNET_PROTOCOL_ACCOUNT_ADDRESS) +access(all) let MAINNET_USDF_HOLDER = Test.getAccount(MAINNET_USDF_HOLDER_ADDRESS) +access(all) let MAINNET_WETH_HOLDER = Test.getAccount(MAINNET_WETH_HOLDER_ADDRESS) +access(all) let MAINNET_WBTC_HOLDER = Test.getAccount(MAINNET_WBTC_HOLDER_ADDRESS) +access(all) let MAINNET_FLOW_HOLDER = Test.getAccount(MAINNET_FLOW_HOLDER_ADDRESS) + +access(all) var snapshot: UInt64 = 0 + +// KinkCurve parameters (Aave v3 Volatile One) +access(all) let flowOptimalUtilization: UFix128 = 0.45 // 45% kink point +access(all) let flowBaseRate: UFix128 = 0.0 // 0% base rate +access(all) let flowSlope1: UFix128 = 0.04 // 4% slope below kink +access(all) let flowSlope2: UFix128 = 3.0 // 300% slope above kink + +// Fixed rate for MOET +access(all) let moetFixedRate: UFix128 = 0.04 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + var err = Test.deployContract( + name: "DeFiActionsUtils", + path: "../../FlowActions/cadence/contracts/utils/DeFiActionsUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPMath", + path: "../lib/FlowALPMath.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "DeFiActions", + path: "../../FlowActions/cadence/contracts/interfaces/DeFiActions.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "MockOracle", + path: "../contracts/mocks/MockOracle.cdc", + arguments: [MAINNET_MOET_TOKEN_ID] + ) + Test.expect(err, Test.beNil()) + + // Deploy FungibleTokenConnectors + err = Test.deployContract( + name: "FungibleTokenConnectors", + path: "../../FlowActions/cadence/contracts/connectors/FungibleTokenConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "MockDexSwapper", + path: "../contracts/mocks/MockDexSwapper.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPv0", + path: "../contracts/FlowALPv0.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + createAndStorePool(signer: MAINNET_PROTOCOL_ACCOUNT, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) + + addSupportedTokenKinkCurve( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + collateralFactor: 0.8, + borrowFactor: 0.9, + optimalUtilization: flowOptimalUtilization, + baseRate: flowBaseRate, + slope1: flowSlope1, + slope2: flowSlope2, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // set MOET to use a FixedRateInterestCurve at 4% APY. + setInterestCurveFixed( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, + yearlyRate: moetFixedRate + ) + + let res = setInsuranceSwapper( + signer: MAINNET_PROTOCOL_ACCOUNT, + swapperInTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + swapperOutTypeIdentifier: MAINNET_MOET_TOKEN_ID, + priceRatio: 1.0, + ) + Test.expect(res, Test.beSucceeded()) + + let setInsRes = setInsuranceRate( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + insuranceRate: 0.001, + ) + Test.expect(setInsRes, Test.beSucceeded()) + + snapshot = getCurrentBlockHeight() +} + + +// ============================================================================= +/// Verifies the scenario when there is no liquidity to borrow. +/// Any attempt to borrow should fail because the pool has no reserves for that token. +/// When new deposit goes, user could borrow. +// ============================================================================= +access(all) +fun test_zero_credit_balance() { + safeReset() + + // setup borrower, create MOET position + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + + let MOETAmount = 10_000.0 + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: MOETAmount, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: MOETAmount, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + // no Flow LP is created — pool has zero FLOW liquidity + + // attempt to borrow FLOW (no reserves) + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + let borrowRes = _executeTransaction( + "./transactions/position-manager/borrow_from_position.cdc", + [pid, MAINNET_FLOW_TOKEN_ID, FLOW_VAULT_STORAGE_PATH, 100.0], + borrower + ) + Test.expect(borrowRes, Test.beFailed()) + + // FLOW interest rate calculation + // + // totalCreditBalance = 0 + // totalDebitBalance = 0 + // baseRate = 0 + // + // KinkInterestCurve: + // debitRate: + // debitRate = (if no debt, debitRate = base rate) = 0 + // + // creditRate: + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate + // + // debitIncome = 0.0 * 0.0 = 0.0 + // protocolFeeAmount = 0.0 + // totalCreditBalance = 0.0 -> creditRate = 0.0 + + // MOET interest rate calculation (FixedRateInterestCurve) + // + // totalCreditBalance = 10000 + // totalDebitBalance = 0 + // + // debitRate: + // debitRate = yearlyRate = 0.04 + // + // creditRate: + // creditRate = debitRate * (1.0 - protocolFeeRate) + // protocolFeeRate = insuranceRate + stabilityFeeRate + // + // protocolFeeRate = 0.001 * 0.05 = 0.051 + // creditRate = 0.04 * (1 - 0.051) = 0.03796 (3.796 % APY) + // + + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 30DaysGrowth = perSecondRate^THIRTY_DAYS - 1 + // + // FLOW debt 30 days growth = (1 + 0/31_557_600)^2_592_000 - 1 = 0 + // MOET credit 30 days growth = (1 + 0.03796/31_557_600)^2_592_000 - 1 = 0.0003122730069 + let detailsAfterTime = getPositionDetails(pid: pid, beFailed: false) + let moetCredit = getCreditBalanceForType(details: detailsAfterTime, vaultType: Type<@MOET.Vault>()) + Test.assert(moetCredit > 10000.0, message: "MOET credit should accrue interest") + + // add FLOW liquidity + let FLOWAmount = 5000.0 + let flowLp = Test.createAccount() + transferFungibleTokens( + tokenIdentifier: MAINNET_FLOW_TOKEN_ID, + from: MAINNET_FLOW_HOLDER, + to: flowLp, + amount: FLOWAmount + ) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: flowLp, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + // borrow FLOW (Flow LP deposited 5000.0 FLOW, liquidity now available) + borrowFromPosition( + signer: borrower, + positionId: pid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 100.0, + beFailed: false + ) + + let details = getPositionDetails(pid: pid, beFailed: false) + let flowDebt = getDebitBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) + Test.assertEqual(100.0, flowDebt) + + // FLOW interest rate calculation (KinkInterestCurve) + // + // totalCreditBalance = 5000 + // totalDebitBalance = 100 + // + // debitRate: + // utilization = debitBalance / (creditBalance + debitBalance) + // utilization = 100 / (5000 + 100) = 100 / 5100 = 0.01960784 < 0.45 (below kink) + // + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // debitRate = 0.0 + (0.04 * 0.01960784 / 0.45) = 0.00174291 (0.174% APY) + // + // creditRate: + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate + // + // debitIncome = 0.0 * 0.0 = 0.0 + // protocolFeeAmount = 0.0 + // totalCreditBalance = 0.0 -> creditRate = 0.0 + + // Advance 1 day to measure exact interest growth + Test.moveTime(by: DAY) + Test.commitBlock() + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // daily growth rate = perSecondRate^86400 - 1 + // FLOW debt daily growth rate = (1 + 0.00174291 / 31_557_600)^86400 - 1 = 0.00000477 + let expectedFlowDebtDailyGrowth = 0.00000477 + + let detailsAfter1Day = getPositionDetails(pid: pid, beFailed: false) + let flowDebtAfter1Day = getDebitBalanceForType(details: detailsAfter1Day, vaultType: Type<@FlowToken.Vault>()) + let flowDebtDailyGrowth = (flowDebtAfter1Day - flowDebt) / flowDebt + Test.assertEqual(expectedFlowDebtDailyGrowth, flowDebtDailyGrowth) +} + +// ============================================================================= +/// Verifies protocol behavior when a lending pool has liquidity but no borrows. +// ============================================================================= +access(all) +fun test_empty_pool() { + safeReset() + + // create Flow LP only — no borrowers + let flowLp = Test.createAccount() + let FLOWAmount = 10000.0 + transferFungibleTokens(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: flowLp, amount: FLOWAmount) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: flowLp, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let lpPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // record initial credit + let detailsBefore = getPositionDetails(pid: lpPid, beFailed: false) + let FLOWcreditBefore = getCreditBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + + // advance 30 days with zero borrowing + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // calculate FLOW rates + // KinkCurve: + // baseRate:0 + // debitBalance:0 + // debitRate: debitRate = baseRate = 0 (debitBalance = 0) + // creditRate: creditRate = 0 (debitIncome = 0) + let detailsAfterNoDebit = getPositionDetails(pid: lpPid, beFailed: false) + let FLOWCreditAfterNoDebit = getCreditBalanceForType(details: detailsAfterNoDebit, vaultType: Type<@FlowToken.Vault>()) + Test.assertEqual(FLOWcreditBefore, FLOWCreditAfterNoDebit) + + // create a borrower to trigger utilization + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 10_000.0, beFailed: false) + + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 10_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + let borrowerPid: UInt64 = 1 + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 2_000.0, + beFailed: false + ) + + // advance another 30 days + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // KinkCurve + // utilization = debitBalance / (creditBalance + debitBalance) + // FLOW: 2000 / (10000 + 2000) = 0.16666666666 < 0.45 (below kink) + // + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0 + 0.04 * (0.16666666666 / 0.45) = 0.01481481481 (1.48% APY) + // + // creditRate: + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate + // + // debitIncome = 2000 * 0.01481481481 = 29.62962962 + // protocolFeeAmount = 29.62962962 * (0.001 + 0.05) = 1.51111111062 + // FLOW: creditRate = (29.62962962 - 1.51111111062) / 10000 = 0.00281185185 (0.28% APY) + + let detailsAfterDebit = getPositionDetails(pid: lpPid, beFailed: false) + let FLOWCreditAfterDebit = getCreditBalanceForType(details: detailsAfterDebit, vaultType: Type<@FlowToken.Vault>()) + + let FLOWCreditGrowth = FLOWCreditAfterDebit - FLOWCreditAfterNoDebit + let FLOWCreditGrowthRate = FLOWCreditGrowth / FLOWCreditAfterNoDebit + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 30 Days Growth = perSecondRate^THIRTY_DAYS - 1 + // FLOW credit 30 days growth = (1 + 0.00281185/31_557_600)^2_592_000 - 1 = 0.0002309792 + let expectedFLOWCreditGrowthRate = 0.00023097 + Test.assertEqual(expectedFLOWCreditGrowthRate, FLOWCreditGrowthRate) +} + +// ============================================================================= +/// Verifies correct interest rate behavior at the utilization kink point. +// ============================================================================= +access(all) +fun test_kink_point_transition() { + safeReset() + + setInterestCurveKink( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + optimalUtilization: flowOptimalUtilization, + baseRate: flowBaseRate, + slope1: flowSlope1, + slope2: flowSlope2 + ) + + // create LP with 10000 FLOW + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + var openEvents = Test.eventsOfType(Type()) + let lpPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // create borrower with large MOET collateral + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 100_000.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 100_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // KinkCurve + // To achieve exactly 45% utilization: + // utilization = debit / (credit + debit) + // 0.45 = debit / (credit + debit) + // + // 0.45 * credit = 0.55 * debit + // credit = (0.55 / 0.45) * debit = (11/9) * debit + // + // credit = 10000 + // debit = 10000 * 9/11 = 8181.818181 + // + // utilization = 8181.818181 / (10000 + 8181.818181) = 0.45 + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 8181.818181, + beFailed: false + ) + + // KinkCurve + // utilization = debitBalance / (creditBalance + debitBalance) + // FLOW: 0.45 <= 0.45 (below kink) + // + // debit rate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0.0 + (0.04 * 0.45 / 0.45) = 0.04 (4% APY) + + // record state at kink point + let detailsAtKink = getPositionDetails(pid: borrowerPid, beFailed: false) + let debtAtKink = getDebitBalanceForType(details: detailsAtKink, vaultType: Type<@FlowToken.Vault>()) + + // advance 1 year and verify rate matches 4% APY + Test.moveTime(by: ONE_YEAR) + Test.commitBlock() + + let detailsAfterYear = getPositionDetails(pid: borrowerPid, beFailed: false) + let debtAfterYear = getDebitBalanceForType(details: detailsAfterYear, vaultType: Type<@FlowToken.Vault>()) + let yearlyGrowthAtKink = (debtAfterYear - debtAtKink) / debtAtKink + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // yearly debt growth = perSecondRate^ONE_YEAR - 1 + // FLOW debit yearly growth = (1 + 0.04 / 31_557_600)^31_557_600 - 1 = 0.04081077417 (4.08%) + let expectedYearlyGrowthAtKink = 0.04081077 + Test.assertEqual(expectedYearlyGrowthAtKink, yearlyGrowthAtKink) +} + +// ============================================================================= +/// Verifies interest accrual over long time periods. +/// Advances blockchain time by 1 year and then by 10 additional years, +/// ensuring the borrower’s debt grows according to the expected +/// compounded interest rate without overflow or precision issues. +// ============================================================================= +access(all) +fun test_long_time_period_accrual() { + safeReset() + + // create LP with 10000 FLOW + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + var openEvents = Test.eventsOfType(Type()) + let lpPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // create borrower + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 100_000.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 100_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // borrow 2000 FLOW + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 2000.0, + beFailed: false + ) + + // Borrower (MOET collateral, FLOW debt): + // KinkCurve + // utilization = debitBalance / (creditBalance + debitBalance) + // FLOW: 2000 / (10000 + 2000) = 0.16666666666 < 0.45 (below kink) + // + // debit rate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0 + 0.04 * (0.16666666666 / 0.45) = 0.01481481481 (1.48% APY) + let expectedFLOWDebtRate = 0.01481481 + + let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) + let debtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + + let creditBefore = getCreditBalanceForType(details: getPositionDetails(pid: lpPid, beFailed: false), + vaultType: Type<@FlowToken.Vault>() + ) + + // 1 full year + Test.moveTime(by: ONE_YEAR) + Test.commitBlock() + + let detailsAfter1Year = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter1Year = getDebitBalanceForType(details: detailsAfter1Year, vaultType: Type<@FlowToken.Vault>()) + let FLOWGrowthRate1Year = (FLOWDebtAfter1Year - debtBefore) / debtBefore + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // yearly debt growth = perSecondRate^ONE_YEAR - 1 + // FLOW debit yearly growth = (1 + 0.01481481481 / 31_557_600)^31_557_600 - 1 = 0.01492509772 + let expectedFLOWDebtYearlyGrowth = 0.01492509 // TODO(Uliana) + add credit rate growth + Test.assertEqual(expectedFLOWDebtYearlyGrowth, FLOWGrowthRate1Year) + + // LP credit should also have grown + let creditAfter1Year = getCreditBalanceForType( + details: getPositionDetails(pid: lpPid, beFailed: false), + vaultType: Type<@FlowToken.Vault>() + ) + Test.assert(creditAfter1Year > creditBefore, message: "credit should grow over 1 year") + + // advance 10 years + Test.moveTime(by: 10.0 * ONE_YEAR) + Test.commitBlock() + + let detailsAfter10Years = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter10Years = getDebitBalanceForType(details: detailsAfter10Years, vaultType: Type<@FlowToken.Vault>()) + let FLOWTotalGrowthRate = (FLOWDebtAfter10Years - debtBefore) / debtBefore + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 11-years debt growth = perSecondRate^(31_557_600*11) - 1 + // FLOW debit 11-years growth = (1 + 0.01481481481 / 31_557_600)^(31_557_600*11) - 1 = 0.17699309112 + let expectedFLOWDebt10YearsGrowth = 0.17699309 + Test.assertEqual(expectedFLOWDebt10YearsGrowth, FLOWTotalGrowthRate) +} + +// ============================================================================= +/// Verifies that interest accrues correctly after large time jumps. +/// Simulates blockchain halts (1 day and 7 days) and ensures the borrower’s +/// debt increases according to the expected compounded interest rate. +// ============================================================================= +access(all) +fun test_time_jump_scenarios() { + safeReset() + + // set up LP and borrower + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + var openEvents = Test.eventsOfType(Type()) + let lpPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 50_000.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 50_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // borrow 5000 FLOW + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 5000.0, + beFailed: false + ) + + // record state before the 1-day gap + let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + let healthBefore = detailsBefore.health + + // Borrower (MOET collateral, FLOW debt): + // KinkCurve + // utilization = debitBalance / (creditBalance + debitBalance) + // FLOW: 5000 / (10000 + 5000) = 0.3(3) < 0.45 (below kink) + // + // debit rate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0 + (0.04 * 0.3(3) / 0.45) = 0.0296296296296296 (2.96% APY) + let expectedFlowDebitRate: UFix128 = 0.02962963 + + // 1-day blockchain halt + Test.moveTime(by: DAY) + Test.commitBlock() + + // first transaction after restart — interest accrual for full gap + let detailsAfter1Day = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter1Day = getDebitBalanceForType(details: detailsAfter1Day, vaultType: Type<@FlowToken.Vault>()) + + Test.assert(FLOWDebtAfter1Day > FLOWDebtBefore, message: "Debt should increase after 1-day gap") + let FLOWDebtDailyGrowth = (FLOWDebtAfter1Day - FLOWDebtBefore) / FLOWDebtBefore + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // dailyGrowth = perSecondRate^86400 - 1 + // FLOW debit daily growth = (1 + 0.02962963 / 31_557_600)^86400 - 1 = 0.00008112479 + + let expectedFLOWDebtDailyGrowth = 0.00008112 + Test.assertEqual(expectedFLOWDebtDailyGrowth, FLOWDebtDailyGrowth) + + // try oto test longer period (7 days) to verify no overflow in calculation + let detailsBefore7Day = getPositionDetails(pid: borrowerPid, beFailed: false) + let FlowDebtBefore7Day = getDebitBalanceForType(details: detailsBefore7Day, vaultType: Type<@FlowToken.Vault>()) + + // 7 days blockchain halt + Test.moveTime(by: 7.0 * DAY) + Test.commitBlock() + + let detailsAfter7Day = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter7Day = getDebitBalanceForType(details: detailsAfter7Day, vaultType: Type<@FlowToken.Vault>()) + Test.assert(FLOWDebtAfter7Day > FlowDebtBefore7Day, message: "FLOW Debt should increase after 7-day gap") + + let FLOWDebtWeeklyGrowth = (FLOWDebtAfter7Day - FlowDebtBefore7Day) / FlowDebtBefore7Day + // weeklyGrowth = perSecondRate^604800 - 1 + // FLOW debit weekly growth = (1 + 0.02962963/31_557_600)^604800 - 1 = 0.00056801 + let expectedFLOWDebtWeeklyGrowth = 0.00056801 + Test.assertEqual(expectedFLOWDebtWeeklyGrowth, FLOWDebtWeeklyGrowth) +} \ No newline at end of file diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index f93f7630..89f878ef 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -43,7 +43,7 @@ access(all) var snapshot: UInt64 = 0 // MOET: FixedRateInterestCurve (Spread Model) // ----------------------------------------------------------------------------- // In the spread model, the curve defines the DEBIT rate (what borrowers pay). -// The CREDIT rate is derived as: creditRate = debitRate - insuranceRate +// The CREDIT rate is derived as: creditRate = debitRate - protocolRate // This ensures lenders always earn less than borrowers pay, with the // difference going to the insurance pool for protocol solvency. // From 8c05412d425bad3b7127842ead6a89c6ea55bdd9 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Mon, 9 Mar 2026 16:29:27 +0200 Subject: [PATCH 4/6] Added test for extreme utilization on Kink interest curve --- cadence/tests/fork_interest_rate_test.cdc | 99 +++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/cadence/tests/fork_interest_rate_test.cdc b/cadence/tests/fork_interest_rate_test.cdc index 8d90b96e..e55d5480 100644 --- a/cadence/tests/fork_interest_rate_test.cdc +++ b/cadence/tests/fork_interest_rate_test.cdc @@ -128,6 +128,105 @@ fun setup() { snapshot = getCurrentBlockHeight() } +// ============================================================================= +/// Verifies extreme utilization (nearly all liquidity borrowed), KinkCurve Steep Slope Behavior +// ============================================================================= +access(all) +fun test_extreme_utilization() { + safeReset() + + setInterestCurveKink( + signer: MAINNET_PROTOCOL_ACCOUNT, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + optimalUtilization: flowOptimalUtilization, + baseRate: flowBaseRate, + slope1: flowSlope1, + slope2: flowSlope2 + ) + + // create Flow LP with 2000 FLOW + let FLOWAmount = 2000.0 + + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + var openEvents = Test.eventsOfType(Type()) + let lpDepositPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // create borrower with MOET collateral + let borrower = Test.createAccount() + setupMoetVault(borrower, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 10_000.0, beFailed: false) + + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 10_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) + + openEvents = Test.eventsOfType(Type()) + let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // borrow 999 FLOW + borrowFromPosition( + signer: borrower, + positionId: borrowerPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + vaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 999.0, + beFailed: false + ) + + // LP withdraws 1000 FLOW to push utilization higher + let withdrawRes = withdrawFromPosition( + signer: MAINNET_FLOW_HOLDER, + positionId: lpDepositPid, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, + amount: 1000.0, + pullFromTopUpSource: false, + ) + + // Pool state: + // FLOW credit = 2000 - 1000 = 1000 + // FLOW debit = 999 + // + // KinkInterestCurve: + // utilization = debitBalance / (creditBalance + debitBalance) //TODO(Uliana): nearly all liquidity borrowed, but utilization = 49% + // utilization = 999 / (1000 + 999) = 999 / 1999 = 0.4997498749 = 49.9% > 45% + // + // rate = baseRate + slope1 + (slope2 * excessUtilization) + // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + /// + // excessUtilization = (0.4997498749 - 0.45) / (1 - 0.45) = 0.090454318 + // rate = 0.0 + 0.04 + 3.0 * 0.090454318 = 0.311362954 (31.13% APY) + + // record initial state + let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + + // Borrower position + // Collateral: + // MOET: 10000 * $1.0 * CF(1.0) = $10000 + // Debt: + // FLOW: 999 * $1.0 / BF(0.9) = $1110 + // + // Health = $10000 / $1110 = 9.00900900900... + + // advance 30 days + Test.moveTime(by: THIRTY_DAYS) + Test.commitBlock() + + // verify debt growth + let detailsAfter = getPositionDetails(pid: borrowerPid, beFailed: false) + let FLOWDebtAfter = getDebitBalanceForType(details: detailsAfter, vaultType: Type<@FlowToken.Vault>()) + let healthAfter = detailsAfter.health + Test.assert(FLOWDebtAfter > FLOWDebtBefore, message: "Debt should increase at above-kink utilization") + + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 30 days growth rate = perSecondRate ^ 2_592_000 - 1 + // FLOW debit 30 days growth rate = (1 + (0.311362954 / 31_557_600))^2_592_000 - 1 = 0.02590377842 = 2.59% + let expectedFLOWGrowthRate = 0.02590377 + let FLOWDebtGrowth = FLOWDebtAfter - FLOWDebtBefore + let FLOWGrowthRate = FLOWDebtGrowth / FLOWDebtBefore + + Test.assert(equalWithinVariance(expectedFLOWGrowthRate, FLOWGrowthRate), + message: "Expected FLOW debt growth rate to be ~\(expectedFLOWGrowthRate), but got \(FLOWGrowthRate)") +} // ============================================================================= /// Verifies the scenario when there is no liquidity to borrow. From 0fce924acda817c62edfb73a14e88854dd3d6e57 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Wed, 11 Mar 2026 14:03:24 +0200 Subject: [PATCH 5/6] Updated tests, merged with utilization fix --- cadence/tests/fork_interest_rate_test.cdc | 276 ++++++++++------------ 1 file changed, 126 insertions(+), 150 deletions(-) diff --git a/cadence/tests/fork_interest_rate_test.cdc b/cadence/tests/fork_interest_rate_test.cdc index 7b75679d..59be7b65 100644 --- a/cadence/tests/fork_interest_rate_test.cdc +++ b/cadence/tests/fork_interest_rate_test.cdc @@ -81,7 +81,7 @@ fun setup() { } // ============================================================================= -/// Verifies protocol behavior when extreme utilization (nearly all liquidity borrowed) and verifies KinkCurve Steep clope Behavior +/// Verifies protocol behavior when extreme utilization (nearly all liquidity borrowed) // ============================================================================= access(all) fun test_extreme_utilization() { @@ -113,77 +113,65 @@ fun test_extreme_utilization() { openEvents = Test.eventsOfType(Type()) let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - // borrow 999 FLOW + // borrow 1800 FLOW (90% of 2000 FLOW credit) borrowFromPosition( signer: borrower, positionId: borrowerPid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - amount: 999.0, + amount: 1800.0, beFailed: false ) - // LP withdraws 1000 FLOW to push utilization higher - let withdrawRes = withdrawFromPosition( - signer: MAINNET_FLOW_HOLDER, - positionId: lpDepositPid, - tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, - receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH, - amount: 1000.0, - pullFromTopUpSource: false, - ) - // Pool state: - // FLOW credit = 2000 - 1000 = 1000 - // FLOW debit = 999 + // FLOW credit = 2000 + // FLOW debit = 1800 // // KinkInterestCurve: - // utilization = debitBalance / (creditBalance + debitBalance) //TODO(Uliana): nearly all liquidity borrowed, but utilization = 49% - // utilization = 999 / (1000 + 999) = 999 / 1999 = 0.4997498749 = 49.9% > 45% + // utilization = debitBalance / creditBalance + // utilization = 1800 / 2000 = 0.9 = 90% > 45% (above kink) // - // rate = baseRate + slope1 + (slope2 * excessUtilization) // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) - /// - // excessUtilization = (0.4997498749 - 0.45) / (1 - 0.45) = 0.090454318 - // rate = 0.0 + 0.04 + 3.0 * 0.090454318 = 0.311362954 (31.13% APY) + // excessUtilization = (0.9 - 0.45) / (1 - 0.45) = 0.45 / 0.55 = 0.81818181818... + // + // rate = baseRate + slope1 + (slope2 * excessUtilization) + // rate = 0.0 + 0.04 + 3.0 * 0.81818181818 = 2.49454545454... (249.45% APY) // record initial state let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) - // Borrower position - // Collateral: - // MOET: 10000 * $1.0 * CF(1.0) = $10000 - // Debt: - // FLOW: 999 * $1.0 / BF(0.9) = $1110 - // - // Health = $10000 / $1110 = 9.00900900900... - // advance 30 days Test.moveTime(by: THIRTY_DAYS) Test.commitBlock() + // perSecondRate = 1 + (yearlyRate / 31_557_600) + // 30 days growth rate = perSecondRate ^ 2_592_000 - 1 + // FLOW debit 30 days growth rate = (1 + 2.49454545 / 31557600)^2592000 - 1 = 0.22739266 //0.22739266 + let expectedFLOWGrowthRate = 0.22739266 + // verify debt growth let detailsAfter = getPositionDetails(pid: borrowerPid, beFailed: false) let FLOWDebtAfter = getDebitBalanceForType(details: detailsAfter, vaultType: Type<@FlowToken.Vault>()) - let healthAfter = detailsAfter.health Test.assert(FLOWDebtAfter > FLOWDebtBefore, message: "Debt should increase at above-kink utilization") - - // perSecondRate = 1 + (yearlyRate / 31_557_600) - // 30 days growth rate = perSecondRate ^ 2_592_000 - 1 - // FLOW debit 30 days growth rate = (1 + (0.311362954 / 31_557_600))^2_592_000 - 1 = 0.02590377842 = 2.59% - let expectedFLOWGrowthRate = 0.02590378 + let FLOWDebtGrowth = FLOWDebtAfter - FLOWDebtBefore let FLOWGrowthRate = FLOWDebtGrowth / FLOWDebtBefore - Test.assert(equalWithinVariance(expectedFLOWGrowthRate, FLOWGrowthRate), - message: "Expected FLOW debt growth rate to be ~\(expectedFLOWGrowthRate), but got \(FLOWGrowthRate)") + // NOTE: TODO(Uliana): update to equalWithinVariance when PR https://github.com/onflow/FlowALP/pull/255 will be merged + // We intentionally do not use `equalWithinVariance` with `defaultUFixVariance` here. + // The default variance is designed for deterministic math, but insurance collection + // depends on block timestamps, which can differ slightly between test runs. + // A larger, time-aware tolerance is required. + let tolerance = 0.00001 + var diff = expectedFLOWGrowthRate > FLOWGrowthRate + ? expectedFLOWGrowthRate - FLOWGrowthRate + : FLOWGrowthRate - expectedFLOWGrowthRate + Test.assert(diff < tolerance, message: "Expected FLOW debt growth rate to be \(expectedFLOWGrowthRate) but got \(FLOWGrowthRate)") } // ============================================================================= -/// Verifies the scenario when there is no liquidity to borrow. -/// Any attempt to borrow should fail because the pool has no reserves for that token. -/// When new deposit goes, user could borrow. +/// Verifies protocol behavior when a lending pool has liquidity but no borrows. // ============================================================================= access(all) fun test_zero_credit_balance() { @@ -216,34 +204,33 @@ fun test_zero_credit_balance() { // totalDebitBalance = 0 // baseRate = 0 // - // debitRate: + // debitRate: // debitRate = (if no debt, debitRate = base rate) = 0 // // creditRate: - // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance - // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) - // debitIncome = totalDebitBalance * debitRate + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate // - // debitIncome = 0.0 * 0.0 = 0.0 - // protocolFeeAmount = 0.0 - // totalCreditBalance = 0.0 -> creditRate = 0.0 + // debitIncome = 0.0 * 0.0 = 0.0 + // protocolFeeAmount = 0.0 + // totalCreditBalance = 0.0 -> creditRate = 0.0 // MOET interest rate calculation (FixedRateInterestCurve) // // totalCreditBalance = 10000 // totalDebitBalance = 0 // - // debitRate: + // debitRate: // debitRate = yearlyRate = 0.04 // // creditRate: - // creditRate = debitRate * (1.0 - protocolFeeRate) - // protocolFeeRate = insuranceRate + stabilityFeeRate - // - // protocolFeeRate = 0.001 * 0.05 = 0.051 - // creditRate = 0.04 * (1 - 0.051) = 0.03796 (3.796 % APY) + // creditRate = debitRate * (1.0 - protocolFeeRate) + // protocolFeeRate = insuranceRate + stabilityFeeRate // - + // protocolFeeRate = 0.001 * 0.05 = 0.051 + // creditRate = 0.04 * (1 - 0.051) = 0.03796 (3.796% APY) + Test.moveTime(by: THIRTY_DAYS) Test.commitBlock() @@ -286,29 +273,30 @@ fun test_zero_credit_balance() { // totalCreditBalance = 5000 // totalDebitBalance = 100 // - // debitRate: - // utilization = debitBalance / (creditBalance + debitBalance) - // utilization = 100 / (5000 + 100) = 100 / 5100 = 0.01960784 < 0.45 (below kink) + // debitRate: + // utilization = debitBalance / creditBalance + // utilization = 100 / 5000 = 0.02 < 0.45 (below kink) // // debitRate = baseRate + (slope1 * utilization / optimalUtilization) - // debitRate = 0.0 + (0.04 * 0.01960784 / 0.45) = 0.00174291 (0.174% APY) + // debitRate = 0.0 + (0.04 * 0.02 / 0.45) = 0.00177777777 (0.177% APY) // // creditRate: - // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance - // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) - // debitIncome = totalDebitBalance * debitRate + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate // - // debitIncome = 0.0 * 0.0 = 0.0 - // protocolFeeAmount = 0.0 - // totalCreditBalance = 0.0 -> creditRate = 0.0 + // debitIncome = 0.0 * 0.0 = 0.0 + // protocolFeeAmount = 0.0 + // totalCreditBalance = 0.0 -> creditRate = 0.0 // Advance 1 day to measure exact interest growth Test.moveTime(by: DAY) Test.commitBlock() + // perSecondRate = 1 + (yearlyRate / 31_557_600) // daily growth rate = perSecondRate^86400 - 1 - // FLOW debt daily growth rate = (1 + 0.00174291 / 31_557_600)^86400 - 1 = 0.00000477 - let expectedFlowDebtDailyGrowth = 0.00000477 + // FLOW debt daily growth rate = (1 + 0.00177777777 / 31_557_600)^86400 - 1 = 0.00000486766 + let expectedFlowDebtDailyGrowth = 0.00000486 let detailsAfter1Day = getPositionDetails(pid: pid, beFailed: false) let flowDebtAfter1Day = getDebitBalanceForType(details: detailsAfter1Day, vaultType: Type<@FlowToken.Vault>()) @@ -334,21 +322,21 @@ fun test_empty_pool() { // record initial credit let detailsBefore = getPositionDetails(pid: lpPid, beFailed: false) - let FLOWcreditBefore = getCreditBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + let FLOWCreditBefore = getCreditBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) // advance 30 days with zero borrowing Test.moveTime(by: THIRTY_DAYS) Test.commitBlock() - // calculate FLOW rates - // KinkCurve: - // baseRate:0 - // debitBalance:0 - // debitRate: debitRate = baseRate = 0 (debitBalance = 0) - // creditRate: creditRate = 0 (debitIncome = 0) + // FLOW rate calculation (KinkInterestCurve) + // baseRate:0 + // debitBalance:0 + // + // debitRate = (if no debt, debitRate = base rate) = 0 + // creditRate = 0 (debitIncome = 0) let detailsAfterNoDebit = getPositionDetails(pid: lpPid, beFailed: false) let FLOWCreditAfterNoDebit = getCreditBalanceForType(details: detailsAfterNoDebit, vaultType: Type<@FlowToken.Vault>()) - Test.assertEqual(FLOWcreditBefore, FLOWCreditAfterNoDebit) + Test.assertEqual(FLOWCreditBefore, FLOWCreditAfterNoDebit) // create a borrower to trigger utilization let borrower = Test.createAccount() @@ -372,20 +360,20 @@ fun test_empty_pool() { Test.commitBlock() // KinkCurve - // utilization = debitBalance / (creditBalance + debitBalance) - // FLOW: 2000 / (10000 + 2000) = 0.16666666666 < 0.45 (below kink) + // utilization = debitBalance / creditBalance + // FLOW: 2000 / 10000 = 0.2 < 0.45 (below kink) // // debitRate = baseRate + (slope1 * utilization / optimalUtilization) - // FLOW: debitRate = 0 + 0.04 * (0.16666666666 / 0.45) = 0.01481481481 (1.48% APY) + // FLOW: debitRate = 0 + 0.04 * (0.2 / 0.45) = 0.01777777777 (1.777% APY) // // creditRate: - // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance - // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) - // debitIncome = totalDebitBalance * debitRate - // - // debitIncome = 2000 * 0.01481481481 = 29.62962962 - // protocolFeeAmount = 29.62962962 * (0.001 + 0.05) = 1.51111111062 - // FLOW: creditRate = (29.62962962 - 1.51111111062) / 10000 = 0.00281185185 (0.28% APY) + // creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance + // protocolFeeAmount = debitIncome * (insuranceRate + stabilityFeeRate) + // debitIncome = totalDebitBalance * debitRate + // + // debitIncome = 2000 * 0.01777777777 = 35.55555554 + // protocolFeeAmount = 35.55555554 * (0.001 + 0.05) = 1.81333333254 + // creditRate = (35.55555554 - 1.81333333254) / 10000 = 0.00337422222 (0.337% APY) let detailsAfterDebit = getPositionDetails(pid: lpPid, beFailed: false) let FLOWCreditAfterDebit = getCreditBalanceForType(details: detailsAfterDebit, vaultType: Type<@FlowToken.Vault>()) @@ -395,8 +383,8 @@ fun test_empty_pool() { // perSecondRate = 1 + (yearlyRate / 31_557_600) // 30 Days Growth = perSecondRate^THIRTY_DAYS - 1 - // FLOW credit 30 days growth = (1 + 0.00281185/31_557_600)^2_592_000 - 1 = 0.0002309792 - let expectedFLOWCreditGrowthRate = 0.00023097 + // FLOW credit 30 days growth = (1 + 0.00337422222 / 31_557_600)^2_592_000 - 1 = 0.00027718 + let expectedFLOWCreditGrowthRate = 0.00027718 Test.assertEqual(expectedFLOWCreditGrowthRate, FLOWCreditGrowthRate) } @@ -424,37 +412,30 @@ fun test_kink_point_transition() { setupMoetVault(borrower, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 100_000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 100_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid // KinkCurve // To achieve exactly 45% utilization: - // utilization = debit / (credit + debit) - // 0.45 = debit / (credit + debit) - // - // 0.45 * credit = 0.55 * debit - // credit = (0.55 / 0.45) * debit = (11/9) * debit - // - // credit = 10000 - // debit = 10000 * 9/11 = 8181.818181 - // - // utilization = 8181.818181 / (10000 + 8181.818181) = 0.45 + // utilization = debit / credit + // 0.45 = debit / 10000 + // debit = 10000 * 0.45 = 4500 borrowFromPosition( signer: borrower, positionId: borrowerPid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, - amount: 8181.818181, + amount: 4500.0, beFailed: false ) // KinkCurve - // utilization = debitBalance / (creditBalance + debitBalance) - // FLOW: 0.45 <= 0.45 (below kink) + // utilization = debitBalance / creditBalance + // FLOW: 4500 / 10000 = 0.45 <= 0.45 (exactly at kink) // - // debit rate = baseRate + (slope1 * utilization / optimalUtilization) - // FLOW: debitRate = 0.0 + (0.04 * 0.45 / 0.45) = 0.04 (4% APY) + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0.0 + (0.04 * 0.45 / 0.45) = 0.04 (4% APY) // record state at kink point let detailsAtKink = getPositionDetails(pid: borrowerPid, beFailed: false) @@ -469,17 +450,14 @@ fun test_kink_point_transition() { let yearlyGrowthAtKink = (debtAfterYear - debtAtKink) / debtAtKink // perSecondRate = 1 + (yearlyRate / 31_557_600) - // yearly debt growth = perSecondRate^ONE_YEAR - 1 + // yearly debt growth = perSecondRate^ONE_YEAR - 1 // FLOW debit yearly growth = (1 + 0.04 / 31_557_600)^31_557_600 - 1 = 0.04081077417 (4.08%) let expectedYearlyGrowthAtKink = 0.04081077 Test.assertEqual(expectedYearlyGrowthAtKink, yearlyGrowthAtKink) } // ============================================================================= -/// Verifies interest accrual over long time periods. -/// Advances blockchain time by 1 year and then by 10 additional years, -/// ensuring the borrower’s debt grows according to the expected -/// compounded interest rate without overflow or precision issues. +/// Verifies interest accrual over long time periods // ============================================================================= access(all) fun test_long_time_period_accrual() { @@ -487,7 +465,7 @@ fun test_long_time_period_accrual() { // create LP with 10000 FLOW createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_FLOW_HOLDER, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + var openEvents = Test.eventsOfType(Type()) let lpPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid @@ -496,7 +474,7 @@ fun test_long_time_period_accrual() { setupMoetVault(borrower, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 100_000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 100_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) - + openEvents = Test.eventsOfType(Type()) let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid @@ -510,21 +488,19 @@ fun test_long_time_period_accrual() { beFailed: false ) - // Borrower (MOET collateral, FLOW debt): + // Borrower FLOW rate calculation (KinkInterestCurve) // KinkCurve - // utilization = debitBalance / (creditBalance + debitBalance) - // FLOW: 2000 / (10000 + 2000) = 0.16666666666 < 0.45 (below kink) + // utilization = debitBalance / creditBalance + // FLOW: 2000 / 10000 = 0.2 < 0.45 (below kink) // - // debit rate = baseRate + (slope1 * utilization / optimalUtilization) - // FLOW: debitRate = 0 + 0.04 * (0.16666666666 / 0.45) = 0.01481481481 (1.48% APY) - let expectedFLOWDebtRate = 0.01481481 + // debitRate = baseRate + (slope1 * utilization / optimalUtilization) + // FLOW: debitRate = 0 + 0.04 * (0.2 / 0.45) = 0.01777777777 (1.77% APY) + let expectedFLOWDebtRate = 0.01777777 let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) - let debtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) + let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) - let creditBefore = getCreditBalanceForType(details: getPositionDetails(pid: lpPid, beFailed: false), - vaultType: Type<@FlowToken.Vault>() - ) + let FLOWCreditBefore = getCreditBalanceForType(details: getPositionDetails(pid: lpPid, beFailed: false), vaultType: Type<@FlowToken.Vault>()) // 1 full year Test.moveTime(by: ONE_YEAR) @@ -532,12 +508,12 @@ fun test_long_time_period_accrual() { let detailsAfter1Year = getPositionDetails(pid: borrowerPid, beFailed: false) let FLOWDebtAfter1Year = getDebitBalanceForType(details: detailsAfter1Year, vaultType: Type<@FlowToken.Vault>()) - let FLOWGrowthRate1Year = (FLOWDebtAfter1Year - debtBefore) / debtBefore + let FLOWGrowthRate1Year = (FLOWDebtAfter1Year - FLOWDebtBefore) / FLOWDebtBefore // perSecondRate = 1 + (yearlyRate / 31_557_600) - // yearly debt growth = perSecondRate^ONE_YEAR - 1 - // FLOW debit yearly growth = (1 + 0.01481481481 / 31_557_600)^31_557_600 - 1 = 0.01492509772 - let expectedFLOWDebtYearlyGrowth = 0.01492509 // TODO(Uliana) + add credit rate growth + // yearly debt growth = perSecondRate^ONE_YEAR - 1 + // FLOW debit yearly growth = (1 + 0.017777778 / 31_557_600)^31_557_600 - 1 = 0.01793674 + let expectedFLOWDebtYearlyGrowth = 0.01793674 Test.assertEqual(expectedFLOWDebtYearlyGrowth, FLOWGrowthRate1Year) // LP credit should also have grown @@ -545,27 +521,25 @@ fun test_long_time_period_accrual() { details: getPositionDetails(pid: lpPid, beFailed: false), vaultType: Type<@FlowToken.Vault>() ) - Test.assert(creditAfter1Year > creditBefore, message: "credit should grow over 1 year") + Test.assert(creditAfter1Year > FLOWCreditBefore, message: "credit should grow over 1 year") - // advance 10 years + // advance 10 more years Test.moveTime(by: 10.0 * ONE_YEAR) Test.commitBlock() let detailsAfter10Years = getPositionDetails(pid: borrowerPid, beFailed: false) let FLOWDebtAfter10Years = getDebitBalanceForType(details: detailsAfter10Years, vaultType: Type<@FlowToken.Vault>()) - let FLOWTotalGrowthRate = (FLOWDebtAfter10Years - debtBefore) / debtBefore + let FLOWTotalGrowthRate = (FLOWDebtAfter10Years - FLOWDebtBefore) / FLOWDebtBefore // perSecondRate = 1 + (yearlyRate / 31_557_600) - // 11-years debt growth = perSecondRate^(31_557_600*11) - 1 - // FLOW debit 11-years growth = (1 + 0.01481481481 / 31_557_600)^(31_557_600*11) - 1 = 0.17699309112 - let expectedFLOWDebt10YearsGrowth = 0.17699309 + // 11-year debt growth = perSecondRate^(31_557_600 * 11) - 1 + // FLOW debit 11-year growth = (1 + 0.017777778 / 31_557_600)^(31_557_600*11) - 1 = 0.21598635 + let expectedFLOWDebt10YearsGrowth = 0.21598635 Test.assertEqual(expectedFLOWDebt10YearsGrowth, FLOWTotalGrowthRate) } // ============================================================================= -/// Verifies that interest accrues correctly after large time jumps. -/// Simulates blockchain halts (1 day and 7 days) and ensures the borrower’s -/// debt increases according to the expected compounded interest rate. +/// Verifies that interest accrues correctly after large time jumps // ============================================================================= access(all) fun test_time_jump_scenarios() { @@ -578,7 +552,7 @@ fun test_time_jump_scenarios() { setupMoetVault(borrower, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: borrower.address, amount: 50_000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: borrower, amount: 50_000.0, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let borrowerPid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid @@ -595,16 +569,17 @@ fun test_time_jump_scenarios() { // record state before the 1-day gap let detailsBefore = getPositionDetails(pid: borrowerPid, beFailed: false) let FLOWDebtBefore = getDebitBalanceForType(details: detailsBefore, vaultType: Type<@FlowToken.Vault>()) - let healthBefore = detailsBefore.health - // Borrower (MOET collateral, FLOW debt): - // KinkCurve - // utilization = debitBalance / (creditBalance + debitBalance) - // FLOW: 5000 / (10000 + 5000) = 0.3(3) < 0.45 (below kink) + // Borrower FLOW rate calculation (KinkInterestCurve) + // utilization = debitBalance / creditBalance + // FLOW: 5000 / 10000 = 0.5 > 0.45 (above kink) // - // debit rate = baseRate + (slope1 * utilization / optimalUtilization) - // FLOW: debitRate = 0 + (0.04 * 0.3(3) / 0.45) = 0.0296296296296296 (2.96% APY) - let expectedFlowDebitRate: UFix128 = 0.02962963 + // excessUtilization = (utilization - optimalUtilization) / (1 - optimalUtilization) + // = (0.5 - 0.45) / (1 - 0.45) = 0.05 / 0.55 = 0.09090909090909... + // + // debitRate = baseRate + slope1 + (slope2 * excessUtilization) + // FLOW: debitRate = 0 + 0.04 + 3.0 * 0.09090909 = 0.3127272727... (31.27% APY) + let expectedFlowDebitRate: UFix128 = 0.31272727 // 1-day blockchain halt Test.moveTime(by: DAY) @@ -618,13 +593,13 @@ fun test_time_jump_scenarios() { let FLOWDebtDailyGrowth = (FLOWDebtAfter1Day - FLOWDebtBefore) / FLOWDebtBefore // perSecondRate = 1 + (yearlyRate / 31_557_600) - // dailyGrowth = perSecondRate^86400 - 1 - // FLOW debit daily growth = (1 + 0.02962963 / 31_557_600)^86400 - 1 = 0.00008112479 - - let expectedFLOWDebtDailyGrowth = 0.00008112 - Test.assertEqual(expectedFLOWDebtDailyGrowth, FLOWDebtDailyGrowth) + // dailyGrowth = perSecondRate^86400 - 1 + // FLOW debit daily growth = (1 + 0.31272727 / 31557600)^86400 - 1 = 0.00085660 + let expectedFLOWDebtDailyGrowth = 0.00085660 + Test.assert(equalWithinVariance(expectedFLOWDebtDailyGrowth, FLOWDebtDailyGrowth), + message: "Expected FLOW debt growth rate to be ~\(expectedFLOWDebtDailyGrowth), but got \(FLOWDebtDailyGrowth)") - // try oto test longer period (7 days) to verify no overflow in calculation + // test longer period (7 days) to verify no overflow in calculation let detailsBefore7Day = getPositionDetails(pid: borrowerPid, beFailed: false) let FlowDebtBefore7Day = getDebitBalanceForType(details: detailsBefore7Day, vaultType: Type<@FlowToken.Vault>()) @@ -638,7 +613,8 @@ fun test_time_jump_scenarios() { let FLOWDebtWeeklyGrowth = (FLOWDebtAfter7Day - FlowDebtBefore7Day) / FlowDebtBefore7Day // weeklyGrowth = perSecondRate^604800 - 1 - // FLOW debit weekly growth = (1 + 0.02962963/31_557_600)^604800 - 1 = 0.00056801 - let expectedFLOWDebtWeeklyGrowth = 0.00056801 - Test.assertEqual(expectedFLOWDebtWeeklyGrowth, FLOWDebtWeeklyGrowth) + // FLOW debit weekly growth = (1 + 0.31272727272 / 31_557_600)^604800 - 1 = 0.00601143 + let expectedFLOWDebtWeeklyGrowth = 0.00601143 + Test.assert(equalWithinVariance(expectedFLOWDebtWeeklyGrowth, FLOWDebtWeeklyGrowth), + message: "Expected FLOW debt growth rate to be ~\(expectedFLOWDebtWeeklyGrowth), but got \(FLOWDebtWeeklyGrowth)") } \ No newline at end of file From e71fbc5fcaba00855b1e88885dcca455b9c4b5f9 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Wed, 11 Mar 2026 17:22:20 +0200 Subject: [PATCH 6/6] Revert FlowActions submodule pointer to correct commit --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index 0cd0fcc9..6769d4c9 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 0cd0fcc94e63fcfc954ea98b5b8b30d3535011da +Subproject commit 6769d4c9f9ded4a5b4404d8c982300e84ccef532