From 90362831cb25ef6365bf2bdb27e0b598cde0c378 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Wed, 18 Feb 2026 20:24:24 +0100 Subject: [PATCH 01/25] Add mainnet fork test for multiple positions per user and batch liquidation - fork_multiple_positions_per_user.cdc covering three scenarios: - Multiple positions with distinct collateral types (FLOW, USDF, USDC, WETH, WBTC) and isolation guarantees between them - Cross-position effects through shared liquidity pools - Batch liquidation of 4 positions (2 full, 2 partial) in a single tx --- cadence/scripts/flow-alp/get_oracle_price.cdc | 9 + ...rsarial_recursive_withdraw_source_test.cdc | 1 + .../tests/adversarial_type_spoofing_test.cdc | 2 +- cadence/tests/async_update_position_test.cdc | 2 +- cadence/tests/deposit_capacity_test.cdc | 12 +- .../fork_multiple_positions_per_user.cdc | 629 ++++++++++++++++++ .../insurance_collection_formula_test.cdc | 4 +- cadence/tests/insurance_collection_test.cdc | 28 +- .../interest_accrual_integration_test.cdc | 24 +- cadence/tests/liquidation_phase1_test.cdc | 207 +++--- cadence/tests/pool_pause_test.cdc | 2 +- .../stability_collection_formula_test.cdc | 4 +- cadence/tests/stability_collection_test.cdc | 22 +- cadence/tests/test_helpers.cdc | 63 +- .../position-manager/borrow_from_position.cdc | 21 +- .../tests/withdraw_stability_funds_test.cdc | 4 +- .../batch_manual_liquidation.cdc | 82 +++ .../fungible-tokens/generic_transfer.cdc | 49 ++ .../fungible-tokens/setup_generic_vault.cdc | 45 ++ .../test/transfer_tokens_with_setup.cdc | 31 + flow.json | 18 +- 21 files changed, 1092 insertions(+), 167 deletions(-) create mode 100644 cadence/scripts/flow-alp/get_oracle_price.cdc create mode 100644 cadence/tests/fork_multiple_positions_per_user.cdc create mode 100644 cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc create mode 100644 cadence/transactions/fungible-tokens/generic_transfer.cdc create mode 100644 cadence/transactions/fungible-tokens/setup_generic_vault.cdc create mode 100644 cadence/transactions/test/transfer_tokens_with_setup.cdc diff --git a/cadence/scripts/flow-alp/get_oracle_price.cdc b/cadence/scripts/flow-alp/get_oracle_price.cdc new file mode 100644 index 00000000..acc74bac --- /dev/null +++ b/cadence/scripts/flow-alp/get_oracle_price.cdc @@ -0,0 +1,9 @@ +import "MockOracle" + +access(all) fun main(tokenIdentifier: String): UFix64? { + let tokenType = CompositeType(tokenIdentifier) + ?? panic("Invalid token identifier: ".concat(tokenIdentifier)) + + let oracle = MockOracle.PriceOracle() + return oracle.price(ofToken: tokenType) +} diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index c4593dbf..e980e615 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -74,6 +74,7 @@ fun testRecursiveWithdrawSource() { let initialDeposit1 = 10000.0 createPosition( + admin: PROTOCOL_ACCOUNT, signer: user1, amount: initialDeposit1, vaultStoragePath: /storage/flowTokenVault, diff --git a/cadence/tests/adversarial_type_spoofing_test.cdc b/cadence/tests/adversarial_type_spoofing_test.cdc index d6f37a14..aa7d497e 100644 --- a/cadence/tests/adversarial_type_spoofing_test.cdc +++ b/cadence/tests/adversarial_type_spoofing_test.cdc @@ -42,7 +42,7 @@ fun setup() { setupMoetVault(hackerAccount, beFailed: false) // provide liquidity to the pool we can extract - createPosition(signer: liquidityAccount, amount: 10000.0, vaultStoragePath: flowVaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: liquidityAccount, amount: 10000.0, vaultStoragePath: flowVaultStoragePath, pushToDrawDownSink: false) } access(all) diff --git a/cadence/tests/async_update_position_test.cdc b/cadence/tests/async_update_position_test.cdc index abed24b2..f6dc85ae 100644 --- a/cadence/tests/async_update_position_test.cdc +++ b/cadence/tests/async_update_position_test.cdc @@ -33,7 +33,7 @@ fun testUpdatePosition() { setupMoetVault(user, beFailed: false) mintFlow(to: user, amount: 1_000.0) - createPosition(signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 100.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // increase price setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: initialPrice * priceIncreaseFactor) diff --git a/cadence/tests/deposit_capacity_test.cdc b/cadence/tests/deposit_capacity_test.cdc index 1d44d57b..6c259ee5 100644 --- a/cadence/tests/deposit_capacity_test.cdc +++ b/cadence/tests/deposit_capacity_test.cdc @@ -59,7 +59,7 @@ fun test_deposit_capacity_consumption() { let capacityBeforePositionCreation = capacityInfo["depositCapacity"]! let initialDepositAmount = 100.0 - createPosition(signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // Get capacity right after position creation - should have decreased by initialDepositAmount capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER) @@ -118,7 +118,7 @@ fun test_per_user_deposit_limits() { mintMoet(signer: PROTOCOL_ACCOUNT, to: user1.address, amount: 10000.0, beFailed: false) let initialDeposit1 = 100.0 - createPosition(signer: user1, amount: initialDeposit1, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user1, amount: initialDeposit1, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // After position creation: usage = 100 (out of 500 limit) // User 1 deposits more (should be accepted up to limit) @@ -140,7 +140,7 @@ fun test_per_user_deposit_limits() { mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 10000.0, beFailed: false) let initialDeposit2 = 100.0 - createPosition(signer: user2, amount: initialDeposit2, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user2, amount: initialDeposit2, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // After position creation: usage = 100 (out of 500 limit) // User 2 should be able to deposit up to their own limit (500 total, so 400 more) @@ -197,7 +197,7 @@ fun test_capacity_regeneration() { let capacityBeforePositionCreation = capacityInfo["depositCapacity"]! let initialDepositAmount = 100.0 - createPosition(signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // Get capacity right after position creation (no regeneration should occur) capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER) @@ -275,7 +275,7 @@ fun test_user_usage_reset_on_regeneration() { mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 10000.0, beFailed: false) let initialDepositAmount = 100.0 - createPosition(signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // After position creation: usage = 100 (out of 500 limit) // User deposits more to reach their limit (500 total, so 400 more) @@ -346,7 +346,7 @@ fun test_multiple_hours_regeneration() { mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 1000.0, beFailed: false) let initialDepositAmount = 100.0 - createPosition(signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: initialDepositAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // The initial deposit consumes capacity, but we're checking the cap regeneration, not capacity // Make a small deposit to trigger regeneration diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user.cdc new file mode 100644 index 00000000..29ad6ac2 --- /dev/null +++ b/cadence/tests/fork_multiple_positions_per_user.cdc @@ -0,0 +1,629 @@ +#test_fork(network: "mainnet", height: 142528994) + +import Test +import BlockchainHelpers + +import "FlowToken" +import "FungibleToken" +import "MOET" +import "FlowALPv1" +import "test_helpers.cdc" + +// Real mainnet token identifiers (overriding test_helpers for mainnet) +access(all) let FLOW_TOKEN_IDENTIFIER_MAINNET = "A.1654653399040a61.FlowToken.Vault" +access(all) let USDC_TOKEN_IDENTIFIER = "A.f1ab99c82dee3526.USDCFlow.Vault" +access(all) let USDF_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault" +access(all) let WETH_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" +access(all) let WBTC_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" +access(all) let MOET_TOKEN_IDENTIFIER_MAINNET = "A.6b00ff876c299c61.MOET.Vault" + +// Storage paths for different token types +access(all) let USDC_VAULT_STORAGE_PATH = /storage/usdcFlowVault +access(all) let USDF_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault +access(all) let WETH_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Vault +access(all) let WBTC_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Vault + +// Protocol account: in fork mode, Test.deployContract() deploys to the contract's mainnet +// alias address. FlowALPv1's mainnet alias is 0x47f544294e3b7656, so PoolFactory and all +// pool admin resources are stored there. Note: this is the same address as wbtcHolder. +access(all) let protocolAccount = Test.getAccount(0x47f544294e3b7656) + +access(all) let usdfHolder = Test.getAccount(0xf18b50870aed46ad) // 25000 +access(all) let wethHolder = Test.getAccount(0xf62e3381a164f993) // 0.07032 +access(all) let wbtcHolder = Test.getAccount(0x47f544294e3b7656) // 0.0005 +access(all) let flowHolder = Test.getAccount(0xe467b9dd11fa00df) // 1921 +access(all) let usdcHolder = Test.getAccount(0xec6119051f7adc31) // 97 + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) fun setup() { + + // Deploy DeFiActionsUtils + var err = Test.deployContract( + name: "DeFiActionsUtils", + path: "../../FlowActions/cadence/contracts/utils/DeFiActionsUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Deploy FlowALPMath + err = Test.deployContract( + name: "FlowALPMath", + path: "../lib/FlowALPMath.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Deploy DeFiActions + err = Test.deployContract( + name: "DeFiActions", + path: "../../FlowActions/cadence/contracts/interfaces/DeFiActions.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + // Deploy MockOracle (references mainnet MOET) + err = Test.deployContract( + name: "MockOracle", + path: "../contracts/mocks/MockOracle.cdc", + arguments: [MOET_TOKEN_IDENTIFIER_MAINNET] + ) + 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()) + + // Deploy FlowALPv1 + err = Test.deployContract( + name: "FlowALPv1", + path: "../contracts/FlowALPv1.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER_MAINNET, beFailed: false) + + // Setup pool with real mainnet token prices + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDF_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WETH_TOKEN_IDENTIFIER, price: 3500.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WBTC_TOKEN_IDENTIFIER, price: 50000.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MOET_TOKEN_IDENTIFIER_MAINNET, price: 1.0) + + // Add multiple token types as supported collateral (FLOW, USDC, USDF, WETH, WBTC) + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + collateralFactor: 0.8, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: USDC_TOKEN_IDENTIFIER, + collateralFactor: 0.85, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: USDF_TOKEN_IDENTIFIER, + collateralFactor: 0.85, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: WETH_TOKEN_IDENTIFIER, + collateralFactor: 0.75, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // Set minimum deposit for WETH to 0.01 (since holder only has 0.07032) + setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: WETH_TOKEN_IDENTIFIER, minimum: 0.01) + + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: WBTC_TOKEN_IDENTIFIER, + collateralFactor: 0.75, + borrowFactor: 1.0, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + // Set minimum deposit for WBTC to 0.0001 (since holder only has 0.0005) + setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: WBTC_TOKEN_IDENTIFIER, minimum: 0.0001) + + snapshot = getCurrentBlockHeight() +} + +/// Transfer tokens from holder to recipient (creates vault for recipient if needed) +access(all) fun transferTokensFromHolder(holder: Test.TestAccount, recipient: Test.TestAccount, amount: UFix64, storagePath: StoragePath, tokenName: String) { + let tx = Test.Transaction( + code: Test.readFile("../transactions/test/transfer_tokens_with_setup.cdc"), + authorizers: [holder.address, recipient.address], + signers: [holder, recipient], + arguments: [amount, storagePath] + ) + let result = Test.executeTransaction(tx) + Test.expect(result, Test.beSucceeded()) +} + +/// Test Multiple Positions Per User +/// +/// Validates requirements: +/// 1. User creates 5+ positions with different collateral types +/// 2. Each position has different health factors +/// 3. Operations on one position should not affect others (isolation) +/// +access(all) fun testMultiplePositionsPerUser() { + safeReset() + + log("Testing Multiple Positions with Real Mainnet Tokens\n") + + let lpUser = Test.createAccount() + let user = Test.createAccount() + + // Transfer FLOW from holder to LP + log("Setting up liquidity provider with FLOW\n") + let liquidityAmount = 800.0 + transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + + // LP deposits FLOW to create liquidity for borrowing + createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + //////////// Position creation /////////////////// + log("Create 5 Positions with Different Collateral Types\n") + + // Define positions with different collateral types + // Token holder balances and prices: + // - flowHolder: 1921 FLOW x $1 = $1921 + // - usdfHolder: 25000 USDF x $1 = $25000 + // - usdcHolder: 97 USDC x $1 = $97 + // - wethHolder: 0.07032 WETH x $3500 = $246.12 + // - wbtcHolder: 0.0005 WBTC x $50000 = $25 + + let positions = [ + {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 500.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": flowHolder}, + {"type": USDF_TOKEN_IDENTIFIER, "amount": 1500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": usdfHolder}, + {"type": USDC_TOKEN_IDENTIFIER, "amount": 10.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": usdcHolder}, + {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.05, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": wethHolder}, + {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": wbtcHolder} + ] + + let debts = [100.0, 150.0, 5.0, 50.0, 8.0] + + var userPids: [UInt64] = [] + + for i, position in positions { + let collateralType = position["type"]! as! String + let collateralName = position["name"]! as! String + let collateralAmount = position["amount"]! as! UFix64 + let storagePath = position["storagePath"]! as! StoragePath + let holder = position["holder"]! as! Test.TestAccount + + // Transfer tokens from holder to user + transferTokensFromHolder(holder: holder, recipient: user, amount: collateralAmount, storagePath: storagePath, tokenName: collateralName) + + createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) + let openEvts = Test.eventsOfType(Type()) + userPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + + // Calculate USD value based on token price from oracle + let price = getOraclePrice(tokenIdentifier: collateralType) + let value = collateralAmount * price + log(" Position \(userPids[i]): \(collateralAmount) \(collateralName) collateral (\(value) value)") + } + + //////////// Borrowing from each position /////////////////// + + log("Borrowing different amounts from each position\n") + + var healths: [UFix128] = [] + for i, debt in debts { + let pid = userPids[i] + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: debt, beFailed: false) + + // Get health factor + let health = getPositionHealth(pid: pid, beFailed: false) + healths.append(health) + + log(" Position \(pid): Borrowed \(debt) - Health = \(health)") + } + + //////////// Test isolation: borrow more from position 2, verify others unchanged /////////////////// + + // userPids[1] is the second user position (USDF collateral) + let isolationTestPid = userPids[1] + let additionalDebt = 100.0 + + log("Testing isolation by borrowing more from Position \(isolationTestPid)\n") + + log("\n Action: Borrow 100 more FLOW from Position \(isolationTestPid)\n") + borrowFromPosition(signer: user, positionId: isolationTestPid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: additionalDebt, beFailed: false) + + // Get health of all positions after + var healthsAfterBorrow: [UFix128] = [] + for m in InclusiveRange(0, 4) { + let h = getPositionHealth(pid: userPids[m], beFailed: false) + healthsAfterBorrow.append(h) + } + + // Verify isolation: only position 2 (index 1) should change + Test.assert(healthsAfterBorrow[0] == healths[0], message: "Position 1 should be unchanged") + Test.assert(healthsAfterBorrow[1] < healths[1], message: "Position 2 should decrease") + Test.assert(healthsAfterBorrow[2] == healths[2], message: "Position 3 should be unchanged") + Test.assert(healthsAfterBorrow[3] == healths[3], message: "Position 4 should be unchanged") + Test.assert(healthsAfterBorrow[4] == healths[4], message: "Position 5 should be unchanged") +} + +/// Test Position Interactions Through Shared Liquidity Pools +/// +/// Validates that multiple positions interact through shared pool resources: +/// 1. Multiple positions compete for limited deposit capacity +/// 2. Position A's borrowing reduces available liquidity for Position B +/// 3. Shared liquidity pools create cross-position effects +/// 4. Pool capacity constraints affect all positions +access(all) fun testPositionInteractionsSharedLiquidity() { + safeReset() + + log("Testing Position Interactions Through Shared Liquidity Pools\n") + + // Create liquidity provider to deposit FLOW (the shared liquidity pool) + let lpUser = Test.createAccount() + let user = Test.createAccount() + + log("Setting up shared liquidity pool with limited capacity\n") + let liquidityAmount = 400.0 + transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + + // LP deposits FLOW - this creates the shared liquidity pool + createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + log(" Liquidity Provider deposited: \(liquidityAmount) FLOW\n") + + //////////// Create Position A with USDC collateral /////////////////// + + let userACollateral = 90.0 // 90 USDC + log("Creating Position A with \(userACollateral) USDC collateral\n") + transferTokensFromHolder(holder: usdcHolder, recipient: user, amount: userACollateral, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") + createPosition(admin: protocolAccount, signer: user, amount: userACollateral, vaultStoragePath: USDC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + var openEvts = Test.eventsOfType(Type()) + let positionA_id = (openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid + + //////////// Create Position B with USDF collateral /////////////////// + + let userBCollateral = 500.0 // 500 USDF + log("Creating Position B with \(userBCollateral) USDF collateral\n") + transferTokensFromHolder(holder: usdfHolder, recipient: user, amount: userBCollateral, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") + createPosition(admin: protocolAccount, signer: user, amount: userBCollateral, vaultStoragePath: USDF_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + openEvts = Test.eventsOfType(Type()) + let positionB_id = (openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid + + //////////// 1. Position A borrows heavily, affecting available liquidity /////////////////// + + log("Position A borrows heavily from shared pool\n") + // Formula: Effective Collateral = (debitAmount * price) * collateralFactor = (90 × 1.0) × 0.85 = 76.50 + // Max Borrow = 76.50 / 1.1 (minHealth) = 69.55 FLOW + // Health after borrow = 76.50 / 60 = 1.275 + let positionA_borrow1 = 60.0 // Borrow 60 FLOW (within max 69.55) + borrowFromPosition(signer: user, positionId: positionA_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionA_borrow1, beFailed: false) + + let healthA_after1 = getPositionHealth(pid: positionA_id, beFailed: false) + log(" Position A borrowed \(positionA_borrow1) FLOW - Health: \(healthA_after1)\n") + + // Check remaining liquidity in pool + let remainingLiquidity1 = 340.0 // liquidityAmount - positionA_borrow1 = 400.0 - 60.0 = 340.0 + log(" Remaining liquidity in pool: \(remainingLiquidity1) FLOW\n") + + //////////// 2. Position B borrows successfully from shared pool /////////////////// + log("Position B borrows from shared pool\n") + + // Formula: Effective Collateral = (debitAmount * price) * collateralFactor = (500 × 1.0) × 0.85 = 425.00 + // Max Borrow = 425.00 / 1.1 (minHealth) = 386.36 FLOW + let positionB_borrow1 = 340.0 // Borrow 340 FLOW (within max 386.36 borrow and 340 remaining liquidity) + log(" Attempting to borrow \(positionB_borrow1) FLOW...") + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow1, beFailed: false) + log(" Success - Position B borrowed \(positionB_borrow1) FLOW") + let healthB_after1 = getPositionHealth(pid: positionB_id, beFailed: false) + log(" Position B Health: \(healthB_after1)\n") + + let remainingLiquidity2 = 0.0 + log(" Remaining liquidity in pool: \(remainingLiquidity2) FLOW\n") + + //////////// 3. Position B tries to exceed max borrowing capacity - expects failure /////////////////// + log("Position B tries to borrow beyond its capacity - EXPECTS FAILURE\n") + + // Position B can't borrow more because remaining liquidity is 0 + let positionB_borrow2_attempt = 1.0 + log(" Attempting to borrow \(positionB_borrow2_attempt) FLOW...") + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow2_attempt, beFailed: true) + log(" Failed as expected - remaining liquidity is 0\n") + + let healthB_after2 = getPositionHealth(pid: positionB_id, beFailed: false) + + //////////// 4. Position A repayment increases available liquidity /////////////////// + log("Position A repays debt, freeing liquidity back to pool\n") + + // Position A repays substantial debt by depositing borrowed FLOW back + let repayAmount = 40.0 + + // Deposit FLOW back to position (repays debt using previously borrowed funds) + depositToPosition(signer: user, positionID: positionA_id, amount: repayAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let healthA_after2 = getPositionHealth(pid: positionA_id, beFailed: false) + log(" Position A repaid \(repayAmount) FLOW - Health: \(healthA_after2)\n") + + let remainingLiquidity4 = repayAmount // 40.0, because remainingLiquidity2 == 0 + log(" Remaining liquidity in pool after repayment: \(remainingLiquidity4) FLOW\n") + + //////////// Verify cross-position effects /////////////////// + + Test.assert(healthA_after2 > healthA_after1, message: "Position A health should improve after repayment") + Test.assert(healthB_after2 == healthB_after1, message: "Position B health should be unchanged - second borrow attempt failed") + + + //////////// 5. Test Position A health change affects Position B's borrowing capacity /////////////////// + log("Testing how Position A's health deterioration affects Position B\n") + + let healthB_before_priceChange = getPositionHealth(pid: positionB_id, beFailed: false) + log(" Position B health: \(healthB_before_priceChange)") + + // Crash USDC price (Position A's collateral) from $1.0 to $0.5 + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 0.5) + + let healthA_after_crash = getPositionHealth(pid: positionA_id, beFailed: false) + log(" Position A health after price crash: \(healthA_after_crash)\n") + + // Position A's effective collateral is now: (90 * 0.5) * 0.85 = 38.25 + // Position A's debt is: 60 - 40 = 20 FLOW + // Position A's health is: 38.25 / 20 = 1.9125 + Test.assert(healthA_after_crash < healthA_after2, message: "Position A health should decrease after collateral price crash") + + // Position B's health should be UNCHANGED (different collateral type) + let healthB_after_priceChange = getPositionHealth(pid: positionB_id, beFailed: false) + log(" Position B health after Position A's price crash: \(healthB_after_priceChange)\n") + Test.assert(healthB_after_priceChange == healthB_before_priceChange, message: "Position B health unaffected by Position A's collateral price change") + + // Position B can still borrow from the shared pool (liquidity is independent of Position A's health) + // Position B has: 425 effective collateral, 340 borrowed, can borrow up to 46.36 more + let positionB_borrow3 = 30.0 // Well within remaining capacity (40 FLOW available, 46.36 max allowed) + log(" Position B attempts to borrow \(positionB_borrow3) FLOW after Position A's health deterioration...") + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow3, beFailed: false) + log(" Success - Position B can still borrow despite Position A's poor health\n") + + let healthB_final = getPositionHealth(pid: positionB_id, beFailed: false) + log(" Position B final health: \(healthB_final)\n") + Test.assert(healthB_final < healthB_after_priceChange, message: "Position B health decreases from its own borrowing, not Position A's health") + +} + +/// Test Batch Liquidations +/// +/// Validates batch liquidation capabilities: +/// 1. Multiple unhealthy positions liquidated in SINGLE transaction +/// 2. Partial liquidation of multiple positions +/// 3. Gas cost optimization through batch processing +access(all) fun testBatchLiquidations() { + safeReset() + + log("Testing Batch Liquidations of Multiple Positions\n") + + let lpUser = Test.createAccount() + let user = Test.createAccount() + + // LP deposits 600 FLOW to provide borrowing liquidity + // (total borrows = 200+90+40+10+80 = 420 FLOW < 600) + transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: 600.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + createPosition(admin: protocolAccount, signer: lpUser, amount: 600.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + // 5 positions with distinct collateral types: + // + // pid | Collateral | Amount | Borrow | Crash price | Health after | Action + // ----|-----------|-------------|----------|-------------|--------------|-------- + // 1 | USDF | 500 USDF | 200 FLOW | $0.30 (-70%)| 0.638 | FULL liquidation + // 2 | WETH | 0.06 WETH | 90 FLOW | $1050 (-70%)| 0.525 | FULL liquidation + // 3 | USDC | 80 USDC | 40 FLOW | $0.50 (-50%)| 0.850 | PARTIAL liquidation + // 4 | WBTC | 0.0004 WBTC | 10 FLOW | $25000(-50%)| 0.750 | PARTIAL liquidation + // 5 | FLOW | 200 FLOW | 80 FLOW | $1.00 (0%) | 2.000 | NOT liquidated + // + // FLOW position (pid=5): health = 0.8 * collateral / debt is price-independent + // when both collateral and debt are FLOW, so any FLOW price crash leaves it unaffected. + log("Creating 5 positions with different collateral types\n") + + let positions = [ + {"type": USDF_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": usdfHolder, "borrow": 200.0}, + {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.06, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": wethHolder, "borrow": 90.0}, + {"type": USDC_TOKEN_IDENTIFIER, "amount": 80.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": usdcHolder, "borrow": 40.0}, + {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": wbtcHolder, "borrow": 10.0}, + {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 200.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": flowHolder, "borrow": 80.0} + ] + + var userPids: [UInt64] = [] + + for i, position in positions { + let collateralName = position["name"]! as! String + let collateralAmount = position["amount"]! as! UFix64 + let storagePath = position["storagePath"]! as! StoragePath + let holder = position["holder"]! as! Test.TestAccount + + transferTokensFromHolder(holder: holder, recipient: user, amount: collateralAmount, storagePath: storagePath, tokenName: collateralName) + createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) + let openEvts = Test.eventsOfType(Type()) + userPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + } + + log("Borrowing FLOW from each position\n") + var healths: [UFix128] = [] + for i, position in positions { + let pid = userPids[i] + let borrowAmount = position["borrow"]! as! UFix64 + let collateralName = position["name"]! as! String + + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: borrowAmount, beFailed: false) + + let health = getPositionHealth(pid: pid, beFailed: false) + healths.append(health) + log(" Position \(pid) (\(collateralName)): Borrowed \(borrowAmount) FLOW - Health: \(health)") + } + + // Crash collateral prices. FLOW stays at $1.0 so userPids[4] stays healthy. + log("\nCrashing collateral prices to trigger liquidations\n") + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDF_TOKEN_IDENTIFIER, price: 0.3) // -70% + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WETH_TOKEN_IDENTIFIER, price: 1050.0) // -70% + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 0.5) // -50% + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WBTC_TOKEN_IDENTIFIER, price: 25000.0) // -50% + + log("\nPosition health after price crash:\n") + for i in InclusiveRange(0, 4) { + let pid = userPids[i] + let health = getPositionHealth(pid: pid, beFailed: false) + let collateralName = positions[i]["name"]! as! String + healths[i] = health + log(" Position \(pid) (\(collateralName)): Health = \(health)") + } + + // Verify expected health states + Test.assert(healths[0] < 1.0, message: "USDF position should be unhealthy") + Test.assert(healths[1] < 1.0, message: "WETH position should be unhealthy") + Test.assert(healths[2] < 1.0, message: "USDC position should be unhealthy") + Test.assert(healths[3] < 1.0, message: "WBTC position should be unhealthy") + Test.assert(healths[4] > 1.0, message: "FLOW position should remain healthy") + + // Verify worst-health ordering: WETH < USDF < WBTC < USDC + Test.assert(healths[1] < healths[0], message: "WETH should be worse than USDF") + Test.assert(healths[0] < healths[3], message: "USDF should be worse than WBTC") + Test.assert(healths[3] < healths[2], message: "WBTC should be worse than USDC") + + // Setup protocol account FLOW vault as the DEX output source. + // priceRatio = Pc_crashed / Pd = post-crash collateral price / FLOW price. + // This must match the oracle prices exactly to pass the DEX/oracle deviation check. + transferTokensFromHolder(holder: flowHolder, recipient: protocolAccount, amount: 300.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + + log("\nSetting up DEX swappers (priceRatio = post-crash Pc / Pd)\n") + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: USDF_TOKEN_IDENTIFIER, + outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, + priceRatio: 0.3 // $0.30 USDF / $1.00 FLOW + ) + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: WETH_TOKEN_IDENTIFIER, + outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, + priceRatio: 1050.0 // $1050 WETH / $1.00 FLOW + ) + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: USDC_TOKEN_IDENTIFIER, + outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, + priceRatio: 0.5 // $0.50 USDC / $1.00 FLOW + ) + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: WBTC_TOKEN_IDENTIFIER, + outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, + priceRatio: 25000.0 // $25000 WBTC / $1.00 FLOW + ) + + // Liquidator setup: transfer FLOW for debt repayment (total needed: 71+113+4+12 = 200 FLOW) + // and 1 unit of each collateral token to initialize vault storage paths. + log("\nSetting up liquidator account\n") + let liquidator = Test.createAccount() + transferTokensFromHolder(holder: flowHolder, recipient: liquidator, amount: 250.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: usdfHolder, recipient: liquidator, amount: 1.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") + transferTokensFromHolder(holder: wethHolder, recipient: liquidator, amount: 0.001, storagePath: WETH_VAULT_STORAGE_PATH, tokenName: "WETH") + transferTokensFromHolder(holder: usdcHolder, recipient: liquidator, amount: 1.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") + transferTokensFromHolder(holder: wbtcHolder, recipient: liquidator, amount: 0.00001, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") + + // Batch liquidation parameters — ordered worst health first: + // WETH (0.525) → USDF (0.638) → WBTC (0.750) → USDC (0.850) + // + // seize/repay values satisfy three constraints: + // 1. seize < quote.inAmount (offer beats DEX price) + // 2. postHealth <= 1.05 (liquidationTargetHF default) + // 3. postHealth > pre-liq health (position improves) + // + // Full liquidations — bring health up to ~1.03-1.04 (as close to 1.05 target as possible): + // pid=WETH: repay 71 FLOW, seize 0.035 WETH + // postHealth = (47.25 - 0.035*787.5) / (90 - 71) = 19.6875/19 ≈ 1.036 + // DEX check: 0.035 < 71/1050 = 0.0676 + // pid=USDF: repay 113 FLOW, seize 147 USDF + // postHealth = (127.5 - 147*0.255) / (200 - 113) = 90.015/87 ≈ 1.034 + // DEX check: 147 < 113/0.3 = 376.7 + // + // Partial liquidations — improve health without reaching 1.05: + // pid=WBTC: repay 4 FLOW, seize 0.00011 WBTC + // postHealth = (7.5 - 0.00011*18750) / (10 - 4) = 5.4375/6 ≈ 0.906 + // DEX check: 0.00011 < 4/25000 = 0.00016 + // pid=USDC: repay 12 FLOW, seize 17 USDC + // postHealth = (34 - 17*0.425) / (40 - 12) = 26.775/28 ≈ 0.956 + // DEX check: 17 < 12/0.5 = 24 + + log("\nExecuting batch liquidation of 4 positions (2 full, 2 partial) in SINGLE transaction...\n") + // Ordered worst health first: WETH (idx=1), USDF (idx=0), WBTC (idx=3), USDC (idx=2) + let batchPids = [userPids[1], userPids[0], userPids[3], userPids[2] ] + let batchSeizeTypes = [WETH_TOKEN_IDENTIFIER, USDF_TOKEN_IDENTIFIER, WBTC_TOKEN_IDENTIFIER, USDC_TOKEN_IDENTIFIER ] + let batchSeizeAmounts = [0.035, 147.0, 0.00011, 17.0 ] + let batchRepayAmounts = [71.0, 113.0, 4.0, 12.0 ] + + let batchLiqRes = _executeTransaction( + "../transactions/flow-alp/pool-management/batch_manual_liquidation.cdc", + [batchPids, FLOW_TOKEN_IDENTIFIER_MAINNET, batchSeizeTypes, batchSeizeAmounts, batchRepayAmounts], + liquidator + ) + Test.expect(batchLiqRes, Test.beSucceeded()) + + log("\nVerifying results after batch liquidation:\n") + + // Full liquidations (WETH, USDF): health must cross above 1.0 (healthy again) + let healthAfterWeth = getPositionHealth(pid: userPids[1], beFailed: false) + let healthAfterUsdf = getPositionHealth(pid: userPids[0], beFailed: false) + log(" WETH (FULL): \(healths[1]) -> \(healthAfterWeth)") + log(" USDF (FULL): \(healths[0]) -> \(healthAfterUsdf)") + Test.assert(healthAfterWeth > 1.0, message: "WETH position should be healthy after full liquidation") + Test.assert(healthAfterUsdf > 1.0, message: "USDF position should be healthy after full liquidation") + + // Partial liquidations (WBTC, USDC): health must improve but stays below 1.0 + let healthAfterWbtc = getPositionHealth(pid: userPids[3], beFailed: false) + let healthAfterUsdc = getPositionHealth(pid: userPids[2], beFailed: false) + log(" WBTC (PARTIAL): \(healths[3]) -> \(healthAfterWbtc)") + log(" USDC (PARTIAL): \(healths[2]) -> \(healthAfterUsdc)") + Test.assert(healthAfterWbtc > healths[3], message: "WBTC position health should improve after partial liquidation") + Test.assert(healthAfterUsdc > healths[2], message: "USDC position health should improve after partial liquidation") + + // FLOW position (userPids[4]): completely unaffected — health is price-independent for FLOW/FLOW + let healthAfterFlow = getPositionHealth(pid: userPids[4], beFailed: false) + log(" FLOW (NONE): \(healths[4]) -> \(healthAfterFlow)") + Test.assert(healthAfterFlow == healths[4], message: "FLOW position health should be unchanged") +} diff --git a/cadence/tests/insurance_collection_formula_test.cdc b/cadence/tests/insurance_collection_formula_test.cdc index c392f8da..27c7cc39 100644 --- a/cadence/tests/insurance_collection_formula_test.cdc +++ b/cadence/tests/insurance_collection_formula_test.cdc @@ -41,7 +41,7 @@ fun test_collectInsurance_success_fullAmount() { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) // LP deposits MOET (creates credit balance, provides borrowing liquidity) - createPosition(signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral // With 0.8 CF and 1.3 target health: 1000 FLOW collateral allows borrowing ~615 MOET @@ -51,7 +51,7 @@ fun test_collectInsurance_success_fullAmount() { transferFlowTokens(to: borrower, amount: 1000.0) // borrower deposits FLOW and auto-borrows MOET (creates debit balance ~615 MOET) - createPosition(signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) diff --git a/cadence/tests/insurance_collection_test.cdc b/cadence/tests/insurance_collection_test.cdc index 2cb214a4..87aa8d96 100644 --- a/cadence/tests/insurance_collection_test.cdc +++ b/cadence/tests/insurance_collection_test.cdc @@ -47,7 +47,7 @@ fun test_collectInsurance_noInsuranceRate_returnsNil() { mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: 1000.0, beFailed: false) // create position - createPosition(signer: user, amount: 500.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 500.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // verify no swapper Test.assertEqual(false, insuranceSwapperExists(tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER)) @@ -111,7 +111,7 @@ fun test_collectInsurance_partialReserves_collectsAvailable() { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 1000.0, beFailed: false) // LP deposits 1000 MOET (creates credit balance, provides borrowing liquidity) - createPosition(signer: lp, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with large FLOW collateral to borrow most of the MOET let borrower = Test.createAccount() @@ -122,7 +122,7 @@ fun test_collectInsurance_partialReserves_collectsAvailable() { // With 0.8 CF and 1.3 target health: 10000 FLOW allows borrowing ~6153 MOET // But pool only has 1000 MOET, so borrower gets ~1000 MOET (limited by liquidity) // This leaves reserves very low (close to 0) - createPosition(signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) @@ -175,7 +175,7 @@ fun test_collectInsurance_tinyAmount_roundsToZero_returnsNil() { setMinimumTokenBalancePerPosition(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, minimum: tinyDeposit) // create position with tiny deposit - createPosition(signer: user, amount: tinyDeposit, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: tinyDeposit, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) @@ -217,7 +217,7 @@ fun test_collectInsurance_success_fullAmount() { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) // LP deposits MOET (creates credit balance, provides borrowing liquidity) - createPosition(signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral let borrower = Test.createAccount() @@ -225,7 +225,7 @@ fun test_collectInsurance_success_fullAmount() { transferFlowTokens(to: borrower, amount: 1000.0) // borrower deposits FLOW and auto-borrows MOET (creates debit balance) - createPosition(signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) @@ -285,7 +285,7 @@ fun test_collectInsurance_multipleTokens() { mintMoet(signer: PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) // MOET LP deposits MOET (creates MOET credit balance) - createPosition(signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup FLOW LP to provide FLOW liquidity for borrowing let flowLp = Test.createAccount() @@ -293,7 +293,7 @@ fun test_collectInsurance_multipleTokens() { transferFlowTokens(to: flowLp, amount: 10000.0) // FLOW LP deposits FLOW (creates FLOW debit balance) - createPosition(signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // setup MOET borrower with FLOW collateral (creates MOET debit) let moetBorrower = Test.createAccount() @@ -301,7 +301,7 @@ fun test_collectInsurance_multipleTokens() { transferFlowTokens(to: moetBorrower, amount: 1000.0) // MOET borrower deposits FLOW and auto-borrows MOET (creates MOET debit balance) - createPosition(signer: moetBorrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: moetBorrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // setup FLOW borrower with MOET collateral (creates FLOW debit) let flowBorrower = Test.createAccount() @@ -309,9 +309,9 @@ fun test_collectInsurance_multipleTokens() { mintMoet(signer: PROTOCOL_ACCOUNT, to: flowBorrower.address, amount: 1000.0, beFailed: false) // FLOW borrower deposits MOET as collateral - createPosition(signer: flowBorrower, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: flowBorrower, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // Then borrow FLOW (creates FLOW debit balance) - borrowFromPosition(signer: flowBorrower, positionId: 3, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, amount: 500.0, beFailed: false) + borrowFromPosition(signer: flowBorrower, positionId: 3, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 500.0, beFailed: false) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) @@ -404,7 +404,7 @@ fun test_collectInsurance_dexOracleSlippageProtection() { transferFlowTokens(to: flowLp, amount: 10000.0) // FLOW LP deposits FLOW - createPosition(signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // setup borrower that borrows FLOW (creates FLOW debit balance for insurance calculation) let borrower = Test.createAccount() @@ -412,8 +412,8 @@ fun test_collectInsurance_dexOracleSlippageProtection() { mintMoet(signer: PROTOCOL_ACCOUNT, to: borrower.address, amount: 5000.0, beFailed: false) // borrower deposits MOET as collateral and borrows FLOW - createPosition(signer: borrower, amount: 5000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - borrowFromPosition(signer: borrower, positionId: 1, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, amount: 2000.0, beFailed: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 5000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + borrowFromPosition(signer: borrower, positionId: 1, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 2000.0, beFailed: false) // setup protocol account with MOET vault for the swapper setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index 53162ffe..4df93a07 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -623,12 +623,7 @@ fun test_flow_debit_accrues_interest() { // Step 5b: Explicitly borrow Flow from the position // Borrowing 4,000 FLOW from 10,000 FLOW pool = 40% utilization let borrowPid: UInt64 = 1 - let borrowRes = executeTransaction( - "./transactions/position-manager/borrow_from_position.cdc", - [borrowPid, FLOW_TOKEN_IDENTIFIER, 4_000.0], - borrower - ) - Test.expect(borrowRes, Test.beSucceeded()) + borrowFromPosition(signer: borrower, positionId: borrowPid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 4_000.0, beFailed: false) log("Borrower deposited 10,000 MOET and borrowed 4,000 Flow") // ------------------------------------------------------------------------- @@ -813,12 +808,8 @@ fun test_flow_credit_accrues_interest_with_insurance() { // Borrow 4,000 Flow (40% utilization) let borrowPid: UInt64 = 1 - let borrowRes = executeTransaction( - "./transactions/position-manager/borrow_from_position.cdc", - [borrowPid, FLOW_TOKEN_IDENTIFIER, 4_000.0], - borrower - ) - Test.expect(borrowRes, Test.beSucceeded()) + borrowFromPosition(signer: borrower, positionId: borrowPid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 4_000.0, beFailed: false) + log("Borrower deposited 10,000 MOET and borrowed 4,000 Flow") // ------------------------------------------------------------------------- @@ -1242,12 +1233,9 @@ fun test_combined_all_interest_scenarios() { // Explicitly borrow 2,000 Flow // Flow utilization = 2,000 / (5,000 LP2 + 2,000 Borrower1) = 2,000 / 7,000 ≈ 28.6% - let b2BorrowRes = executeTransaction( - "./transactions/position-manager/borrow_from_position.cdc", - [3 as UInt64, FLOW_TOKEN_IDENTIFIER, 2_000.0], - borrower2 - ) - Test.expect(b2BorrowRes, Test.beSucceeded()) + let borrowPid: UInt64 = 3 + borrowFromPosition(signer: borrower2, positionId: borrowPid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 2_000.0, beFailed: false) + log("Borrower2: Deposited 3,000 MOET, borrowed 2,000 Flow") // ------------------------------------------------------------------------- diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index 53e8cc05..6a2fbf90 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -59,7 +59,7 @@ fun testManualLiquidation_healthyPosition() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // Log initial health let health = getPositionHealth(pid: pid, beFailed: false) @@ -75,10 +75,13 @@ fun testManualLiquidation_healthyPosition() { // Repay MOET to seize FLOW let repayAmount = 2.0 let seizeAmount = 1.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) Test.expect(liqRes, Test.beFailed()) Test.assertError(liqRes, errorMessage: "Cannot liquidate healthy position") @@ -97,7 +100,7 @@ fun testManualLiquidation_liquidationExceedsTargetHealth() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -126,10 +129,13 @@ fun testManualLiquidation_liquidationExceedsTargetHealth() { // TODO(jord): add helper to compute health boundaries given best acceptable price, then test boundaries let repayAmount = 500.0 let seizeAmount = 500.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because we are repaying/seizing too much Test.expect(liqRes, Test.beFailed()) @@ -154,7 +160,7 @@ fun testManualLiquidation_repayExceedsDebt() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -186,10 +192,13 @@ fun testManualLiquidation_repayExceedsDebt() { // Repay MOET to seize FLOW. Choose repay amount above debt balance let repayAmount = debtBalance + 0.001 let seizeAmount = (repayAmount / newPrice) * 0.99 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because we are repaying too much Test.expect(liqRes, Test.beFailed()) @@ -214,7 +223,7 @@ fun testManualLiquidation_seizeExceedsCollateral() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -244,10 +253,13 @@ fun testManualLiquidation_seizeExceedsCollateral() { // Repay MOET to seize FLOW. Choose seize amount above collateral balance let seizeAmount = collateralBalance + 0.001 let repayAmount = seizeAmount * newPrice * 1.01 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because we are seizing too much collateral Test.expect(liqRes, Test.beFailed()) @@ -272,7 +284,7 @@ fun testManualLiquidation_reduceHealth() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -303,10 +315,13 @@ fun testManualLiquidation_reduceHealth() { // Repay MOET to seize FLOW. Choose seize amount above collateral balance let seizeAmount = collateralBalancePreLiq - 0.01 let repayAmount = seizeAmount * newPrice * 1.01 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should succeed, even though we are reducing health Test.expect(liqRes, Test.beSucceeded()) @@ -338,7 +353,7 @@ fun testManualLiquidation_increaseHealthBelowTarget() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause severe undercollateralization let newPrice = 0.5 // $/FLOW @@ -366,10 +381,13 @@ fun testManualLiquidation_increaseHealthBelowTarget() { // Liquidator offers 150 FLOW < 200 FLOW (better price) let repayAmount = 100.0 let seizeAmount = 150.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should succeed Test.expect(liqRes, Test.beSucceeded()) @@ -397,7 +415,7 @@ fun testManualLiquidation_liquidateToTarget() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization let newPrice = 0.7 // $/FLOW @@ -431,10 +449,13 @@ fun testManualLiquidation_liquidateToTarget() { // Liquidator offers 33.66 FLOW < 142.86 FLOW (better price) let repayAmount = 100.0 let seizeAmount = 33.66 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should succeed Test.expect(liqRes, Test.beSucceeded()) @@ -459,7 +480,7 @@ fun testManualLiquidation_repaymentVaultCollateralType() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -512,7 +533,7 @@ fun testManualLiquidation_repaymentVaultTypeMismatch() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -567,7 +588,7 @@ fun testManualLiquidation_unsupportedDebtType() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -622,7 +643,7 @@ fun testManualLiquidation_unsupportedCollateralType() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -654,10 +675,13 @@ fun testManualLiquidation_unsupportedCollateralType() { // Repay MOET to seize FLOW. Choose seize amount above collateral balance let seizeAmount = collateralBalancePreLiq - 0.01 let repayAmount = seizeAmount * newPrice * 1.01 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, MOCK_YIELD_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because we are specifying an unsupported collateral type (yield token) Test.expect(liqRes, Test.beFailed()) @@ -699,7 +723,7 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { // user1 opens wrapped position with FLOW collateral // debt is MOET, collateral is FLOW let pid1: UInt64 = 0 - createPosition(signer: user1, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user1, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // user2 setup - deposits MockYieldToken let user2 = Test.createAccount() @@ -709,7 +733,7 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { // user2 opens wrapped position with MockYieldToken collateral let pid2: UInt64 = 1 - createPosition(signer: user2, amount: 1000.0, vaultStoragePath: MockYieldToken.VaultStoragePath, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user2, amount: 1000.0, vaultStoragePath: MockYieldToken.VaultStoragePath, pushToDrawDownSink: true) // health before price drop for user1 let hBefore = getPositionHealth(pid: pid1, beFailed: false) @@ -738,10 +762,13 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { // user1 has no MockYieldToken debt balance let seizeAmount = 0.01 let repayAmount = 100.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid1, MOCK_YIELD_TOKEN_IDENTIFIER, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid1, + debtVaultIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because user1's position doesn't have MockYieldToken collateral Test.expect(liqRes, Test.beFailed()) @@ -782,7 +809,7 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { // user1 opens wrapped position with FLOW collateral, MOET debt let pid1: UInt64 = 0 - createPosition(signer: user1, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user1, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // user2 setup - deposits MockYieldToken, borrows MOET let user2 = Test.createAccount() @@ -792,7 +819,7 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { // user2 opens wrapped position with MockYieldToken collateral let pid2: UInt64 = 1 - createPosition(signer: user2, amount: 1000.0, vaultStoragePath: MockYieldToken.VaultStoragePath, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user2, amount: 1000.0, vaultStoragePath: MockYieldToken.VaultStoragePath, pushToDrawDownSink: true) // health before price drop for user1 let hBefore = getPositionHealth(pid: pid1, beFailed: false) @@ -822,10 +849,13 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { // User1 only has MOET debt, not MockYieldToken debt let seizeAmount = 0.01 let repayAmount = 100.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid1, Type<@MOET.Vault>().identifier, MOCK_YIELD_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid1, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because user1's position doesn't have MockYieldToken debt Test.expect(liqRes, Test.beFailed()) @@ -853,7 +883,7 @@ fun testManualLiquidation_dexOraclePriceDivergence_withinThreshold() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization let oraclePrice = 0.7 // $/FLOW @@ -881,10 +911,13 @@ fun testManualLiquidation_dexOraclePriceDivergence_withinThreshold() { // Liquidator offers 72 FLOW < 73.53 FLOW (better price) let repayAmount = 50.0 let seizeAmount = 72.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should succeed because divergence is within threshold Test.expect(liqRes, Test.beSucceeded()) @@ -899,7 +932,7 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexBelowOracle() { let user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.7) @@ -916,11 +949,13 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexBelowOracle() { let liquidator = Test.createAccount() setupMoetVault(liquidator, beFailed: false) mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) - - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, 70.0, 50.0], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: 70.0, + repayAmount: 50.0, ) // Should fail because divergence exceeds threshold Test.expect(liqRes, Test.beFailed()) @@ -936,7 +971,7 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexAboveOracle() { let user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.7) @@ -953,11 +988,13 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexAboveOracle() { let liquidator = Test.createAccount() setupMoetVault(liquidator, beFailed: false) mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) - - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, 66.0, 50.0], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: 66.0, + repayAmount: 50.0, ) // Should fail because divergence exceeds threshold Test.expect(liqRes, Test.beFailed()) @@ -977,7 +1014,7 @@ fun testManualLiquidation_liquidatorOfferWorseThanDex() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization let newPrice = 0.7 // $/FLOW @@ -1005,10 +1042,13 @@ fun testManualLiquidation_liquidatorOfferWorseThanDex() { // Liquidator offers 75 FLOW > 71.43 FLOW (worse price) let repayAmount = 50.0 let seizeAmount = 75.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because liquidator offer is worse than DEX Test.expect(liqRes, Test.beFailed()) @@ -1028,7 +1068,7 @@ fun testManualLiquidation_combinedEdgeCase() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW - createPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization let oraclePrice = 0.7 // $/FLOW @@ -1058,10 +1098,13 @@ fun testManualLiquidation_combinedEdgeCase() { // But divergence is 9.375% which exceeds 3% threshold let repayAmount = 50.0 let seizeAmount = 75.0 - let liqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], - liquidator + let liqRes = manualLiquidation( + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should fail because DEX/oracle divergence is too high, even though liquidator offer is competitive Test.expect(liqRes, Test.beFailed()) diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index a5be56cb..db174974 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -51,7 +51,7 @@ fun test_pool_pause_deposit_withdrawal() { mintMoet(signer: PROTOCOL_ACCOUNT, to: user2.address, amount: 1000.0, beFailed: false) // create a position for user1 - createPosition(signer: user1, amount: initialDepositAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: user1, amount: initialDepositAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // Pause the pool let pauseRes = setPoolPauseState(signer: PROTOCOL_ACCOUNT, pause: true) diff --git a/cadence/tests/stability_collection_formula_test.cdc b/cadence/tests/stability_collection_formula_test.cdc index ae518e79..00e8a0d6 100644 --- a/cadence/tests/stability_collection_formula_test.cdc +++ b/cadence/tests/stability_collection_formula_test.cdc @@ -41,7 +41,7 @@ fun test_collectStability_success_fullAmount() { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) // LP deposits MOET (creates credit balance, provides borrowing liquidity) - createPosition(signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral // With 0.8 CF and 1.3 target health: 1000 FLOW collateral allows borrowing ~615 MOET @@ -51,7 +51,7 @@ fun test_collectStability_success_fullAmount() { transferFlowTokens(to: borrower, amount: 1000.0) // borrower deposits FLOW and auto-borrows MOET (creates debit balance ~615 MOET) - createPosition(signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // set 10% annual debit rate // stability is calculated on interest income, not debit balance directly diff --git a/cadence/tests/stability_collection_test.cdc b/cadence/tests/stability_collection_test.cdc index 87ac8807..7d0eb36e 100644 --- a/cadence/tests/stability_collection_test.cdc +++ b/cadence/tests/stability_collection_test.cdc @@ -79,7 +79,7 @@ fun test_collectStability_partialReserves_collectsAvailable() { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 1000.0, beFailed: false) // LP deposits 1000 MOET (creates credit balance, provides borrowing liquidity) - createPosition(signer: lp, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with large FLOW collateral to borrow most of the MOET let borrower = Test.createAccount() @@ -90,7 +90,7 @@ fun test_collectStability_partialReserves_collectsAvailable() { // With 0.8 CF and 1.3 target health: 10000 FLOW allows borrowing ~6153 MOET // But pool only has 1000 MOET, so borrower gets ~1000 MOET (limited by liquidity) // This leaves reserves very low (close to 0) - createPosition(signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 10000.0, beFailed: false) @@ -136,7 +136,7 @@ fun test_collectStability_tinyAmount_roundsToZero_returnsNil() { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 100.0, beFailed: false) // LP deposits small amount - createPosition(signer: lp, amount: 100.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 100.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with tiny borrow let borrower = Test.createAccount() @@ -144,7 +144,7 @@ fun test_collectStability_tinyAmount_roundsToZero_returnsNil() { transferFlowTokens(to: borrower, amount: 1.0) // borrower deposits small FLOW and borrows tiny amount of MOET - createPosition(signer: borrower, amount: 1.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // set a very low stability fee rate let rateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, stabilityFeeRate: 0.0001) // 0.01% @@ -180,7 +180,7 @@ fun test_collectStability_multipleTokens() { mintMoet(signer: PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) // MOET LP deposits MOET (creates MOET credit balance) - createPosition(signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup FLOW LP to provide FLOW liquidity for borrowing let flowLp = Test.createAccount() @@ -188,7 +188,7 @@ fun test_collectStability_multipleTokens() { transferFlowTokens(to: flowLp, amount: 10000.0) // FLOW LP deposits FLOW (creates FLOW credit balance) - createPosition(signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: flowLp, amount: 10000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // setup MOET borrower with FLOW collateral (creates MOET debit) let moetBorrower = Test.createAccount() @@ -196,7 +196,7 @@ fun test_collectStability_multipleTokens() { transferFlowTokens(to: moetBorrower, amount: 1000.0) // MOET borrower deposits FLOW and auto-borrows MOET (creates MOET debit balance) - createPosition(signer: moetBorrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: moetBorrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // setup FLOW borrower with MOET collateral (creates FLOW debit) let flowBorrower = Test.createAccount() @@ -204,9 +204,9 @@ fun test_collectStability_multipleTokens() { mintMoet(signer: PROTOCOL_ACCOUNT, to: flowBorrower.address, amount: 1000.0, beFailed: false) // FLOW borrower deposits MOET as collateral - createPosition(signer: flowBorrower, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: flowBorrower, amount: 1000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // Then borrow FLOW (creates FLOW debit balance) - borrowFromPosition(signer: flowBorrower, positionId: 3, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, amount: 500.0, beFailed: false) + borrowFromPosition(signer: flowBorrower, positionId: 3, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 500.0, beFailed: false) // set 10% annual debit rates // Stability is calculated on interest income, not debit balance directly @@ -290,7 +290,7 @@ fun test_collectStability_zeroRate_returnsNil() { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) // LP deposits MOET - createPosition(signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral let borrower = Test.createAccount() @@ -298,7 +298,7 @@ fun test_collectStability_zeroRate_returnsNil() { transferFlowTokens(to: borrower, amount: 1000.0) // borrower deposits FLOW and auto-borrows MOET (creates debit balance) - createPosition(signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // set stability fee rate to 0 let rateResult = setStabilityFeeRate(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, stabilityFeeRate: 0.0) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 2b07240a..95762ae5 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -371,6 +371,23 @@ fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, pri Test.expect(setRes, Test.beSucceeded()) } +access(all) +fun getOraclePrice(tokenIdentifier: String): UFix64 { + let result = Test.executeScript( + Test.readFile("../scripts/flow-alp/get_oracle_price.cdc"), + [tokenIdentifier] + ) + + if result.error != nil { + panic("Failed to get oracle price: ".concat(result.error!.message)) + } + + let price = result.returnValue! as! UFix64? + ?? panic("No price set for token: ".concat(tokenIdentifier)) + + return price +} + /// Sets a swapper for the given pair with the given price ratio. /// This overwrites any previously stored swapper for this pair, if any exists. /// This is intended to be used in tests both to set an initial DEX price for a supported token, @@ -477,9 +494,9 @@ fun setPoolPauseState( } access(all) -fun createPosition(signer: Test.TestAccount, amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: Bool) { +fun createPosition(admin: Test.TestAccount, signer: Test.TestAccount, amount: UFix64, vaultStoragePath: StoragePath, pushToDrawDownSink: Bool) { // Grant beta access to the signer if they don't have it yet - grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, signer) + grantBetaPoolParticipantAccess(admin, signer) let openRes = _executeTransaction( "../transactions/flow-alp/position/create_position.cdc", @@ -523,10 +540,10 @@ fun depositToPositionNotManaged(signer: Test.TestAccount, positionStoragePath: S } access(all) -fun borrowFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeIdentifier: String, amount: UFix64, beFailed: Bool) { +fun borrowFromPosition(signer: Test.TestAccount, positionId: UInt64, tokenTypeIdentifier: String, vaultStoragePath: StoragePath, amount: UFix64, beFailed: Bool) { let borrowRes = _executeTransaction( "./transactions/position-manager/borrow_from_position.cdc", - [positionId, tokenTypeIdentifier, amount], + [positionId, tokenTypeIdentifier, vaultStoragePath, amount], signer ) Test.expect(borrowRes, beFailed ? Test.beFailed() : Test.beSucceeded()) @@ -706,12 +723,33 @@ fun rebalancePosition(signer: Test.TestAccount, pid: UInt64, force: Bool, beFail Test.expect(rebalanceRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun manualLiquidation( + signer: Test.TestAccount, + pid: UInt64, + debtVaultIdentifier: String, + seizeVaultIdentifier: String, + seizeAmount: UFix64, + repayAmount: UFix64, +): Test.TransactionResult { + return _executeTransaction( + "../transactions/flow-alp/pool-management/manual_liquidation.cdc", + [pid, debtVaultIdentifier, seizeVaultIdentifier, seizeAmount, repayAmount], + signer + ) +} + access(all) fun setupMoetVault(_ signer: Test.TestAccount, beFailed: Bool) { let setupRes = _executeTransaction("../transactions/moet/setup_vault.cdc", [], signer) Test.expect(setupRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun setupGenericVault(_ signer: Test.TestAccount, vaultIdentifier: String): Test.TransactionResult { + return _executeTransaction("../transactions/fungible-tokens/setup_generic_vault.cdc", [vaultIdentifier], signer) +} + access(all) fun mintMoet(signer: Test.TestAccount, to: Address, amount: UFix64, beFailed: Bool) { let mintRes = _executeTransaction("../transactions/moet/mint_moet.cdc", [to, amount], signer) @@ -756,6 +794,23 @@ fun sendFlow(from: Test.TestAccount, to: Test.TestAccount, amount: UFix64) { Test.expect(res, Test.beSucceeded()) } +/// Transfers any fungible token from one account to another using the token identifier +access(all) +fun transferFungibleTokens( + tokenIdentifier: String, + from: Test.TestAccount, + to: Test.TestAccount, + amount: UFix64 +) { + let transferTx = Test.Transaction( + code: Test.readFile("../transactions/fungible-tokens/generic_transfer.cdc"), + authorizers: [from.address], + signers: [from], + arguments: [tokenIdentifier, amount, to.address] + ) + let res = Test.executeTransaction(transferTx) + Test.expect(res, Test.beSucceeded()) +} access(all) fun expectEvents(eventType: Type, expectedCount: Int) { diff --git a/cadence/tests/transactions/position-manager/borrow_from_position.cdc b/cadence/tests/transactions/position-manager/borrow_from_position.cdc index 552e2716..65a42e73 100644 --- a/cadence/tests/transactions/position-manager/borrow_from_position.cdc +++ b/cadence/tests/transactions/position-manager/borrow_from_position.cdc @@ -10,6 +10,7 @@ import "FlowALPv1" transaction( positionId: UInt64, tokenTypeIdentifier: String, + tokenVaultStoragePath: StoragePath, amount: UFix64 ) { let position: auth(FungibleToken.Withdraw) &FlowALPv1.Position @@ -29,23 +30,9 @@ transaction( // Parse the token type self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier: \(tokenTypeIdentifier)") - - // Ensure signer has a FlowToken vault to receive borrowed tokens - // (Most borrows in tests are FlowToken) - 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 borrow: \(tokenTypeIdentifier)") - } + + self.receiverVault = signer.storage.borrow<&{FungibleToken.Receiver}>(from: tokenVaultStoragePath) + ?? panic("Could not borrow receiver vault") } execute { diff --git a/cadence/tests/withdraw_stability_funds_test.cdc b/cadence/tests/withdraw_stability_funds_test.cdc index 34924e62..0a71f7cb 100644 --- a/cadence/tests/withdraw_stability_funds_test.cdc +++ b/cadence/tests/withdraw_stability_funds_test.cdc @@ -45,7 +45,7 @@ fun setupStabilityFundWithBalance(): UFix64 { mintMoet(signer: PROTOCOL_ACCOUNT, to: lp.address, amount: 10000.0, beFailed: false) // LP deposits MOET (creates credit balance, provides borrowing liquidity) - createPosition(signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + createPosition(admin: PROTOCOL_ACCOUNT, signer: lp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // setup borrower with FLOW collateral to create debit balance let borrower = Test.createAccount() @@ -53,7 +53,7 @@ fun setupStabilityFundWithBalance(): UFix64 { transferFlowTokens(to: borrower, amount: 1000.0) // borrower deposits FLOW and auto-borrows MOET (creates debit balance) - createPosition(signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) + createPosition(admin: PROTOCOL_ACCOUNT, signer: borrower, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: true) // set 10% annual debit rate (stability is calculated on interest income) setInterestCurveFixed(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: 0.1) diff --git a/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc b/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc new file mode 100644 index 00000000..c6fd1969 --- /dev/null +++ b/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc @@ -0,0 +1,82 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "MetadataViews" + +import "FlowALPv1" + +/// Batch liquidate multiple positions in a single transaction +/// +/// pids: Array of position IDs to liquidate +/// debtVaultIdentifier: e.g., Type<@FlowToken.Vault>().identifier +/// seizeVaultIdentifiers: Array of collateral vault identifiers to seize +/// seizeAmounts: Array of max seize amounts for each position +/// repayAmounts: Array of repay amounts for each position +transaction( + pids: [UInt64], + debtVaultIdentifier: String, + seizeVaultIdentifiers: [String], + seizeAmounts: [UFix64], + repayAmounts: [UFix64] +) { + let pool: &FlowALPv1.Pool + let debtType: Type + let debtVaultRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + let protocolAddress = Type<@FlowALPv1.Pool>().address! + self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv1.Pool>(FlowALPv1.PoolPublicPath) + ?? panic("Could not borrow Pool at \(FlowALPv1.PoolPublicPath)") + + self.debtType = CompositeType(debtVaultIdentifier) ?? panic("Invalid debtVaultIdentifier: \(debtVaultIdentifier)") + + let debtVaultData = MetadataViews.resolveContractViewFromTypeIdentifier( + resourceTypeIdentifier: debtVaultIdentifier, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not construct valid FT type and view from identifier \(debtVaultIdentifier)") + + self.debtVaultRef = signer.storage.borrow(from: debtVaultData.storagePath) + ?? panic("no debt vault in storage at path \(debtVaultData.storagePath)") + } + + execute { + let numPositions = pids.length + assert(seizeVaultIdentifiers.length == numPositions, message: "seizeVaultIdentifiers length mismatch") + assert(seizeAmounts.length == numPositions, message: "seizeAmounts length mismatch") + assert(repayAmounts.length == numPositions, message: "repayAmounts length mismatch") + + var totalRepaid = 0.0 + + for i in InclusiveRange(0, numPositions - 1) { + let pid = pids[i] + let seizeVaultIdentifier = seizeVaultIdentifiers[i] + let seizeAmount = seizeAmounts[i] + let repayAmount = repayAmounts[i] + + let seizeType = CompositeType(seizeVaultIdentifier) + ?? panic("Invalid seizeVaultIdentifier: \(seizeVaultIdentifier)") + + assert(self.debtVaultRef.balance >= repayAmount, + message: "Insufficient debt token balance for position \(pid)") + + let repay <- self.debtVaultRef.withdraw(amount: repayAmount) + + let seizedVault <- self.pool.manualLiquidation( + pid: pid, + debtType: self.debtType, + seizeType: seizeType, + seizeAmount: seizeAmount, + repayment: <-repay + ) + + totalRepaid = totalRepaid + repayAmount + + // Deposit seized collateral back to liquidator + // For simplicity, we'll just destroy it in this test transaction + // In production, you'd want to properly handle the seized collateral + destroy seizedVault + } + + log("Batch liquidation completed: \(numPositions) positions, total repaid: \(totalRepaid)") + } +} diff --git a/cadence/transactions/fungible-tokens/generic_transfer.cdc b/cadence/transactions/fungible-tokens/generic_transfer.cdc new file mode 100644 index 00000000..0c2ac110 --- /dev/null +++ b/cadence/transactions/fungible-tokens/generic_transfer.cdc @@ -0,0 +1,49 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" + +/// Transfers fungible tokens from the signer to a recipient using the token's identifier +/// +/// @param tokenIdentifier: The identifier of the Vault type (e.g., "A.0x1654653399040a61.FlowToken.Vault") +/// @param amount: The amount of tokens to transfer +/// @param recipient: The address to receive the tokens +transaction(tokenIdentifier: String, amount: UFix64, recipient: Address) { + let sentVault: @{FungibleToken.Vault} + let receiverRef: &{FungibleToken.Receiver} + + prepare(signer: auth(BorrowValue) &Account) { + // Resolve the Vault type from identifier + let vaultType = CompositeType(tokenIdentifier) + ?? panic("Invalid Vault identifier: \(tokenIdentifier)") + + let contractAddress = vaultType.address + ?? panic("Could not derive contract address from identifier: \(tokenIdentifier)") + let contractName = vaultType.contractName + ?? panic("Could not derive contract name from identifier: \(tokenIdentifier)") + + // Borrow the contract and resolve FTVaultData + let ftContract = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>(name: contractName) + ?? panic("No such FungibleToken contract found") + + let data = ftContract.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData for Vault type: \(tokenIdentifier)") + + // Borrow signer's vault and withdraw tokens + let vaultRef = signer.storage.borrow( + from: data.storagePath + ) ?? panic("Could not borrow reference to signer's vault at path: \(data.storagePath.toString())") + + self.sentVault <- vaultRef.withdraw(amount: amount) + + // Get recipient's receiver capability + self.receiverRef = getAccount(recipient).capabilities.borrow<&{FungibleToken.Receiver}>( + data.receiverPath + ) ?? panic("Could not borrow receiver reference for recipient at path: \(data.receiverPath.toString())") + } + + execute { + self.receiverRef.deposit(from: <-self.sentVault) + } +} \ No newline at end of file diff --git a/cadence/transactions/fungible-tokens/setup_generic_vault.cdc b/cadence/transactions/fungible-tokens/setup_generic_vault.cdc new file mode 100644 index 00000000..a83ee173 --- /dev/null +++ b/cadence/transactions/fungible-tokens/setup_generic_vault.cdc @@ -0,0 +1,45 @@ +import "FungibleToken" +import "NonFungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" + +/// Configures a Vault according to the shared FungibleToken standard and the defaults specified by the Vault's +/// defining contract. +/// +/// @param vaultIdentifier: The identifier of the Vault to configure. +transaction(vaultIdentifier: String) { + prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + // Gather identifying information about the Vault and its defining contract + let vaultType = CompositeType(vaultIdentifier) ?? panic("Invalid Vault identifier: \(vaultIdentifier)") + let contractAddress = vaultType.address + ?? panic("Could not derive contract address from identifier: \(vaultIdentifier)") + let contractName = vaultType.contractName + ?? panic("Could not derive contract name from identifier: \(vaultIdentifier)") + // Borrow the contract and resolve its Vault data + let ftContract = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>(name: contractName) + ?? panic("No such FungibleToken contract found") + let data = ftContract.resolveContractView( + resourceType: vaultType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData for Vault type: \(vaultIdentifier)") + + // Check for collision, returning if the vault already exists or reverting on unexpected collision + let storedType = signer.storage.type(at: data.storagePath) + if storedType == vaultType { + return + } else if storedType != nil { + panic( + "Another resource of type \(storedType!.identifier) already exists at the storage path: \(data.storagePath.toString())" + ) + } + + // Create a new vault and save it to signer's storage at the vault's default storage path + signer.storage.save(<-data.createEmptyVault(), to: data.storagePath) + + // Issue a public Vault capability and publish it to the vault's default public path + signer.capabilities.unpublish(data.receiverPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(data.storagePath) + signer.capabilities.publish(receiverCap, at: data.receiverPath) + } +} diff --git a/cadence/transactions/test/transfer_tokens_with_setup.cdc b/cadence/transactions/test/transfer_tokens_with_setup.cdc new file mode 100644 index 00000000..455fa6da --- /dev/null +++ b/cadence/transactions/test/transfer_tokens_with_setup.cdc @@ -0,0 +1,31 @@ +import FungibleToken from "FungibleToken" + +/// Transfer tokens from holder to recipient +/// Sets up recipient's vault if it doesn't exist +transaction(amount: UFix64, vaultPath: StoragePath) { + prepare(holder: auth(BorrowValue, Storage) &Account, recipient: auth(BorrowValue, Storage, Capabilities) &Account) { + + log("\(holder.address.toString())") + // Borrow holder's vault + let holderVault = holder.storage.borrow(from: vaultPath) + ?? panic("Could not borrow holder vault") + + // Setup recipient's vault if it doesn't exist + if recipient.storage.borrow<&{FungibleToken.Vault}>(from: vaultPath) == nil { + // Create empty vault + let emptyVault <- holderVault.withdraw(amount: 0.0) + recipient.storage.save(<-emptyVault, to: vaultPath) + + // Create and publish public capability + let pathIdentifier = vaultPath.toString().slice(from: 9, upTo: vaultPath.toString().length) + let publicPath = PublicPath(identifier: pathIdentifier)! + let cap = recipient.capabilities.storage.issue<&{FungibleToken.Receiver}>(vaultPath) + recipient.capabilities.publish(cap, at: publicPath) + } + + // Transfer tokens + let recipientVault = recipient.storage.borrow<&{FungibleToken.Receiver}>(from: vaultPath)! + let tokens <- holderVault.withdraw(amount: amount) + recipientVault.deposit(from: <-tokens) + } +} diff --git a/flow.json b/flow.json index 8d5f3e34..a5cf0192 100644 --- a/flow.json +++ b/flow.json @@ -25,13 +25,15 @@ "FlowALPv1": { "source": "./cadence/contracts/FlowALPv1.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet": "47f544294e3b7656" } }, "FlowALPMath": { "source": "./cadence/lib/FlowALPMath.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet": "6b00ff876c299c61" } }, "FlowALPRebalancerPaidv1": { @@ -55,25 +57,29 @@ "FungibleTokenConnectors": { "source": "./FlowActions/cadence/contracts/connectors/FungibleTokenConnectors.cdc", "aliases": { + "mainnet": "0c237e1265caa7a3", "testing": "0000000000000006" } }, "MOET": { "source": "./cadence/contracts/MOET.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet": "6b00ff876c299c61" } }, "MockDexSwapper": { "source": "./cadence/contracts/mocks/MockDexSwapper.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet": "6b00ff876c299c61" } }, "MockOracle": { "source": "./cadence/contracts/mocks/MockOracle.cdc", "aliases": { - "testing": "0000000000000007" + "testing": "0000000000000007", + "mainnet": "b1d63873c3cc9f79" } }, "MockYieldToken": { @@ -317,4 +323,4 @@ ] } } -} +} \ No newline at end of file From 4c2bf163afc76d684edf4e7c8e6680e4ac8ee14a Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Thu, 19 Feb 2026 13:58:23 +0100 Subject: [PATCH 02/25] =?UTF-8?q?Add=20testMassUnhealthyLiquidations=20?= =?UTF-8?q?=E2=80=94=20a=20system-wide=20stress=20test=20that=20creates=20?= =?UTF-8?q?100=20positions=20across=20three=20collateral=20types=20(50=20U?= =?UTF-8?q?SDF,=2045=20USDC,=205=20WBTC),=20crashes=20all=20collateral=20p?= =?UTF-8?q?rices=2040%=20simultaneously,=20and=20batch-liquidates=20all=20?= =?UTF-8?q?positions=20via=20MockDexSwapper=20in=20chunks=20of=2010=20to?= =?UTF-8?q?=20stay=20within=20computation=20limits.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fork_multiple_positions_per_user.cdc | 432 ++++++++++++++++-- .../batch_liquidate_via_mock_dex.cdc | 95 ++++ 2 files changed, 490 insertions(+), 37 deletions(-) create mode 100644 cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user.cdc index 29ad6ac2..0da341e2 100644 --- a/cadence/tests/fork_multiple_positions_per_user.cdc +++ b/cadence/tests/fork_multiple_positions_per_user.cdc @@ -158,8 +158,8 @@ access(all) fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - // Set minimum deposit for WBTC to 0.0001 (since holder only has 0.0005) - setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: WBTC_TOKEN_IDENTIFIER, minimum: 0.0001) + // Set minimum deposit for WBTC to 0.00001 (since holder only has 0.0005) + setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: WBTC_TOKEN_IDENTIFIER, minimum: 0.00001) snapshot = getCurrentBlockHeight() } @@ -176,6 +176,56 @@ access(all) fun transferTokensFromHolder(holder: Test.TestAccount, recipient: Te Test.expect(result, Test.beSucceeded()) } +/// Batch-liquidate positions using the liquidator's own tokens as repayment (no DEX). +/// The liquidator must hold sufficient debt tokens upfront. +access(all) fun batchManualLiquidation( + pids: [UInt64], + debtVaultIdentifier: String, + seizeVaultIdentifiers: [String], + seizeAmounts: [UFix64], + repayAmounts: [UFix64], + signer: Test.TestAccount +) { + let res = _executeTransaction( + "../transactions/flow-alp/pool-management/batch_manual_liquidation.cdc", + [pids, debtVaultIdentifier, seizeVaultIdentifiers, seizeAmounts, repayAmounts], + signer + ) + Test.expect(res, Test.beSucceeded()) +} + +/// Batch-liquidate positions using MockDexSwapper as the repayment source in chunks of +/// chunkSize to stay within the computation limit. +access(all) fun batchLiquidateViaMockDex( + pids: [UInt64], + debtVaultIdentifier: String, + seizeVaultIdentifiers: [String], + seizeAmounts: [UFix64], + repayAmounts: [UFix64], + chunkSize: Int, + signer: Test.TestAccount +) { + let total = pids.length + let numChunks = (total + chunkSize - 1) / chunkSize + for i in InclusiveRange(0, numChunks - 1) { + let startIdx = i * chunkSize + var endIdx = startIdx + chunkSize + if endIdx > total { + endIdx = total + } + let res = _executeTransaction( + "../transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc", + [pids.slice(from: startIdx, upTo: endIdx), + debtVaultIdentifier, + seizeVaultIdentifiers.slice(from: startIdx, upTo: endIdx), + seizeAmounts.slice(from: startIdx, upTo: endIdx), + repayAmounts.slice(from: startIdx, upTo: endIdx)], + signer + ) + Test.expect(res, Test.beSucceeded()) + } +} + /// Test Multiple Positions Per User /// /// Validates requirements: @@ -236,7 +286,6 @@ access(all) fun testMultiplePositionsPerUser() { let openEvts = Test.eventsOfType(Type()) userPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) - // Calculate USD value based on token price from oracle let price = getOraclePrice(tokenIdentifier: collateralType) let value = collateralAmount * price log(" Position \(userPids[i]): \(collateralAmount) \(collateralName) collateral (\(value) value)") @@ -338,14 +387,13 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let healthA_after1 = getPositionHealth(pid: positionA_id, beFailed: false) log(" Position A borrowed \(positionA_borrow1) FLOW - Health: \(healthA_after1)\n") - // Check remaining liquidity in pool - let remainingLiquidity1 = 340.0 // liquidityAmount - positionA_borrow1 = 400.0 - 60.0 = 340.0 - log(" Remaining liquidity in pool: \(remainingLiquidity1) FLOW\n") + // Check remaining liquidity in pool: liquidityAmount - positionA_borrow1 = 400.0 - 60.0 = 340.0 FLOW + log(" Remaining liquidity in pool: 340.0 FLOW\n") //////////// 2. Position B borrows successfully from shared pool /////////////////// log("Position B borrows from shared pool\n") - // Formula: Effective Collateral = (debitAmount * price) * collateralFactor = (500 × 1.0) × 0.85 = 425.00 + // Formula: Effective Collateral = (collateralAmount * price) * collateralFactor = (500 × 1.0) × 0.85 = 425.00 // Max Borrow = 425.00 / 1.1 (minHealth) = 386.36 FLOW let positionB_borrow1 = 340.0 // Borrow 340 FLOW (within max 386.36 borrow and 340 remaining liquidity) log(" Attempting to borrow \(positionB_borrow1) FLOW...") @@ -353,9 +401,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log(" Success - Position B borrowed \(positionB_borrow1) FLOW") let healthB_after1 = getPositionHealth(pid: positionB_id, beFailed: false) log(" Position B Health: \(healthB_after1)\n") - - let remainingLiquidity2 = 0.0 - log(" Remaining liquidity in pool: \(remainingLiquidity2) FLOW\n") + log(" Remaining liquidity in pool: 0.0 FLOW\n") //////////// 3. Position B tries to exceed max borrowing capacity - expects failure /////////////////// log("Position B tries to borrow beyond its capacity - EXPECTS FAILURE\n") @@ -379,9 +425,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let healthA_after2 = getPositionHealth(pid: positionA_id, beFailed: false) log(" Position A repaid \(repayAmount) FLOW - Health: \(healthA_after2)\n") - - let remainingLiquidity4 = repayAmount // 40.0, because remainingLiquidity2 == 0 - log(" Remaining liquidity in pool after repayment: \(remainingLiquidity4) FLOW\n") + log(" Remaining liquidity in pool after repayment: \(repayAmount) FLOW\n") //////////// Verify cross-position effects /////////////////// @@ -445,7 +489,7 @@ access(all) fun testBatchLiquidations() { // 5 positions with distinct collateral types: // - // pid | Collateral | Amount | Borrow | Crash price | Health after | Action + // pid | Collateral| Amount | Borrow | Crash price | Health after | Action // ----|-----------|-------------|----------|-------------|--------------|-------- // 1 | USDF | 500 USDF | 200 FLOW | $0.30 (-70%)| 0.638 | FULL liquidation // 2 | WETH | 0.06 WETH | 90 FLOW | $1050 (-70%)| 0.525 | FULL liquidation @@ -453,12 +497,10 @@ access(all) fun testBatchLiquidations() { // 4 | WBTC | 0.0004 WBTC | 10 FLOW | $25000(-50%)| 0.750 | PARTIAL liquidation // 5 | FLOW | 200 FLOW | 80 FLOW | $1.00 (0%) | 2.000 | NOT liquidated // - // FLOW position (pid=5): health = 0.8 * collateral / debt is price-independent - // when both collateral and debt are FLOW, so any FLOW price crash leaves it unaffected. log("Creating 5 positions with different collateral types\n") let positions = [ - {"type": USDF_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": usdfHolder, "borrow": 200.0}, + {"type": USDF_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": usdfHolder, "borrow": 200.0}, {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.06, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": wethHolder, "borrow": 90.0}, {"type": USDC_TOKEN_IDENTIFIER, "amount": 80.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": usdcHolder, "borrow": 40.0}, {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": wbtcHolder, "borrow": 10.0}, @@ -558,17 +600,20 @@ access(all) fun testBatchLiquidations() { // Liquidator setup: transfer FLOW for debt repayment (total needed: 71+113+4+12 = 200 FLOW) // and 1 unit of each collateral token to initialize vault storage paths. + // + // Repay amounts derived from: repay = debt - (collat - seize) * CF * P_crashed / H_target + // WETH=71: debt=90, (0.06-0.035)*0.75*1050 = 19.6875, H≈1.034 → 90 - 19.6875/1.034 ≈ 71 + // USDF=113: debt=200, (500-147)*0.85*0.3 = 90.015, H≈1.034 → 200 - 90.015/1.034 ≈ 113 + // WBTC=4: partial; (0.0004-0.00011)*0.75*25000 = 5.4375 → repay=4 → postHealth=5.4375/6≈0.906 + // USDC=12: partial; (80-17)*0.85*0.5 = 26.775 → repay=12 → postHealth=26.775/28≈0.956 log("\nSetting up liquidator account\n") let liquidator = Test.createAccount() - transferTokensFromHolder(holder: flowHolder, recipient: liquidator, amount: 250.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") - transferTokensFromHolder(holder: usdfHolder, recipient: liquidator, amount: 1.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") - transferTokensFromHolder(holder: wethHolder, recipient: liquidator, amount: 0.001, storagePath: WETH_VAULT_STORAGE_PATH, tokenName: "WETH") - transferTokensFromHolder(holder: usdcHolder, recipient: liquidator, amount: 1.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") - transferTokensFromHolder(holder: wbtcHolder, recipient: liquidator, amount: 0.00001, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") - - // Batch liquidation parameters — ordered worst health first: - // WETH (0.525) → USDF (0.638) → WBTC (0.750) → USDC (0.850) - // + transferTokensFromHolder(holder: flowHolder, recipient: liquidator, amount: 250.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: usdfHolder, recipient: liquidator, amount: 1.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") + transferTokensFromHolder(holder: wethHolder, recipient: liquidator, amount: 0.001, storagePath: WETH_VAULT_STORAGE_PATH, tokenName: "WETH") + transferTokensFromHolder(holder: usdcHolder, recipient: liquidator, amount: 1.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") + transferTokensFromHolder(holder: wbtcHolder, recipient: liquidator, amount: 0.00001, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") + // seize/repay values satisfy three constraints: // 1. seize < quote.inAmount (offer beats DEX price) // 2. postHealth <= 1.05 (liquidationTargetHF default) @@ -591,18 +636,19 @@ access(all) fun testBatchLiquidations() { // DEX check: 17 < 12/0.5 = 24 log("\nExecuting batch liquidation of 4 positions (2 full, 2 partial) in SINGLE transaction...\n") - // Ordered worst health first: WETH (idx=1), USDF (idx=0), WBTC (idx=3), USDC (idx=2) - let batchPids = [userPids[1], userPids[0], userPids[3], userPids[2] ] - let batchSeizeTypes = [WETH_TOKEN_IDENTIFIER, USDF_TOKEN_IDENTIFIER, WBTC_TOKEN_IDENTIFIER, USDC_TOKEN_IDENTIFIER ] - let batchSeizeAmounts = [0.035, 147.0, 0.00011, 17.0 ] - let batchRepayAmounts = [71.0, 113.0, 4.0, 12.0 ] - - let batchLiqRes = _executeTransaction( - "../transactions/flow-alp/pool-management/batch_manual_liquidation.cdc", - [batchPids, FLOW_TOKEN_IDENTIFIER_MAINNET, batchSeizeTypes, batchSeizeAmounts, batchRepayAmounts], - liquidator + let batchPids = [userPids[0], userPids[1], userPids[2], userPids[3] ] + let batchSeizeTypes = [USDF_TOKEN_IDENTIFIER, WETH_TOKEN_IDENTIFIER, USDC_TOKEN_IDENTIFIER, WBTC_TOKEN_IDENTIFIER] + let batchSeizeAmounts = [147.0, 0.035, 17.0, 0.00011 ] + let batchRepayAmounts = [113.0, 71.0, 12.0, 4.0 ] + + batchManualLiquidation( + pids: batchPids, + debtVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + seizeVaultIdentifiers: batchSeizeTypes, + seizeAmounts: batchSeizeAmounts, + repayAmounts: batchRepayAmounts, + signer: liquidator ) - Test.expect(batchLiqRes, Test.beSucceeded()) log("\nVerifying results after batch liquidation:\n") @@ -627,3 +673,315 @@ access(all) fun testBatchLiquidations() { log(" FLOW (NONE): \(healths[4]) -> \(healthAfterFlow)") Test.assert(healthAfterFlow == healths[4], message: "FLOW position health should be unchanged") } + +/// Test Mass Simultaneous Unhealthy Positions – 100-Position Multi-Collateral Stress Test +/// +/// System-wide stress test validating protocol behavior under mass position failure +/// across three collateral types — all crashing 40% simultaneously: +/// +/// 100 positions (all borrowing FLOW as debt): +/// Group A: 50 USDF positions (10 USDF each) — 25 high-risk + 25 moderate +/// Group B: 45 USDC positions (2 USDC each) — 23 high-risk + 22 moderate +/// Group C: 5 WBTC positions (0.00009 WBTC ea) — 5 uniform (same risk tier) +/// +/// Health before crash (CF_USDF=CF_USDC=0.85, CF_WBTC=0.75): +/// USDF high-risk: borrow 7.0 FLOW → (10×1.0×0.85)/7.0 = 1.214 +/// USDF moderate: borrow 6.0 FLOW → (10×1.0×0.85)/6.0 = 1.417 +/// USDC high-risk: borrow 1.4 FLOW → (2×1.0×0.85)/1.4 = 1.214 +/// USDC moderate: borrow 1.2 FLOW → (2×1.0×0.85)/1.2 = 1.417 +/// WBTC uniform: borrow 2.5 FLOW → (0.00009×50000×0.75)/2.5 = 1.350 +/// +/// All collateral crashes 40% simultaneously: +/// USDF: $1.00 → $0.60 | USDC: $1.00 → $0.60 | WBTC: $50000 → $30000 +/// +/// Health after crash: +/// USDF high: (10×0.60×0.85)/7.0 = 0.729 USDF mod: (10×0.60×0.85)/6.0 = 0.850 +/// USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 +/// WBTC: (0.00009×30000×0.75)/2.5 = 0.810 +/// +/// Liquidation (liquidationTargetHF=1.05, post target≈1.02–1.04): +/// USDF high: seize 4.0 USDF, repay 4.0 FLOW → post = (10-4)×0.6×0.85/(7-4) = 1.02 +/// DEX: 4.0 < 4.0/0.6 = 6.67 +/// USDF mod: seize 4.0 USDF, repay 3.0 FLOW → post = (10-4)×0.6×0.85/(6-3) = 1.02 +/// DEX: 4.0 < 3.0/0.6 = 5.00 +/// USDC high: seize 0.8 USDC, repay 0.8 FLOW → post = (2-0.8)×0.6×0.85/(1.4-0.8) = 1.02 +/// DEX: 0.8 < 0.8/0.6 = 1.33 +/// USDC mod: seize 0.8 USDC, repay 0.6 FLOW → post = (2-0.8)×0.6×0.85/(1.2-0.6) = 1.02 +/// DEX: 0.8 < 0.6/0.6 = 1.00 +/// WBTC: seize 0.00003 WBTC, repay 1.18 FLOW → post = (0.00006)×22500/(2.5-1.18) = 1.023 +/// DEX: 0.00003 < 1.18/30000 = 0.0000393 +/// +/// Batch order (worst health first): USDF-high (0.729) → USDC-high (0.729) → WBTC (0.810) → USDF-mod (0.850) → USDC-mod (0.850) +/// +/// Token budget (mainnet): +/// flowHolder (1921 FLOW): 450 LP + 230 DEX source = 680 FLOW total +/// usdfHolder (25000 USDF): 500 USDF for 50 positions +/// usdcHolder (97 USDC): 90 USDC for 45 positions +/// wbtcHolder (0.0005 WBTC): 0.00045 WBTC for 5 positions (holder has 0.00049998) +access(all) fun testMassUnhealthyLiquidations() { + safeReset() + + log("=== Stress Test: 100 Positions (USDF/USDC/WBTC) Simultaneously Unhealthy ===\n") + + let lpUser = Test.createAccount() + let user = Test.createAccount() + let liquidator = Test.createAccount() + + //////////// LP setup /////////////////// + + // LP deposits 450 FLOW — covers the ~397 FLOW of total borrows with headroom. + log("LP depositing 450 FLOW to shared liquidity pool\n") + transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: 450.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + createPosition(admin: protocolAccount, signer: lpUser, amount: 450.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + //////////// Transfer collateral to user /////////////////// + + // Group A: 50 positions × 10 USDF = 500 USDF + // Group B: 45 positions × 2 USDC = 90 USDC + // Group C: 5 positions × 0.00009 WBTC = 0.00045 WBTC + log("Transferring collateral: 500 USDF + 90 USDC + 0.00045 WBTC\n") + transferTokensFromHolder(holder: usdfHolder, recipient: user, amount: 500.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") + transferTokensFromHolder(holder: usdcHolder, recipient: user, amount: 90.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") + transferTokensFromHolder(holder: wbtcHolder, recipient: user, amount: 0.00045, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") + + //////////// Create 100 positions /////////////////// + + var allPids: [UInt64] = [] + + // Group A — 50 USDF positions + log("Creating 50 USDF positions (10 USDF each)...\n") + for i in InclusiveRange(0, 49) { + createPosition(admin: protocolAccount, signer: user, amount: 10.0, vaultStoragePath: USDF_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + let openEvts = Test.eventsOfType(Type()) + allPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + } + + // Group B — 45 USDC positions + log("Creating 45 USDC positions (2 USDC each)...\n") + for i in InclusiveRange(50, 94) { + createPosition(admin: protocolAccount, signer: user, amount: 2.0, vaultStoragePath: USDC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + let openEvts = Test.eventsOfType(Type()) + allPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + } + + // Group C — 5 WBTC positions + log("Creating 5 WBTC positions (0.00009 WBTC each)...\n") + for i in InclusiveRange(95, 99) { + createPosition(admin: protocolAccount, signer: user, amount: 0.00009, vaultStoragePath: WBTC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + let openEvts = Test.eventsOfType(Type()) + allPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + } + + Test.assert(allPids.length == 100, message: "Expected 100 positions, got \(allPids.length)") + + //////////// Borrow FLOW from each position /////////////////// + + // Group A — USDF positions: + // high-risk [0..24]: borrow 7.0 FLOW → health = (10×1.0×0.85)/7.0 = 1.214 + // moderate [25..49]: borrow 6.0 FLOW → health = (10×1.0×0.85)/6.0 = 1.417 + log("Borrowing FLOW from 50 USDF positions...\n") + for i in InclusiveRange(0, 24) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 7.0, beFailed: false) + } + for i in InclusiveRange(25, 49) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 6.0, beFailed: false) + } + + // Group B — USDC positions: + // high-risk [50..72]: borrow 1.4 FLOW → health = (2×1.0×0.85)/1.4 = 1.214 + // moderate [73..94]: borrow 1.2 FLOW → health = (2×1.0×0.85)/1.2 = 1.417 + log("Borrowing FLOW from 45 USDC positions...\n") + for i in InclusiveRange(50, 72) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.4, beFailed: false) + } + for i in InclusiveRange(73, 94) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.2, beFailed: false) + } + + // Group C — WBTC positions: + // uniform [95..99]: borrow 2.5 FLOW → health = (0.00009×50000×0.75)/2.5 = 1.350 + log("Borrowing FLOW from 5 WBTC positions...\n") + for i in InclusiveRange(95, 99) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 2.5, beFailed: false) + } + + // Confirm all 100 positions are healthy before the crash + for i in InclusiveRange(0, 99) { + let health = getPositionHealth(pid: allPids[i], beFailed: false) + Test.assert(health > 1.0, message: "Position \(allPids[i]) must be healthy before crash (got \(health))") + } + + //////////// Simulate 40% price crash across all three collateral types /////////////////// + + // USDF/USDC: $1.00 → $0.60 (-40%) | WBTC: $50000 → $30000 (-40%) + // + // Health after crash: + // USDF high: (10×0.60×0.85)/7.0 = 0.729 USDF mod: (10×0.60×0.85)/6.0 = 0.850 + // USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 + // WBTC: (0.00009×30000×0.75)/2.5 = 0.810 + log("All three collateral types crash 40% simultaneously\n") + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDF_TOKEN_IDENTIFIER, price: 0.6) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 0.6) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WBTC_TOKEN_IDENTIFIER, price: 30000.0) + + // Capture post-crash health by token type and verify all positions are unhealthy + var usdfHealths: [UFix128] = [] + var usdcHealths: [UFix128] = [] + var wbtcHealths: [UFix128] = [] + + for i in InclusiveRange(0, 49) { + let h = getPositionHealth(pid: allPids[i], beFailed: false) + usdfHealths.append(h) + Test.assert(h < 1.0, message: "USDF pos \(allPids[i]) must be unhealthy (got \(h))") + } + for i in InclusiveRange(50, 94) { + let h = getPositionHealth(pid: allPids[i], beFailed: false) + usdcHealths.append(h) + Test.assert(h < 1.0, message: "USDC pos \(allPids[i]) must be unhealthy (got \(h))") + } + for i in InclusiveRange(95, 99) { + let h = getPositionHealth(pid: allPids[i], beFailed: false) + wbtcHealths.append(h) + Test.assert(h < 1.0, message: "WBTC pos \(allPids[i]) must be unhealthy (got \(h))") + } + + // Verify risk ordering: high-risk (more debt) → worse health than moderate + // usdfHealths[0]=high-risk, usdfHealths[25]=first moderate; usdcHealths[0]=high-risk, usdcHealths[23]=first moderate + Test.assert(usdfHealths[0] < usdfHealths[25], message: "USDF high-risk must be worse than moderate") + Test.assert(usdcHealths[0] < usdcHealths[23], message: "USDC high-risk must be worse than moderate") + + log(" USDF high: \(usdfHealths[0]) (≈0.729) mod: \(usdfHealths[25]) (≈0.850)\n") + log(" USDC high: \(usdcHealths[0]) (≈0.729) mod: \(usdcHealths[23]) (≈0.850)\n") + log(" WBTC: \(wbtcHealths[0]) (≈0.810)\n") + log(" All 100 positions confirmed unhealthy — proceeding to batch liquidation\n") + + //////////// DEX setup /////////////////// + + // Three DEX pairs (all source FLOW from protocolAccount's vault): + // USDF→FLOW at priceRatio=0.6 ($0.60 USDF / $1.00 FLOW) + // USDC→FLOW at priceRatio=0.6 ($0.60 USDC / $1.00 FLOW) + // WBTC→FLOW at priceRatio=30000 ($30000 WBTC / $1.00 FLOW) + // + // Total DEX FLOW: 25×4.0 + 25×3.0 + 23×0.8 + 22×0.6 + 5×1.18 + // = 100 + 75 + 18.4 + 13.2 + 5.90 = 212.50; transfer 230 for headroom + log("Configuring DEX pairs: USDF→FLOW, USDC→FLOW, WBTC→FLOW\n") + transferTokensFromHolder(holder: flowHolder, recipient: protocolAccount, amount: 230.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: USDF_TOKEN_IDENTIFIER, + outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, + priceRatio: 0.6 // $0.60 USDF / $1.00 FLOW + ) + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: USDC_TOKEN_IDENTIFIER, + outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, + priceRatio: 0.6 // $0.60 USDC / $1.00 FLOW + ) + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: WBTC_TOKEN_IDENTIFIER, + outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, + priceRatio: 30000.0 // $30000 WBTC / $1.00 FLOW + ) + + //////////// Build batch parameters (ordered worst health first) /////////////////// + // + // Seize/repay parameters: + // USDF high [0..24]: seize 4.0 USDF, repay 4.0 FLOW post=1.02, DEX: 4<6.67 + // USDC high [50..72]: seize 0.8 USDC, repay 0.8 FLOW post=1.02, DEX: 0.8<1.33 + // WBTC [95..99]: seize 0.00003 WBTC, repay 1.18 FLOW post=1.023, DEX: 0.00003<0.0000393 + // USDF mod [25..49]: seize 4.0 USDF, repay 3.0 FLOW post=1.02, DEX: 4<5.00 + // USDC mod [73..94]: seize 0.8 USDC, repay 0.6 FLOW post=1.02, DEX: 0.8<1.00 + var batchPids: [UInt64] = [] + var batchSeize: [String] = [] + var batchAmounts: [UFix64] = [] + var batchRepay: [UFix64] = [] + + // USDF high-risk [0..24] + for i in InclusiveRange(0, 24) { + batchPids.append(allPids[i]) + batchSeize.append(USDF_TOKEN_IDENTIFIER) + batchAmounts.append(4.0) + batchRepay.append(4.0) + } + // USDC high-risk [50..72] + for i in InclusiveRange(50, 72) { + batchPids.append(allPids[i]) + batchSeize.append(USDC_TOKEN_IDENTIFIER) + batchAmounts.append(0.8) + batchRepay.append(0.8) + } + // WBTC uniform [95..99] + for i in InclusiveRange(95, 99) { + batchPids.append(allPids[i]) + batchSeize.append(WBTC_TOKEN_IDENTIFIER) + batchAmounts.append(0.00003) + batchRepay.append(1.18) + } + // USDF moderate [25..49] + for i in InclusiveRange(25, 49) { + batchPids.append(allPids[i]) + batchSeize.append(USDF_TOKEN_IDENTIFIER) + batchAmounts.append(4.0) + batchRepay.append(3.0) + } + // USDC moderate [73..94] + for i in InclusiveRange(73, 94) { + batchPids.append(allPids[i]) + batchSeize.append(USDC_TOKEN_IDENTIFIER) + batchAmounts.append(0.8) + batchRepay.append(0.6) + } + + Test.assert(batchPids.length == 100, message: "Expected 100 batch entries, got \(batchPids.length)") + + //////////// Batch liquidation — 100 positions in chunks of 10 /////////////////// + + // Split into chunks of 10 to stay within the computation limit (single tx of 100 exceeds it). + // DEX sources FLOW from protocolAccount's vault; liquidator needs no tokens upfront. + log("Liquidating all 100 positions via DEX in chunks of 10...\n") + batchLiquidateViaMockDex( + pids: batchPids, + debtVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + seizeVaultIdentifiers: batchSeize, + seizeAmounts: batchAmounts, + repayAmounts: batchRepay, + chunkSize: 10, + signer: liquidator + ) + + //////////// Verification /////////////////// + + // All 100 positions must have improved and be healthy again + log("Verifying all 100 positions recovered...\n") + + // USDF [0..49] + for i in InclusiveRange(0, 49) { + let h = getPositionHealth(pid: allPids[i], beFailed: false) + Test.assert(h > usdfHealths[i], message: "USDF pos \(allPids[i]) health must improve: \(usdfHealths[i]) → \(h)") + Test.assert(h > 1.0, message: "USDF pos \(allPids[i]) must be healthy again (got \(h))") + } + // USDC [50..94] + for i in InclusiveRange(0, 44) { + let pidIdx = i + 50 + let h = getPositionHealth(pid: allPids[pidIdx], beFailed: false) + Test.assert(h > usdcHealths[i], message: "USDC pos \(allPids[i]) health must improve: \(usdcHealths[i]) → \(h)") + Test.assert(h > 1.0, message: "USDC pos \(allPids[pidIdx]) must be healthy again (got \(h))") + } + // WBTC [95..99] + for i in InclusiveRange(0, 4) { + let pidIdx = i + 95 + let h = getPositionHealth(pid: allPids[pidIdx], beFailed: false) + Test.assert(h > wbtcHealths[i], message: "WBTC pos \(allPids[pidIdx]) health must improve: \(wbtcHealths[i]) → \(h)") + Test.assert(h > 1.0, message: "WBTC pos \(allPids[pidIdx]) must be healthy again (got \(h))") + } + + // Protocol solvency: FLOW reserve must remain positive after mass liquidation + let reserveBalance = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET) + log("Protocol FLOW reserve after mass liquidation: \(reserveBalance)\n") + Test.assert(reserveBalance > 0.0, message: "Protocol must remain solvent (positive FLOW reserve) after mass liquidation") +} diff --git a/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc b/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc new file mode 100644 index 00000000..f9edac9f --- /dev/null +++ b/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc @@ -0,0 +1,95 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "MetadataViews" + +import "FlowALPv1" +import "MockDexSwapper" + +/// TEST-ONLY: Batch liquidate multiple positions using the stored MockDexSwapper as the debt +/// repayment source. The swapper's vaultSource (configured via setMockDexPriceForPair) withdraws +/// the required debt tokens, so the transaction signer needs no debt tokens upfront. +/// +/// Positions are liquidated in the order provided (caller is responsible for ordering by priority). +/// +/// pids: Array of position IDs to liquidate +/// debtVaultIdentifier: e.g., Type<@FlowToken.Vault>().identifier +/// seizeVaultIdentifiers: Array of collateral vault identifiers to seize (one per position) +/// seizeAmounts: Array of collateral amounts to seize from each position +/// repayAmounts: Array of debt amounts to repay for each position (sourced from the DEX) +transaction( + pids: [UInt64], + debtVaultIdentifier: String, + seizeVaultIdentifiers: [String], + seizeAmounts: [UFix64], + repayAmounts: [UFix64] +) { + let pool: &FlowALPv1.Pool + let debtType: Type + + prepare(signer: &Account) { + let protocolAddress = Type<@FlowALPv1.Pool>().address! + self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv1.Pool>(FlowALPv1.PoolPublicPath) + ?? panic("Could not borrow Pool at \(FlowALPv1.PoolPublicPath)") + + self.debtType = CompositeType(debtVaultIdentifier) + ?? panic("Invalid debtVaultIdentifier: \(debtVaultIdentifier)") + } + + execute { + let numPositions = pids.length + assert(seizeVaultIdentifiers.length == numPositions, message: "seizeVaultIdentifiers length mismatch") + assert(seizeAmounts.length == numPositions, message: "seizeAmounts length mismatch") + assert(repayAmounts.length == numPositions, message: "repayAmounts length mismatch") + + var totalRepaid = 0.0 + + for idx in InclusiveRange(0, numPositions - 1) { + let pid = pids[idx] + let seizeVaultIdentifier = seizeVaultIdentifiers[idx] + let seizeAmount = seizeAmounts[idx] + let repayAmount = repayAmounts[idx] + + let seizeType = CompositeType(seizeVaultIdentifier) + ?? panic("Invalid seizeVaultIdentifier: \(seizeVaultIdentifier)") + + // Retrieve the stored MockDexSwapper for this collateral → debt pair. + // The swapper's vaultSource (protocolAccount's vault) provides the debt tokens. + let swapper = MockDexSwapper.getSwapper(inType: seizeType, outType: self.debtType) + ?? panic("No MockDexSwapper configured for \(seizeVaultIdentifier) -> \(debtVaultIdentifier)") + + // Build an exact quote for the repayAmount we need from the swapper's vaultSource + let swapQuote = MockDexSwapper.BasicQuote( + inType: seizeType, + outType: self.debtType, + inAmount: 0.0, + outAmount: repayAmount + ) + + // Create an empty collateral vault as a dummy swap input — MockDexSwapper burns it + // and withdraws repayAmount debt tokens from its configured vaultSource instead. + let seizeVaultData = MetadataViews.resolveContractViewFromTypeIdentifier( + resourceTypeIdentifier: seizeVaultIdentifier, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for \(seizeVaultIdentifier)") + let emptyCollateralVault <- seizeVaultData.createEmptyVault() + + // Swap: burns emptyCollateralVault, withdraws repayAmount from vaultSource + let repayVault <- swapper.swap(quote: swapQuote, inVault: <-emptyCollateralVault) + + // Execute the liquidation: pool seizes collateral, caller provides repayment + let seizedVault <- self.pool.manualLiquidation( + pid: pid, + debtType: self.debtType, + seizeType: seizeType, + seizeAmount: seizeAmount, + repayment: <-repayVault + ) + + totalRepaid = totalRepaid + repayAmount + destroy seizedVault + } + + log("Batch DEX liquidation completed: \(numPositions) positions, total repaid: \(totalRepaid)") + } +} From 441c58e48cbd383b1570ba6ece877666998982bb Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Thu, 19 Feb 2026 14:35:07 +0100 Subject: [PATCH 03/25] fixes after merge --- .../fork_multiple_positions_per_user.cdc | 38 +++++++++---------- .../batch_liquidate_via_mock_dex.cdc | 10 ++--- .../batch_manual_liquidation.cdc | 10 ++--- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user.cdc index 0da341e2..675969ce 100644 --- a/cadence/tests/fork_multiple_positions_per_user.cdc +++ b/cadence/tests/fork_multiple_positions_per_user.cdc @@ -6,7 +6,7 @@ import BlockchainHelpers import "FlowToken" import "FungibleToken" import "MOET" -import "FlowALPv1" +import "FlowALPv0" import "test_helpers.cdc" // Real mainnet token identifiers (overriding test_helpers for mainnet) @@ -24,7 +24,7 @@ access(all) let WETH_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3 access(all) let WBTC_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Vault // Protocol account: in fork mode, Test.deployContract() deploys to the contract's mainnet -// alias address. FlowALPv1's mainnet alias is 0x47f544294e3b7656, so PoolFactory and all +// alias address. FlowALPv0's mainnet alias is 0x47f544294e3b7656, so PoolFactory and all // pool admin resources are stored there. Note: this is the same address as wbtcHolder. access(all) let protocolAccount = Test.getAccount(0x47f544294e3b7656) @@ -92,10 +92,10 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - // Deploy FlowALPv1 + // Deploy FlowALPv0 err = Test.deployContract( - name: "FlowALPv1", - path: "../contracts/FlowALPv1.cdc", + name: "FlowALPv0", + path: "../contracts/FlowALPv0.cdc", arguments: [] ) Test.expect(err, Test.beNil()) @@ -283,8 +283,8 @@ access(all) fun testMultiplePositionsPerUser() { transferTokensFromHolder(holder: holder, recipient: user, amount: collateralAmount, storagePath: storagePath, tokenName: collateralName) createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) - let openEvts = Test.eventsOfType(Type()) - userPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + let openEvts = Test.eventsOfType(Type()) + userPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) let price = getOraclePrice(tokenIdentifier: collateralType) let value = collateralAmount * price @@ -363,8 +363,8 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log("Creating Position A with \(userACollateral) USDC collateral\n") transferTokensFromHolder(holder: usdcHolder, recipient: user, amount: userACollateral, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") createPosition(admin: protocolAccount, signer: user, amount: userACollateral, vaultStoragePath: USDC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - var openEvts = Test.eventsOfType(Type()) - let positionA_id = (openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid + var openEvts = Test.eventsOfType(Type()) + let positionA_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid //////////// Create Position B with USDF collateral /////////////////// @@ -372,8 +372,8 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log("Creating Position B with \(userBCollateral) USDF collateral\n") transferTokensFromHolder(holder: usdfHolder, recipient: user, amount: userBCollateral, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") createPosition(admin: protocolAccount, signer: user, amount: userBCollateral, vaultStoragePath: USDF_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - openEvts = Test.eventsOfType(Type()) - let positionB_id = (openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid + openEvts = Test.eventsOfType(Type()) + let positionB_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid //////////// 1. Position A borrows heavily, affecting available liquidity /////////////////// @@ -517,8 +517,8 @@ access(all) fun testBatchLiquidations() { transferTokensFromHolder(holder: holder, recipient: user, amount: collateralAmount, storagePath: storagePath, tokenName: collateralName) createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) - let openEvts = Test.eventsOfType(Type()) - userPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + let openEvts = Test.eventsOfType(Type()) + userPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } log("Borrowing FLOW from each position\n") @@ -752,24 +752,24 @@ access(all) fun testMassUnhealthyLiquidations() { log("Creating 50 USDF positions (10 USDF each)...\n") for i in InclusiveRange(0, 49) { createPosition(admin: protocolAccount, signer: user, amount: 10.0, vaultStoragePath: USDF_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvts = Test.eventsOfType(Type()) - allPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + let openEvts = Test.eventsOfType(Type()) + allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } // Group B — 45 USDC positions log("Creating 45 USDC positions (2 USDC each)...\n") for i in InclusiveRange(50, 94) { createPosition(admin: protocolAccount, signer: user, amount: 2.0, vaultStoragePath: USDC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvts = Test.eventsOfType(Type()) - allPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + let openEvts = Test.eventsOfType(Type()) + allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } // Group C — 5 WBTC positions log("Creating 5 WBTC positions (0.00009 WBTC each)...\n") for i in InclusiveRange(95, 99) { createPosition(admin: protocolAccount, signer: user, amount: 0.00009, vaultStoragePath: WBTC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvts = Test.eventsOfType(Type()) - allPids.append((openEvts[openEvts.length - 1] as! FlowALPv1.Opened).pid) + let openEvts = Test.eventsOfType(Type()) + allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } Test.assert(allPids.length == 100, message: "Expected 100 positions, got \(allPids.length)") diff --git a/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc b/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc index f9edac9f..389477cb 100644 --- a/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc +++ b/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "MetadataViews" -import "FlowALPv1" +import "FlowALPv0" import "MockDexSwapper" /// TEST-ONLY: Batch liquidate multiple positions using the stored MockDexSwapper as the debt @@ -23,13 +23,13 @@ transaction( seizeAmounts: [UFix64], repayAmounts: [UFix64] ) { - let pool: &FlowALPv1.Pool + let pool: &FlowALPv0.Pool let debtType: Type prepare(signer: &Account) { - let protocolAddress = Type<@FlowALPv1.Pool>().address! - self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv1.Pool>(FlowALPv1.PoolPublicPath) - ?? panic("Could not borrow Pool at \(FlowALPv1.PoolPublicPath)") + let protocolAddress = Type<@FlowALPv0.Pool>().address! + self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not borrow Pool at \(FlowALPv0.PoolPublicPath)") self.debtType = CompositeType(debtVaultIdentifier) ?? panic("Invalid debtVaultIdentifier: \(debtVaultIdentifier)") diff --git a/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc b/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc index c6fd1969..a5f7933d 100644 --- a/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc +++ b/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc @@ -2,7 +2,7 @@ import "FungibleToken" import "FungibleTokenMetadataViews" import "MetadataViews" -import "FlowALPv1" +import "FlowALPv0" /// Batch liquidate multiple positions in a single transaction /// @@ -18,14 +18,14 @@ transaction( seizeAmounts: [UFix64], repayAmounts: [UFix64] ) { - let pool: &FlowALPv1.Pool + let pool: &FlowALPv0.Pool let debtType: Type let debtVaultRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { - let protocolAddress = Type<@FlowALPv1.Pool>().address! - self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv1.Pool>(FlowALPv1.PoolPublicPath) - ?? panic("Could not borrow Pool at \(FlowALPv1.PoolPublicPath)") + let protocolAddress = Type<@FlowALPv0.Pool>().address! + self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not borrow Pool at \(FlowALPv0.PoolPublicPath)") self.debtType = CompositeType(debtVaultIdentifier) ?? panic("Invalid debtVaultIdentifier: \(debtVaultIdentifier)") From 86086fa1cadb1fd4b27e8fcb4f32d83c8c9c5d68 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Thu, 19 Feb 2026 15:06:35 +0100 Subject: [PATCH 04/25] code style fix --- .../fork_multiple_positions_per_user.cdc | 138 +++++++++--------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user.cdc index 675969ce..58df77dd 100644 --- a/cadence/tests/fork_multiple_positions_per_user.cdc +++ b/cadence/tests/fork_multiple_positions_per_user.cdc @@ -164,7 +164,7 @@ access(all) fun setup() { snapshot = getCurrentBlockHeight() } -/// Transfer tokens from holder to recipient (creates vault for recipient if needed) +// Transfer tokens from holder to recipient (creates vault for recipient if needed) access(all) fun transferTokensFromHolder(holder: Test.TestAccount, recipient: Test.TestAccount, amount: UFix64, storagePath: StoragePath, tokenName: String) { let tx = Test.Transaction( code: Test.readFile("../transactions/test/transfer_tokens_with_setup.cdc"), @@ -176,8 +176,8 @@ access(all) fun transferTokensFromHolder(holder: Test.TestAccount, recipient: Te Test.expect(result, Test.beSucceeded()) } -/// Batch-liquidate positions using the liquidator's own tokens as repayment (no DEX). -/// The liquidator must hold sufficient debt tokens upfront. +// Batch-liquidate positions using the liquidator's own tokens as repayment (no DEX). +// The liquidator must hold sufficient debt tokens upfront. access(all) fun batchManualLiquidation( pids: [UInt64], debtVaultIdentifier: String, @@ -194,8 +194,8 @@ access(all) fun batchManualLiquidation( Test.expect(res, Test.beSucceeded()) } -/// Batch-liquidate positions using MockDexSwapper as the repayment source in chunks of -/// chunkSize to stay within the computation limit. +// Batch-liquidate positions using MockDexSwapper as the repayment source in chunks of +// chunkSize to stay within the computation limit. access(all) fun batchLiquidateViaMockDex( pids: [UInt64], debtVaultIdentifier: String, @@ -226,13 +226,13 @@ access(all) fun batchLiquidateViaMockDex( } } -/// Test Multiple Positions Per User -/// -/// Validates requirements: -/// 1. User creates 5+ positions with different collateral types -/// 2. Each position has different health factors -/// 3. Operations on one position should not affect others (isolation) -/// +// Test Multiple Positions Per User +// +// Validates requirements: +// 1. User creates 5+ positions with different collateral types +// 2. Each position has different health factors +// 3. Operations on one position should not affect others (isolation) +// access(all) fun testMultiplePositionsPerUser() { safeReset() @@ -333,13 +333,13 @@ access(all) fun testMultiplePositionsPerUser() { Test.assert(healthsAfterBorrow[4] == healths[4], message: "Position 5 should be unchanged") } -/// Test Position Interactions Through Shared Liquidity Pools -/// -/// Validates that multiple positions interact through shared pool resources: -/// 1. Multiple positions compete for limited deposit capacity -/// 2. Position A's borrowing reduces available liquidity for Position B -/// 3. Shared liquidity pools create cross-position effects -/// 4. Pool capacity constraints affect all positions +// Test Position Interactions Through Shared Liquidity Pools +// +// Validates that multiple positions interact through shared pool resources: +// 1. Multiple positions compete for limited deposit capacity +// 2. Position A's borrowing reduces available liquidity for Position B +// 3. Shared liquidity pools create cross-position effects +// 4. Pool capacity constraints affect all positions access(all) fun testPositionInteractionsSharedLiquidity() { safeReset() @@ -468,12 +468,12 @@ access(all) fun testPositionInteractionsSharedLiquidity() { } -/// Test Batch Liquidations -/// -/// Validates batch liquidation capabilities: -/// 1. Multiple unhealthy positions liquidated in SINGLE transaction -/// 2. Partial liquidation of multiple positions -/// 3. Gas cost optimization through batch processing +// Test Batch Liquidations +// +// Validates batch liquidation capabilities: +// 1. Multiple unhealthy positions liquidated in SINGLE transaction +// 2. Partial liquidation of multiple positions +// 3. Gas cost optimization through batch processing access(all) fun testBatchLiquidations() { safeReset() @@ -674,50 +674,50 @@ access(all) fun testBatchLiquidations() { Test.assert(healthAfterFlow == healths[4], message: "FLOW position health should be unchanged") } -/// Test Mass Simultaneous Unhealthy Positions – 100-Position Multi-Collateral Stress Test -/// -/// System-wide stress test validating protocol behavior under mass position failure -/// across three collateral types — all crashing 40% simultaneously: -/// -/// 100 positions (all borrowing FLOW as debt): -/// Group A: 50 USDF positions (10 USDF each) — 25 high-risk + 25 moderate -/// Group B: 45 USDC positions (2 USDC each) — 23 high-risk + 22 moderate -/// Group C: 5 WBTC positions (0.00009 WBTC ea) — 5 uniform (same risk tier) -/// -/// Health before crash (CF_USDF=CF_USDC=0.85, CF_WBTC=0.75): -/// USDF high-risk: borrow 7.0 FLOW → (10×1.0×0.85)/7.0 = 1.214 -/// USDF moderate: borrow 6.0 FLOW → (10×1.0×0.85)/6.0 = 1.417 -/// USDC high-risk: borrow 1.4 FLOW → (2×1.0×0.85)/1.4 = 1.214 -/// USDC moderate: borrow 1.2 FLOW → (2×1.0×0.85)/1.2 = 1.417 -/// WBTC uniform: borrow 2.5 FLOW → (0.00009×50000×0.75)/2.5 = 1.350 -/// -/// All collateral crashes 40% simultaneously: -/// USDF: $1.00 → $0.60 | USDC: $1.00 → $0.60 | WBTC: $50000 → $30000 -/// -/// Health after crash: -/// USDF high: (10×0.60×0.85)/7.0 = 0.729 USDF mod: (10×0.60×0.85)/6.0 = 0.850 -/// USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 -/// WBTC: (0.00009×30000×0.75)/2.5 = 0.810 -/// -/// Liquidation (liquidationTargetHF=1.05, post target≈1.02–1.04): -/// USDF high: seize 4.0 USDF, repay 4.0 FLOW → post = (10-4)×0.6×0.85/(7-4) = 1.02 -/// DEX: 4.0 < 4.0/0.6 = 6.67 -/// USDF mod: seize 4.0 USDF, repay 3.0 FLOW → post = (10-4)×0.6×0.85/(6-3) = 1.02 -/// DEX: 4.0 < 3.0/0.6 = 5.00 -/// USDC high: seize 0.8 USDC, repay 0.8 FLOW → post = (2-0.8)×0.6×0.85/(1.4-0.8) = 1.02 -/// DEX: 0.8 < 0.8/0.6 = 1.33 -/// USDC mod: seize 0.8 USDC, repay 0.6 FLOW → post = (2-0.8)×0.6×0.85/(1.2-0.6) = 1.02 -/// DEX: 0.8 < 0.6/0.6 = 1.00 -/// WBTC: seize 0.00003 WBTC, repay 1.18 FLOW → post = (0.00006)×22500/(2.5-1.18) = 1.023 -/// DEX: 0.00003 < 1.18/30000 = 0.0000393 -/// -/// Batch order (worst health first): USDF-high (0.729) → USDC-high (0.729) → WBTC (0.810) → USDF-mod (0.850) → USDC-mod (0.850) -/// -/// Token budget (mainnet): -/// flowHolder (1921 FLOW): 450 LP + 230 DEX source = 680 FLOW total -/// usdfHolder (25000 USDF): 500 USDF for 50 positions -/// usdcHolder (97 USDC): 90 USDC for 45 positions -/// wbtcHolder (0.0005 WBTC): 0.00045 WBTC for 5 positions (holder has 0.00049998) +// Test Mass Simultaneous Unhealthy Positions – 100-Position Multi-Collateral Stress Test +// +// System-wide stress test validating protocol behavior under mass position failure +// across three collateral types — all crashing 40% simultaneously: +// +// 100 positions (all borrowing FLOW as debt): +// Group A: 50 USDF positions (10 USDF each) — 25 high-risk + 25 moderate +// Group B: 45 USDC positions (2 USDC each) — 23 high-risk + 22 moderate +// Group C: 5 WBTC positions (0.00009 WBTC ea) — 5 uniform (same risk tier) +// +// Health before crash (CF_USDF=CF_USDC=0.85, CF_WBTC=0.75): +// USDF high-risk: borrow 7.0 FLOW → (10×1.0×0.85)/7.0 = 1.214 +// USDF moderate: borrow 6.0 FLOW → (10×1.0×0.85)/6.0 = 1.417 +// USDC high-risk: borrow 1.4 FLOW → (2×1.0×0.85)/1.4 = 1.214 +// USDC moderate: borrow 1.2 FLOW → (2×1.0×0.85)/1.2 = 1.417 +// WBTC uniform: borrow 2.5 FLOW → (0.00009×50000×0.75)/2.5 = 1.350 +// +// All collateral crashes 40% simultaneously: +// USDF: $1.00 → $0.60 | USDC: $1.00 → $0.60 | WBTC: $50000 → $30000 +// +// Health after crash: +// USDF high: (10×0.60×0.85)/7.0 = 0.729 USDF mod: (10×0.60×0.85)/6.0 = 0.850 +// USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 +// WBTC: (0.00009×30000×0.75)/2.5 = 0.810 +// +// Liquidation (liquidationTargetHF=1.05, post target≈1.02–1.04): +// USDF high: seize 4.0 USDF, repay 4.0 FLOW → post = (10-4)×0.6×0.85/(7-4) = 1.02 +// DEX: 4.0 < 4.0/0.6 = 6.67 +// USDF mod: seize 4.0 USDF, repay 3.0 FLOW → post = (10-4)×0.6×0.85/(6-3) = 1.02 +// DEX: 4.0 < 3.0/0.6 = 5.00 +// USDC high: seize 0.8 USDC, repay 0.8 FLOW → post = (2-0.8)×0.6×0.85/(1.4-0.8) = 1.02 +// DEX: 0.8 < 0.8/0.6 = 1.33 +// USDC mod: seize 0.8 USDC, repay 0.6 FLOW → post = (2-0.8)×0.6×0.85/(1.2-0.6) = 1.02 +// DEX: 0.8 < 0.6/0.6 = 1.00 +// WBTC: seize 0.00003 WBTC, repay 1.18 FLOW → post = (0.00006)×22500/(2.5-1.18) = 1.023 +// DEX: 0.00003 < 1.18/30000 = 0.0000393 +// +// Batch order (worst health first): USDF-high (0.729) → USDC-high (0.729) → WBTC (0.810) → USDF-mod (0.850) → USDC-mod (0.850) +// +// Token budget (mainnet): +// flowHolder (1921 FLOW): 450 LP + 230 DEX source = 680 FLOW total +// usdfHolder (25000 USDF): 500 USDF for 50 positions +// usdcHolder (97 USDC): 90 USDC for 45 positions +// wbtcHolder (0.0005 WBTC): 0.00045 WBTC for 5 positions (holder has 0.00049998) access(all) fun testMassUnhealthyLiquidations() { safeReset() From 526704aa94ce2f0cb709e52c1a1f60e27e384b3a Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Fri, 20 Feb 2026 13:37:20 +0100 Subject: [PATCH 05/25] constant naming fix --- .../fork_multiple_positions_per_user.cdc | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user.cdc index 58df77dd..be8b8977 100644 --- a/cadence/tests/fork_multiple_positions_per_user.cdc +++ b/cadence/tests/fork_multiple_positions_per_user.cdc @@ -28,11 +28,11 @@ access(all) let WBTC_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_717dae2baf7 // pool admin resources are stored there. Note: this is the same address as wbtcHolder. access(all) let protocolAccount = Test.getAccount(0x47f544294e3b7656) -access(all) let usdfHolder = Test.getAccount(0xf18b50870aed46ad) // 25000 -access(all) let wethHolder = Test.getAccount(0xf62e3381a164f993) // 0.07032 -access(all) let wbtcHolder = Test.getAccount(0x47f544294e3b7656) // 0.0005 -access(all) let flowHolder = Test.getAccount(0xe467b9dd11fa00df) // 1921 -access(all) let usdcHolder = Test.getAccount(0xec6119051f7adc31) // 97 +access(all) let USDF_HOLDER = Test.getAccount(0xf18b50870aed46ad) // 25000 +access(all) let WETH_HOLDER = Test.getAccount(0xf62e3381a164f993) // 0.07032 +access(all) let WBTC_HOLDER = Test.getAccount(0x47f544294e3b7656) // 0.0005 +access(all) let FLOW_HOLDER = Test.getAccount(0xe467b9dd11fa00df) // 1921 +access(all) let USDC_HOLDER = Test.getAccount(0xec6119051f7adc31) // 97 access(all) var snapshot: UInt64 = 0 @@ -244,7 +244,7 @@ access(all) fun testMultiplePositionsPerUser() { // Transfer FLOW from holder to LP log("Setting up liquidity provider with FLOW\n") let liquidityAmount = 800.0 - transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") // LP deposits FLOW to create liquidity for borrowing createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) @@ -261,11 +261,11 @@ access(all) fun testMultiplePositionsPerUser() { // - wbtcHolder: 0.0005 WBTC x $50000 = $25 let positions = [ - {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 500.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": flowHolder}, - {"type": USDF_TOKEN_IDENTIFIER, "amount": 1500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": usdfHolder}, - {"type": USDC_TOKEN_IDENTIFIER, "amount": 10.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": usdcHolder}, - {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.05, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": wethHolder}, - {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": wbtcHolder} + {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 500.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": FLOW_HOLDER}, + {"type": USDF_TOKEN_IDENTIFIER, "amount": 1500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": USDF_HOLDER}, + {"type": USDC_TOKEN_IDENTIFIER, "amount": 10.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": USDC_HOLDER}, + {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.05, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": WETH_HOLDER}, + {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": WBTC_HOLDER} ] let debts = [100.0, 150.0, 5.0, 50.0, 8.0] @@ -351,7 +351,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log("Setting up shared liquidity pool with limited capacity\n") let liquidityAmount = 400.0 - transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") // LP deposits FLOW - this creates the shared liquidity pool createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) @@ -361,7 +361,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let userACollateral = 90.0 // 90 USDC log("Creating Position A with \(userACollateral) USDC collateral\n") - transferTokensFromHolder(holder: usdcHolder, recipient: user, amount: userACollateral, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") + transferTokensFromHolder(holder: USDC_HOLDER, recipient: user, amount: userACollateral, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") createPosition(admin: protocolAccount, signer: user, amount: userACollateral, vaultStoragePath: USDC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) var openEvts = Test.eventsOfType(Type()) let positionA_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid @@ -370,7 +370,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let userBCollateral = 500.0 // 500 USDF log("Creating Position B with \(userBCollateral) USDF collateral\n") - transferTokensFromHolder(holder: usdfHolder, recipient: user, amount: userBCollateral, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") + transferTokensFromHolder(holder: USDF_HOLDER, recipient: user, amount: userBCollateral, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") createPosition(admin: protocolAccount, signer: user, amount: userBCollateral, vaultStoragePath: USDF_VAULT_STORAGE_PATH, pushToDrawDownSink: false) openEvts = Test.eventsOfType(Type()) let positionB_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid @@ -484,7 +484,7 @@ access(all) fun testBatchLiquidations() { // LP deposits 600 FLOW to provide borrowing liquidity // (total borrows = 200+90+40+10+80 = 420 FLOW < 600) - transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: 600.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: 600.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") createPosition(admin: protocolAccount, signer: lpUser, amount: 600.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // 5 positions with distinct collateral types: @@ -500,11 +500,11 @@ access(all) fun testBatchLiquidations() { log("Creating 5 positions with different collateral types\n") let positions = [ - {"type": USDF_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": usdfHolder, "borrow": 200.0}, - {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.06, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": wethHolder, "borrow": 90.0}, - {"type": USDC_TOKEN_IDENTIFIER, "amount": 80.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": usdcHolder, "borrow": 40.0}, - {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": wbtcHolder, "borrow": 10.0}, - {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 200.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": flowHolder, "borrow": 80.0} + {"type": USDF_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": USDF_HOLDER, "borrow": 200.0}, + {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.06, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": WETH_HOLDER, "borrow": 90.0}, + {"type": USDC_TOKEN_IDENTIFIER, "amount": 80.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": USDC_HOLDER, "borrow": 40.0}, + {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": WBTC_HOLDER, "borrow": 10.0}, + {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 200.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": FLOW_HOLDER, "borrow": 80.0} ] var userPids: [UInt64] = [] @@ -566,7 +566,7 @@ access(all) fun testBatchLiquidations() { // Setup protocol account FLOW vault as the DEX output source. // priceRatio = Pc_crashed / Pd = post-crash collateral price / FLOW price. // This must match the oracle prices exactly to pass the DEX/oracle deviation check. - transferTokensFromHolder(holder: flowHolder, recipient: protocolAccount, amount: 300.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: FLOW_HOLDER, recipient: protocolAccount, amount: 300.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") log("\nSetting up DEX swappers (priceRatio = post-crash Pc / Pd)\n") setMockDexPriceForPair( @@ -608,11 +608,11 @@ access(all) fun testBatchLiquidations() { // USDC=12: partial; (80-17)*0.85*0.5 = 26.775 → repay=12 → postHealth=26.775/28≈0.956 log("\nSetting up liquidator account\n") let liquidator = Test.createAccount() - transferTokensFromHolder(holder: flowHolder, recipient: liquidator, amount: 250.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") - transferTokensFromHolder(holder: usdfHolder, recipient: liquidator, amount: 1.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") - transferTokensFromHolder(holder: wethHolder, recipient: liquidator, amount: 0.001, storagePath: WETH_VAULT_STORAGE_PATH, tokenName: "WETH") - transferTokensFromHolder(holder: usdcHolder, recipient: liquidator, amount: 1.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") - transferTokensFromHolder(holder: wbtcHolder, recipient: liquidator, amount: 0.00001, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") + transferTokensFromHolder(holder: FLOW_HOLDER, recipient: liquidator, amount: 250.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: USDF_HOLDER, recipient: liquidator, amount: 1.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") + transferTokensFromHolder(holder: WETH_HOLDER, recipient: liquidator, amount: 0.001, storagePath: WETH_VAULT_STORAGE_PATH, tokenName: "WETH") + transferTokensFromHolder(holder: USDC_HOLDER, recipient: liquidator, amount: 1.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") + transferTokensFromHolder(holder: WBTC_HOLDER, recipient: liquidator, amount: 0.00001, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") // seize/repay values satisfy three constraints: // 1. seize < quote.inAmount (offer beats DEX price) @@ -731,7 +731,7 @@ access(all) fun testMassUnhealthyLiquidations() { // LP deposits 450 FLOW — covers the ~397 FLOW of total borrows with headroom. log("LP depositing 450 FLOW to shared liquidity pool\n") - transferTokensFromHolder(holder: flowHolder, recipient: lpUser, amount: 450.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: 450.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") createPosition(admin: protocolAccount, signer: lpUser, amount: 450.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) //////////// Transfer collateral to user /////////////////// @@ -740,9 +740,9 @@ access(all) fun testMassUnhealthyLiquidations() { // Group B: 45 positions × 2 USDC = 90 USDC // Group C: 5 positions × 0.00009 WBTC = 0.00045 WBTC log("Transferring collateral: 500 USDF + 90 USDC + 0.00045 WBTC\n") - transferTokensFromHolder(holder: usdfHolder, recipient: user, amount: 500.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") - transferTokensFromHolder(holder: usdcHolder, recipient: user, amount: 90.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") - transferTokensFromHolder(holder: wbtcHolder, recipient: user, amount: 0.00045, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") + transferTokensFromHolder(holder: USDF_HOLDER, recipient: user, amount: 500.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") + transferTokensFromHolder(holder: USDC_HOLDER, recipient: user, amount: 90.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") + transferTokensFromHolder(holder: WBTC_HOLDER, recipient: user, amount: 0.00045, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") //////////// Create 100 positions /////////////////// @@ -865,7 +865,7 @@ access(all) fun testMassUnhealthyLiquidations() { // Total DEX FLOW: 25×4.0 + 25×3.0 + 23×0.8 + 22×0.6 + 5×1.18 // = 100 + 75 + 18.4 + 13.2 + 5.90 = 212.50; transfer 230 for headroom log("Configuring DEX pairs: USDF→FLOW, USDC→FLOW, WBTC→FLOW\n") - transferTokensFromHolder(holder: flowHolder, recipient: protocolAccount, amount: 230.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensFromHolder(holder: FLOW_HOLDER, recipient: protocolAccount, amount: 230.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") setMockDexPriceForPair( signer: protocolAccount, inVaultIdentifier: USDF_TOKEN_IDENTIFIER, From 13c50e5b6686506c65704967bd785e5af57fab4c Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Tue, 24 Feb 2026 16:13:00 +0100 Subject: [PATCH 06/25] moved helper methods to test_helpers.cdc --- .../fork_multiple_positions_per_user.cdc | 268 ++++++------------ cadence/tests/test_helpers.cdc | 84 ++++++ 2 files changed, 176 insertions(+), 176 deletions(-) diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user.cdc index be8b8977..788f17f4 100644 --- a/cadence/tests/fork_multiple_positions_per_user.cdc +++ b/cadence/tests/fork_multiple_positions_per_user.cdc @@ -9,31 +9,11 @@ import "MOET" import "FlowALPv0" import "test_helpers.cdc" -// Real mainnet token identifiers (overriding test_helpers for mainnet) -access(all) let FLOW_TOKEN_IDENTIFIER_MAINNET = "A.1654653399040a61.FlowToken.Vault" -access(all) let USDC_TOKEN_IDENTIFIER = "A.f1ab99c82dee3526.USDCFlow.Vault" -access(all) let USDF_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault" -access(all) let WETH_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" -access(all) let WBTC_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" -access(all) let MOET_TOKEN_IDENTIFIER_MAINNET = "A.6b00ff876c299c61.MOET.Vault" - -// Storage paths for different token types -access(all) let USDC_VAULT_STORAGE_PATH = /storage/usdcFlowVault -access(all) let USDF_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault -access(all) let WETH_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Vault -access(all) let WBTC_VAULT_STORAGE_PATH = /storage/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Vault - // Protocol account: in fork mode, Test.deployContract() deploys to the contract's mainnet // alias address. FlowALPv0's mainnet alias is 0x47f544294e3b7656, so PoolFactory and all // pool admin resources are stored there. Note: this is the same address as wbtcHolder. access(all) let protocolAccount = Test.getAccount(0x47f544294e3b7656) -access(all) let USDF_HOLDER = Test.getAccount(0xf18b50870aed46ad) // 25000 -access(all) let WETH_HOLDER = Test.getAccount(0xf62e3381a164f993) // 0.07032 -access(all) let WBTC_HOLDER = Test.getAccount(0x47f544294e3b7656) // 0.0005 -access(all) let FLOW_HOLDER = Test.getAccount(0xe467b9dd11fa00df) // 1921 -access(all) let USDC_HOLDER = Test.getAccount(0xec6119051f7adc31) // 97 - access(all) var snapshot: UInt64 = 0 access(all) @@ -73,7 +53,7 @@ access(all) fun setup() { err = Test.deployContract( name: "MockOracle", path: "../contracts/mocks/MockOracle.cdc", - arguments: [MOET_TOKEN_IDENTIFIER_MAINNET] + arguments: [MAINNET_MOET_TOKEN_IDENTIFIER] ) Test.expect(err, Test.beNil()) @@ -100,20 +80,20 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER_MAINNET, beFailed: false) + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MAINNET_MOET_TOKEN_IDENTIFIER, beFailed: false) // Setup pool with real mainnet token prices - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDF_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WETH_TOKEN_IDENTIFIER, price: 3500.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WBTC_TOKEN_IDENTIFIER, price: 50000.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MOET_TOKEN_IDENTIFIER_MAINNET, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, price: 3500.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 50000.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_MOET_TOKEN_IDENTIFIER, price: 1.0) // Add multiple token types as supported collateral (FLOW, USDC, USDF, WETH, WBTC) addSupportedTokenZeroRateCurve( signer: protocolAccount, - tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, collateralFactor: 0.8, borrowFactor: 1.0, depositRate: 1_000_000.0, @@ -122,7 +102,7 @@ access(all) fun setup() { addSupportedTokenZeroRateCurve( signer: protocolAccount, - tokenTypeIdentifier: USDC_TOKEN_IDENTIFIER, + tokenTypeIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, collateralFactor: 0.85, borrowFactor: 1.0, depositRate: 1_000_000.0, @@ -131,7 +111,7 @@ access(all) fun setup() { addSupportedTokenZeroRateCurve( signer: protocolAccount, - tokenTypeIdentifier: USDF_TOKEN_IDENTIFIER, + tokenTypeIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, collateralFactor: 0.85, borrowFactor: 1.0, depositRate: 1_000_000.0, @@ -140,7 +120,7 @@ access(all) fun setup() { addSupportedTokenZeroRateCurve( signer: protocolAccount, - tokenTypeIdentifier: WETH_TOKEN_IDENTIFIER, + tokenTypeIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, collateralFactor: 0.75, borrowFactor: 1.0, depositRate: 1_000_000.0, @@ -148,84 +128,22 @@ access(all) fun setup() { ) // Set minimum deposit for WETH to 0.01 (since holder only has 0.07032) - setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: WETH_TOKEN_IDENTIFIER, minimum: 0.01) + setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, minimum: 0.01) addSupportedTokenZeroRateCurve( signer: protocolAccount, - tokenTypeIdentifier: WBTC_TOKEN_IDENTIFIER, + tokenTypeIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, collateralFactor: 0.75, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) // Set minimum deposit for WBTC to 0.00001 (since holder only has 0.0005) - setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: WBTC_TOKEN_IDENTIFIER, minimum: 0.00001) + setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, minimum: 0.00001) snapshot = getCurrentBlockHeight() } -// Transfer tokens from holder to recipient (creates vault for recipient if needed) -access(all) fun transferTokensFromHolder(holder: Test.TestAccount, recipient: Test.TestAccount, amount: UFix64, storagePath: StoragePath, tokenName: String) { - let tx = Test.Transaction( - code: Test.readFile("../transactions/test/transfer_tokens_with_setup.cdc"), - authorizers: [holder.address, recipient.address], - signers: [holder, recipient], - arguments: [amount, storagePath] - ) - let result = Test.executeTransaction(tx) - Test.expect(result, Test.beSucceeded()) -} - -// Batch-liquidate positions using the liquidator's own tokens as repayment (no DEX). -// The liquidator must hold sufficient debt tokens upfront. -access(all) fun batchManualLiquidation( - pids: [UInt64], - debtVaultIdentifier: String, - seizeVaultIdentifiers: [String], - seizeAmounts: [UFix64], - repayAmounts: [UFix64], - signer: Test.TestAccount -) { - let res = _executeTransaction( - "../transactions/flow-alp/pool-management/batch_manual_liquidation.cdc", - [pids, debtVaultIdentifier, seizeVaultIdentifiers, seizeAmounts, repayAmounts], - signer - ) - Test.expect(res, Test.beSucceeded()) -} - -// Batch-liquidate positions using MockDexSwapper as the repayment source in chunks of -// chunkSize to stay within the computation limit. -access(all) fun batchLiquidateViaMockDex( - pids: [UInt64], - debtVaultIdentifier: String, - seizeVaultIdentifiers: [String], - seizeAmounts: [UFix64], - repayAmounts: [UFix64], - chunkSize: Int, - signer: Test.TestAccount -) { - let total = pids.length - let numChunks = (total + chunkSize - 1) / chunkSize - for i in InclusiveRange(0, numChunks - 1) { - let startIdx = i * chunkSize - var endIdx = startIdx + chunkSize - if endIdx > total { - endIdx = total - } - let res = _executeTransaction( - "../transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc", - [pids.slice(from: startIdx, upTo: endIdx), - debtVaultIdentifier, - seizeVaultIdentifiers.slice(from: startIdx, upTo: endIdx), - seizeAmounts.slice(from: startIdx, upTo: endIdx), - repayAmounts.slice(from: startIdx, upTo: endIdx)], - signer - ) - Test.expect(res, Test.beSucceeded()) - } -} - // Test Multiple Positions Per User // // Validates requirements: @@ -244,7 +162,7 @@ access(all) fun testMultiplePositionsPerUser() { // Transfer FLOW from holder to LP log("Setting up liquidity provider with FLOW\n") let liquidityAmount = 800.0 - transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: liquidityAmount) // LP deposits FLOW to create liquidity for borrowing createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) @@ -261,11 +179,11 @@ access(all) fun testMultiplePositionsPerUser() { // - wbtcHolder: 0.0005 WBTC x $50000 = $25 let positions = [ - {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 500.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": FLOW_HOLDER}, - {"type": USDF_TOKEN_IDENTIFIER, "amount": 1500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": USDF_HOLDER}, - {"type": USDC_TOKEN_IDENTIFIER, "amount": 10.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": USDC_HOLDER}, - {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.05, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": WETH_HOLDER}, - {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": WBTC_HOLDER} + {"type": MAINNET_FLOW_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": MAINNET_FLOW_HOLDER}, + {"type": MAINNET_USDF_TOKEN_IDENTIFIER, "amount": 1500.0, "storagePath": MAINNET_USDF_STORAGE_PATH, "name": "USDF", "holder": MAINNET_USDF_HOLDER}, + {"type": MAINNET_USDC_TOKEN_IDENTIFIER, "amount": 10.0, "storagePath": MAINNET_USDC_STORAGE_PATH, "name": "USDC", "holder": MAINNET_USDC_HOLDER}, + {"type": MAINNET_WETH_TOKEN_IDENTIFIER, "amount": 0.05, "storagePath": MAINNET_WETH_STORAGE_PATH, "name": "WETH", "holder": MAINNET_WETH_HOLDER}, + {"type": MAINNET_WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": MAINNET_WBTC_STORAGE_PATH, "name": "WBTC", "holder": MAINNET_WBTC_HOLDER} ] let debts = [100.0, 150.0, 5.0, 50.0, 8.0] @@ -280,7 +198,7 @@ access(all) fun testMultiplePositionsPerUser() { let holder = position["holder"]! as! Test.TestAccount // Transfer tokens from holder to user - transferTokensFromHolder(holder: holder, recipient: user, amount: collateralAmount, storagePath: storagePath, tokenName: collateralName) + transferTokensWithSetup(tokenIdentifier: collateralType, from: holder, to: user, amount: collateralAmount) createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) @@ -298,7 +216,7 @@ access(all) fun testMultiplePositionsPerUser() { var healths: [UFix128] = [] for i, debt in debts { let pid = userPids[i] - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: debt, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: debt, beFailed: false) // Get health factor let health = getPositionHealth(pid: pid, beFailed: false) @@ -316,7 +234,7 @@ access(all) fun testMultiplePositionsPerUser() { log("Testing isolation by borrowing more from Position \(isolationTestPid)\n") log("\n Action: Borrow 100 more FLOW from Position \(isolationTestPid)\n") - borrowFromPosition(signer: user, positionId: isolationTestPid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: additionalDebt, beFailed: false) + borrowFromPosition(signer: user, positionId: isolationTestPid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: additionalDebt, beFailed: false) // Get health of all positions after var healthsAfterBorrow: [UFix128] = [] @@ -351,7 +269,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log("Setting up shared liquidity pool with limited capacity\n") let liquidityAmount = 400.0 - transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: liquidityAmount, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: liquidityAmount) // LP deposits FLOW - this creates the shared liquidity pool createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) @@ -361,8 +279,8 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let userACollateral = 90.0 // 90 USDC log("Creating Position A with \(userACollateral) USDC collateral\n") - transferTokensFromHolder(holder: USDC_HOLDER, recipient: user, amount: userACollateral, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") - createPosition(admin: protocolAccount, signer: user, amount: userACollateral, vaultStoragePath: USDC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + transferTokensWithSetup(tokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, from: MAINNET_USDC_HOLDER, to: user, amount: userACollateral) + createPosition(admin: protocolAccount, signer: user, amount: userACollateral, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) var openEvts = Test.eventsOfType(Type()) let positionA_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid @@ -370,8 +288,8 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let userBCollateral = 500.0 // 500 USDF log("Creating Position B with \(userBCollateral) USDF collateral\n") - transferTokensFromHolder(holder: USDF_HOLDER, recipient: user, amount: userBCollateral, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") - createPosition(admin: protocolAccount, signer: user, amount: userBCollateral, vaultStoragePath: USDF_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + transferTokensWithSetup(tokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, from: MAINNET_USDF_HOLDER, to: user, amount: userBCollateral) + createPosition(admin: protocolAccount, signer: user, amount: userBCollateral, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) openEvts = Test.eventsOfType(Type()) let positionB_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid @@ -382,7 +300,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { // Max Borrow = 76.50 / 1.1 (minHealth) = 69.55 FLOW // Health after borrow = 76.50 / 60 = 1.275 let positionA_borrow1 = 60.0 // Borrow 60 FLOW (within max 69.55) - borrowFromPosition(signer: user, positionId: positionA_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionA_borrow1, beFailed: false) + borrowFromPosition(signer: user, positionId: positionA_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionA_borrow1, beFailed: false) let healthA_after1 = getPositionHealth(pid: positionA_id, beFailed: false) log(" Position A borrowed \(positionA_borrow1) FLOW - Health: \(healthA_after1)\n") @@ -397,7 +315,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { // Max Borrow = 425.00 / 1.1 (minHealth) = 386.36 FLOW let positionB_borrow1 = 340.0 // Borrow 340 FLOW (within max 386.36 borrow and 340 remaining liquidity) log(" Attempting to borrow \(positionB_borrow1) FLOW...") - borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow1, beFailed: false) + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow1, beFailed: false) log(" Success - Position B borrowed \(positionB_borrow1) FLOW") let healthB_after1 = getPositionHealth(pid: positionB_id, beFailed: false) log(" Position B Health: \(healthB_after1)\n") @@ -409,7 +327,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { // Position B can't borrow more because remaining liquidity is 0 let positionB_borrow2_attempt = 1.0 log(" Attempting to borrow \(positionB_borrow2_attempt) FLOW...") - borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow2_attempt, beFailed: true) + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow2_attempt, beFailed: true) log(" Failed as expected - remaining liquidity is 0\n") let healthB_after2 = getPositionHealth(pid: positionB_id, beFailed: false) @@ -440,7 +358,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log(" Position B health: \(healthB_before_priceChange)") // Crash USDC price (Position A's collateral) from $1.0 to $0.5 - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 0.5) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.5) let healthA_after_crash = getPositionHealth(pid: positionA_id, beFailed: false) log(" Position A health after price crash: \(healthA_after_crash)\n") @@ -459,7 +377,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { // Position B has: 425 effective collateral, 340 borrowed, can borrow up to 46.36 more let positionB_borrow3 = 30.0 // Well within remaining capacity (40 FLOW available, 46.36 max allowed) log(" Position B attempts to borrow \(positionB_borrow3) FLOW after Position A's health deterioration...") - borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow3, beFailed: false) + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow3, beFailed: false) log(" Success - Position B can still borrow despite Position A's poor health\n") let healthB_final = getPositionHealth(pid: positionB_id, beFailed: false) @@ -484,7 +402,7 @@ access(all) fun testBatchLiquidations() { // LP deposits 600 FLOW to provide borrowing liquidity // (total borrows = 200+90+40+10+80 = 420 FLOW < 600) - transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: 600.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: 600.0) createPosition(admin: protocolAccount, signer: lpUser, amount: 600.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // 5 positions with distinct collateral types: @@ -500,22 +418,23 @@ access(all) fun testBatchLiquidations() { log("Creating 5 positions with different collateral types\n") let positions = [ - {"type": USDF_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": USDF_VAULT_STORAGE_PATH, "name": "USDF", "holder": USDF_HOLDER, "borrow": 200.0}, - {"type": WETH_TOKEN_IDENTIFIER, "amount": 0.06, "storagePath": WETH_VAULT_STORAGE_PATH, "name": "WETH", "holder": WETH_HOLDER, "borrow": 90.0}, - {"type": USDC_TOKEN_IDENTIFIER, "amount": 80.0, "storagePath": USDC_VAULT_STORAGE_PATH, "name": "USDC", "holder": USDC_HOLDER, "borrow": 40.0}, - {"type": WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": WBTC_VAULT_STORAGE_PATH, "name": "WBTC", "holder": WBTC_HOLDER, "borrow": 10.0}, - {"type": FLOW_TOKEN_IDENTIFIER_MAINNET, "amount": 200.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": FLOW_HOLDER, "borrow": 80.0} + {"type": MAINNET_USDF_TOKEN_IDENTIFIER, "amount": 500.0, "storagePath": MAINNET_USDF_STORAGE_PATH, "name": "USDF", "holder": MAINNET_USDF_HOLDER, "borrow": 200.0}, + {"type": MAINNET_WETH_TOKEN_IDENTIFIER, "amount": 0.06, "storagePath": MAINNET_WETH_STORAGE_PATH, "name": "WETH", "holder": MAINNET_WETH_HOLDER, "borrow": 90.0}, + {"type": MAINNET_USDC_TOKEN_IDENTIFIER, "amount": 80.0, "storagePath": MAINNET_USDC_STORAGE_PATH, "name": "USDC", "holder": MAINNET_USDC_HOLDER, "borrow": 40.0}, + {"type": MAINNET_WBTC_TOKEN_IDENTIFIER, "amount": 0.0004, "storagePath": MAINNET_WBTC_STORAGE_PATH, "name": "WBTC", "holder": MAINNET_WBTC_HOLDER, "borrow": 10.0}, + {"type": MAINNET_FLOW_TOKEN_IDENTIFIER, "amount": 200.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": MAINNET_FLOW_HOLDER, "borrow": 80.0} ] var userPids: [UInt64] = [] for i, position in positions { + let collateralType = position["type"]! as! String let collateralName = position["name"]! as! String let collateralAmount = position["amount"]! as! UFix64 let storagePath = position["storagePath"]! as! StoragePath let holder = position["holder"]! as! Test.TestAccount - transferTokensFromHolder(holder: holder, recipient: user, amount: collateralAmount, storagePath: storagePath, tokenName: collateralName) + transferTokensWithSetup(tokenIdentifier: collateralType, from: holder, to: user, amount: collateralAmount) createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) userPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) @@ -528,7 +447,7 @@ access(all) fun testBatchLiquidations() { let borrowAmount = position["borrow"]! as! UFix64 let collateralName = position["name"]! as! String - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: borrowAmount, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: borrowAmount, beFailed: false) let health = getPositionHealth(pid: pid, beFailed: false) healths.append(health) @@ -537,10 +456,10 @@ access(all) fun testBatchLiquidations() { // Crash collateral prices. FLOW stays at $1.0 so userPids[4] stays healthy. log("\nCrashing collateral prices to trigger liquidations\n") - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDF_TOKEN_IDENTIFIER, price: 0.3) // -70% - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WETH_TOKEN_IDENTIFIER, price: 1050.0) // -70% - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 0.5) // -50% - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WBTC_TOKEN_IDENTIFIER, price: 25000.0) // -50% + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 0.3) // -70% + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, price: 1050.0) // -70% + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.5) // -50% + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 25000.0) // -50% log("\nPosition health after price crash:\n") for i in InclusiveRange(0, 4) { @@ -566,34 +485,34 @@ access(all) fun testBatchLiquidations() { // Setup protocol account FLOW vault as the DEX output source. // priceRatio = Pc_crashed / Pd = post-crash collateral price / FLOW price. // This must match the oracle prices exactly to pass the DEX/oracle deviation check. - transferTokensFromHolder(holder: FLOW_HOLDER, recipient: protocolAccount, amount: 300.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: protocolAccount, amount: 300.0) log("\nSetting up DEX swappers (priceRatio = post-crash Pc / Pd)\n") setMockDexPriceForPair( signer: protocolAccount, - inVaultIdentifier: USDF_TOKEN_IDENTIFIER, - outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + inVaultIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, + outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.3 // $0.30 USDF / $1.00 FLOW ) setMockDexPriceForPair( signer: protocolAccount, - inVaultIdentifier: WETH_TOKEN_IDENTIFIER, - outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + inVaultIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, + outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 1050.0 // $1050 WETH / $1.00 FLOW ) setMockDexPriceForPair( signer: protocolAccount, - inVaultIdentifier: USDC_TOKEN_IDENTIFIER, - outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + inVaultIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, + outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.5 // $0.50 USDC / $1.00 FLOW ) setMockDexPriceForPair( signer: protocolAccount, - inVaultIdentifier: WBTC_TOKEN_IDENTIFIER, - outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + inVaultIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, + outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 25000.0 // $25000 WBTC / $1.00 FLOW ) @@ -608,11 +527,11 @@ access(all) fun testBatchLiquidations() { // USDC=12: partial; (80-17)*0.85*0.5 = 26.775 → repay=12 → postHealth=26.775/28≈0.956 log("\nSetting up liquidator account\n") let liquidator = Test.createAccount() - transferTokensFromHolder(holder: FLOW_HOLDER, recipient: liquidator, amount: 250.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") - transferTokensFromHolder(holder: USDF_HOLDER, recipient: liquidator, amount: 1.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") - transferTokensFromHolder(holder: WETH_HOLDER, recipient: liquidator, amount: 0.001, storagePath: WETH_VAULT_STORAGE_PATH, tokenName: "WETH") - transferTokensFromHolder(holder: USDC_HOLDER, recipient: liquidator, amount: 1.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") - transferTokensFromHolder(holder: WBTC_HOLDER, recipient: liquidator, amount: 0.00001, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: liquidator, amount: 250.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, from: MAINNET_USDF_HOLDER, to: liquidator, amount: 1.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, from: MAINNET_WETH_HOLDER, to: liquidator, amount: 0.001) + transferTokensWithSetup(tokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, from: MAINNET_USDC_HOLDER, to: liquidator, amount: 1.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, from: MAINNET_WBTC_HOLDER, to: liquidator, amount: 0.00001) // seize/repay values satisfy three constraints: // 1. seize < quote.inAmount (offer beats DEX price) @@ -637,13 +556,13 @@ access(all) fun testBatchLiquidations() { log("\nExecuting batch liquidation of 4 positions (2 full, 2 partial) in SINGLE transaction...\n") let batchPids = [userPids[0], userPids[1], userPids[2], userPids[3] ] - let batchSeizeTypes = [USDF_TOKEN_IDENTIFIER, WETH_TOKEN_IDENTIFIER, USDC_TOKEN_IDENTIFIER, WBTC_TOKEN_IDENTIFIER] + let batchSeizeTypes = [MAINNET_USDF_TOKEN_IDENTIFIER, MAINNET_WETH_TOKEN_IDENTIFIER, MAINNET_USDC_TOKEN_IDENTIFIER, MAINNET_WBTC_TOKEN_IDENTIFIER] let batchSeizeAmounts = [147.0, 0.035, 17.0, 0.00011 ] let batchRepayAmounts = [113.0, 71.0, 12.0, 4.0 ] batchManualLiquidation( pids: batchPids, - debtVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + debtVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, seizeVaultIdentifiers: batchSeizeTypes, seizeAmounts: batchSeizeAmounts, repayAmounts: batchRepayAmounts, @@ -710,9 +629,6 @@ access(all) fun testBatchLiquidations() { // DEX: 0.8 < 0.6/0.6 = 1.00 // WBTC: seize 0.00003 WBTC, repay 1.18 FLOW → post = (0.00006)×22500/(2.5-1.18) = 1.023 // DEX: 0.00003 < 1.18/30000 = 0.0000393 -// -// Batch order (worst health first): USDF-high (0.729) → USDC-high (0.729) → WBTC (0.810) → USDF-mod (0.850) → USDC-mod (0.850) -// // Token budget (mainnet): // flowHolder (1921 FLOW): 450 LP + 230 DEX source = 680 FLOW total // usdfHolder (25000 USDF): 500 USDF for 50 positions @@ -731,7 +647,7 @@ access(all) fun testMassUnhealthyLiquidations() { // LP deposits 450 FLOW — covers the ~397 FLOW of total borrows with headroom. log("LP depositing 450 FLOW to shared liquidity pool\n") - transferTokensFromHolder(holder: FLOW_HOLDER, recipient: lpUser, amount: 450.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: 450.0) createPosition(admin: protocolAccount, signer: lpUser, amount: 450.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) //////////// Transfer collateral to user /////////////////// @@ -740,9 +656,9 @@ access(all) fun testMassUnhealthyLiquidations() { // Group B: 45 positions × 2 USDC = 90 USDC // Group C: 5 positions × 0.00009 WBTC = 0.00045 WBTC log("Transferring collateral: 500 USDF + 90 USDC + 0.00045 WBTC\n") - transferTokensFromHolder(holder: USDF_HOLDER, recipient: user, amount: 500.0, storagePath: USDF_VAULT_STORAGE_PATH, tokenName: "USDF") - transferTokensFromHolder(holder: USDC_HOLDER, recipient: user, amount: 90.0, storagePath: USDC_VAULT_STORAGE_PATH, tokenName: "USDC") - transferTokensFromHolder(holder: WBTC_HOLDER, recipient: user, amount: 0.00045, storagePath: WBTC_VAULT_STORAGE_PATH, tokenName: "WBTC") + transferTokensWithSetup(tokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, from: MAINNET_USDF_HOLDER, to: user, amount: 500.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, from: MAINNET_USDC_HOLDER, to: user, amount: 90.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, from: MAINNET_WBTC_HOLDER, to: user, amount: 0.00045) //////////// Create 100 positions /////////////////// @@ -751,7 +667,7 @@ access(all) fun testMassUnhealthyLiquidations() { // Group A — 50 USDF positions log("Creating 50 USDF positions (10 USDF each)...\n") for i in InclusiveRange(0, 49) { - createPosition(admin: protocolAccount, signer: user, amount: 10.0, vaultStoragePath: USDF_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: protocolAccount, signer: user, amount: 10.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } @@ -759,7 +675,7 @@ access(all) fun testMassUnhealthyLiquidations() { // Group B — 45 USDC positions log("Creating 45 USDC positions (2 USDC each)...\n") for i in InclusiveRange(50, 94) { - createPosition(admin: protocolAccount, signer: user, amount: 2.0, vaultStoragePath: USDC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: protocolAccount, signer: user, amount: 2.0, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } @@ -767,7 +683,7 @@ access(all) fun testMassUnhealthyLiquidations() { // Group C — 5 WBTC positions log("Creating 5 WBTC positions (0.00009 WBTC each)...\n") for i in InclusiveRange(95, 99) { - createPosition(admin: protocolAccount, signer: user, amount: 0.00009, vaultStoragePath: WBTC_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: protocolAccount, signer: user, amount: 0.00009, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } @@ -781,10 +697,10 @@ access(all) fun testMassUnhealthyLiquidations() { // moderate [25..49]: borrow 6.0 FLOW → health = (10×1.0×0.85)/6.0 = 1.417 log("Borrowing FLOW from 50 USDF positions...\n") for i in InclusiveRange(0, 24) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 7.0, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 7.0, beFailed: false) } for i in InclusiveRange(25, 49) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 6.0, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 6.0, beFailed: false) } // Group B — USDC positions: @@ -792,17 +708,17 @@ access(all) fun testMassUnhealthyLiquidations() { // moderate [73..94]: borrow 1.2 FLOW → health = (2×1.0×0.85)/1.2 = 1.417 log("Borrowing FLOW from 45 USDC positions...\n") for i in InclusiveRange(50, 72) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.4, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.4, beFailed: false) } for i in InclusiveRange(73, 94) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.2, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.2, beFailed: false) } // Group C — WBTC positions: // uniform [95..99]: borrow 2.5 FLOW → health = (0.00009×50000×0.75)/2.5 = 1.350 log("Borrowing FLOW from 5 WBTC positions...\n") for i in InclusiveRange(95, 99) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 2.5, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 2.5, beFailed: false) } // Confirm all 100 positions are healthy before the crash @@ -820,9 +736,9 @@ access(all) fun testMassUnhealthyLiquidations() { // USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 // WBTC: (0.00009×30000×0.75)/2.5 = 0.810 log("All three collateral types crash 40% simultaneously\n") - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDF_TOKEN_IDENTIFIER, price: 0.6) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: USDC_TOKEN_IDENTIFIER, price: 0.6) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: WBTC_TOKEN_IDENTIFIER, price: 30000.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 0.6) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.6) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 30000.0) // Capture post-crash health by token type and verify all positions are unhealthy var usdfHealths: [UFix128] = [] @@ -865,25 +781,25 @@ access(all) fun testMassUnhealthyLiquidations() { // Total DEX FLOW: 25×4.0 + 25×3.0 + 23×0.8 + 22×0.6 + 5×1.18 // = 100 + 75 + 18.4 + 13.2 + 5.90 = 212.50; transfer 230 for headroom log("Configuring DEX pairs: USDF→FLOW, USDC→FLOW, WBTC→FLOW\n") - transferTokensFromHolder(holder: FLOW_HOLDER, recipient: protocolAccount, amount: 230.0, storagePath: FLOW_VAULT_STORAGE_PATH, tokenName: "FLOW") + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: protocolAccount, amount: 230.0) setMockDexPriceForPair( signer: protocolAccount, - inVaultIdentifier: USDF_TOKEN_IDENTIFIER, - outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + inVaultIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, + outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.6 // $0.60 USDF / $1.00 FLOW ) setMockDexPriceForPair( signer: protocolAccount, - inVaultIdentifier: USDC_TOKEN_IDENTIFIER, - outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + inVaultIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, + outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.6 // $0.60 USDC / $1.00 FLOW ) setMockDexPriceForPair( signer: protocolAccount, - inVaultIdentifier: WBTC_TOKEN_IDENTIFIER, - outVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + inVaultIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, + outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 30000.0 // $30000 WBTC / $1.00 FLOW ) @@ -904,35 +820,35 @@ access(all) fun testMassUnhealthyLiquidations() { // USDF high-risk [0..24] for i in InclusiveRange(0, 24) { batchPids.append(allPids[i]) - batchSeize.append(USDF_TOKEN_IDENTIFIER) + batchSeize.append(MAINNET_USDF_TOKEN_IDENTIFIER) batchAmounts.append(4.0) batchRepay.append(4.0) } // USDC high-risk [50..72] for i in InclusiveRange(50, 72) { batchPids.append(allPids[i]) - batchSeize.append(USDC_TOKEN_IDENTIFIER) + batchSeize.append(MAINNET_USDC_TOKEN_IDENTIFIER) batchAmounts.append(0.8) batchRepay.append(0.8) } // WBTC uniform [95..99] for i in InclusiveRange(95, 99) { batchPids.append(allPids[i]) - batchSeize.append(WBTC_TOKEN_IDENTIFIER) + batchSeize.append(MAINNET_WBTC_TOKEN_IDENTIFIER) batchAmounts.append(0.00003) batchRepay.append(1.18) } // USDF moderate [25..49] for i in InclusiveRange(25, 49) { batchPids.append(allPids[i]) - batchSeize.append(USDF_TOKEN_IDENTIFIER) + batchSeize.append(MAINNET_USDF_TOKEN_IDENTIFIER) batchAmounts.append(4.0) batchRepay.append(3.0) } // USDC moderate [73..94] for i in InclusiveRange(73, 94) { batchPids.append(allPids[i]) - batchSeize.append(USDC_TOKEN_IDENTIFIER) + batchSeize.append(MAINNET_USDC_TOKEN_IDENTIFIER) batchAmounts.append(0.8) batchRepay.append(0.6) } @@ -946,7 +862,7 @@ access(all) fun testMassUnhealthyLiquidations() { log("Liquidating all 100 positions via DEX in chunks of 10...\n") batchLiquidateViaMockDex( pids: batchPids, - debtVaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET, + debtVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, seizeVaultIdentifiers: batchSeize, seizeAmounts: batchAmounts, repayAmounts: batchRepay, @@ -981,7 +897,7 @@ access(all) fun testMassUnhealthyLiquidations() { } // Protocol solvency: FLOW reserve must remain positive after mass liquidation - let reserveBalance = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER_MAINNET) + let reserveBalance = getReserveBalance(vaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER) log("Protocol FLOW reserve after mass liquidation: \(reserveBalance)\n") Test.assert(reserveBalance > 0.0, message: "Protocol must remain solvent (positive FLOW reserve) after mass liquidation") } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 6e10c494..9e62a534 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -31,6 +31,31 @@ access(all) let TEN_DAYS: Fix64 = 864_000.0 access(all) let THIRTY_DAYS: Fix64 = 2_592_000.0 // 30 * 86400 access(all) let ONE_YEAR: Fix64 = 31_557_600.0 // 365.25 * 86400 +// Mainnet constants +// EVM Bridged Token Identifiers +access(all) let MAINNET_MOET_TOKEN_ID = "A.6b00ff876c299c61.MOET.Vault" +access(all) let MAINNET_FLOW_TOKEN_ID = "A.1654653399040a61.FlowToken.Vault" + +// Real mainnet token identifiers +access(all) let MAINNET_FLOW_TOKEN_IDENTIFIER = "A.1654653399040a61.FlowToken.Vault" +access(all) let MAINNET_USDC_TOKEN_IDENTIFIER = "A.f1ab99c82dee3526.USDCFlow.Vault" +access(all) let MAINNET_USDF_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault" +access(all) let MAINNET_WETH_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" +access(all) let MAINNET_WBTC_TOKEN_IDENTIFIER = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" +access(all) let MAINNET_MOET_TOKEN_IDENTIFIER = "A.6b00ff876c299c61.MOET.Vault" + +// Storage paths for different token types +access(all) let MAINNET_USDC_STORAGE_PATH = /storage/usdcFlowVault +access(all) let MAINNET_USDF_STORAGE_PATH = /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault +access(all) let MAINNET_WETH_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Vault +access(all) let MAINNET_WBTC_STORAGE_PATH = /storage/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Vault + +// Biggest token holders +access(all) let MAINNET_USDF_HOLDER = Test.getAccount(0xf18b50870aed46ad) // 25000 +access(all) let MAINNET_WETH_HOLDER = Test.getAccount(0xf62e3381a164f993) // 0.07032 +access(all) let MAINNET_WBTC_HOLDER = Test.getAccount(0x47f544294e3b7656) // 0.0005 +access(all) let MAINNET_FLOW_HOLDER = Test.getAccount(0xe467b9dd11fa00df) // 1921 +access(all) let MAINNET_USDC_HOLDER = Test.getAccount(0xec6119051f7adc31) // 97 /* --- Test execution helpers --- */ @@ -812,6 +837,65 @@ fun transferFungibleTokens( Test.expect(res, Test.beSucceeded()) } +/// Sets up the recipient's vault (if not already present) and transfers tokens in one call. +/// Combines setupGenericVault + transferFungibleTokens for the common case of funding a fresh account. +access(all) +fun transferTokensWithSetup(tokenIdentifier: String, from: Test.TestAccount, to: Test.TestAccount, amount: UFix64) { + let res = setupGenericVault(to, vaultIdentifier: tokenIdentifier) + Test.expect(res, Test.beSucceeded()) + transferFungibleTokens(tokenIdentifier: tokenIdentifier, from: from, to: to, amount: amount) +} + +/// Batch-liquidate positions using the liquidator's own tokens as repayment (no DEX). +/// The liquidator must hold sufficient debt tokens upfront. +access(all) fun batchManualLiquidation( + pids: [UInt64], + debtVaultIdentifier: String, + seizeVaultIdentifiers: [String], + seizeAmounts: [UFix64], + repayAmounts: [UFix64], + signer: Test.TestAccount +) { + let res = _executeTransaction( + "../transactions/flow-alp/pool-management/batch_manual_liquidation.cdc", + [pids, debtVaultIdentifier, seizeVaultIdentifiers, seizeAmounts, repayAmounts], + signer + ) + Test.expect(res, Test.beSucceeded()) +} + +/// Batch-liquidate positions using MockDexSwapper as the repayment source in chunks of +/// chunkSize to stay within the computation limit. +access(all) fun batchLiquidateViaMockDex( + pids: [UInt64], + debtVaultIdentifier: String, + seizeVaultIdentifiers: [String], + seizeAmounts: [UFix64], + repayAmounts: [UFix64], + chunkSize: Int, + signer: Test.TestAccount +) { + let total = pids.length + let numChunks = (total + chunkSize - 1) / chunkSize + for i in InclusiveRange(0, numChunks - 1) { + let startIdx = i * chunkSize + var endIdx = startIdx + chunkSize + if endIdx > total { + endIdx = total + } + let res = _executeTransaction( + "../transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc", + [pids.slice(from: startIdx, upTo: endIdx), + debtVaultIdentifier, + seizeVaultIdentifiers.slice(from: startIdx, upTo: endIdx), + seizeAmounts.slice(from: startIdx, upTo: endIdx), + repayAmounts.slice(from: startIdx, upTo: endIdx)], + signer + ) + Test.expect(res, Test.beSucceeded()) + } +} + access(all) fun expectEvents(eventType: Type, expectedCount: Int) { let events = Test.eventsOfType(eventType) From e7216bdcfe57bb48fa6c2238090fe01d57c10890 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Wed, 25 Feb 2026 16:51:39 -0300 Subject: [PATCH 07/25] Update cadence/tests/fork_multiple_positions_per_user.cdc Co-authored-by: patrick <72362902+holyfuchs@users.noreply.github.com> --- cadence/tests/fork_multiple_positions_per_user.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user.cdc index 788f17f4..25756026 100644 --- a/cadence/tests/fork_multiple_positions_per_user.cdc +++ b/cadence/tests/fork_multiple_positions_per_user.cdc @@ -885,7 +885,7 @@ access(all) fun testMassUnhealthyLiquidations() { for i in InclusiveRange(0, 44) { let pidIdx = i + 50 let h = getPositionHealth(pid: allPids[pidIdx], beFailed: false) - Test.assert(h > usdcHealths[i], message: "USDC pos \(allPids[i]) health must improve: \(usdcHealths[i]) → \(h)") + Test.assert(h > usdcHealths[i], message: "USDC pos \(allPids[pidIdx]) health must improve: \(usdcHealths[i]) → \(h)") Test.assert(h > 1.0, message: "USDC pos \(allPids[pidIdx]) must be healthy again (got \(h))") } // WBTC [95..99] From d064bc7d18feb37e9cd6969595ab4e22237b503a Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Wed, 25 Feb 2026 21:28:21 +0100 Subject: [PATCH 08/25] fix naming *_test.cdc, move not production script to test dir --- ...s_per_user.cdc => fork_multiple_positions_per_user_test.cdc} | 0 cadence/tests/test_helpers.cdc | 2 +- .../flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename cadence/tests/{fork_multiple_positions_per_user.cdc => fork_multiple_positions_per_user_test.cdc} (100%) rename cadence/{ => tests}/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc (100%) diff --git a/cadence/tests/fork_multiple_positions_per_user.cdc b/cadence/tests/fork_multiple_positions_per_user_test.cdc similarity index 100% rename from cadence/tests/fork_multiple_positions_per_user.cdc rename to cadence/tests/fork_multiple_positions_per_user_test.cdc diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 9e62a534..ae1ac8c5 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -884,7 +884,7 @@ access(all) fun batchLiquidateViaMockDex( endIdx = total } let res = _executeTransaction( - "../transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc", + "./transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc", [pids.slice(from: startIdx, upTo: endIdx), debtVaultIdentifier, seizeVaultIdentifiers.slice(from: startIdx, upTo: endIdx), diff --git a/cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc b/cadence/tests/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc similarity index 100% rename from cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc rename to cadence/tests/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc From 887ae21788fd0172659b8314007dba092b279235 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Wed, 25 Feb 2026 21:37:26 +0100 Subject: [PATCH 09/25] flow.json: fix block height --- flow.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flow.json b/flow.json index 70050a8f..2d5bbcd4 100644 --- a/flow.json +++ b/flow.json @@ -122,7 +122,7 @@ "Burner": { "source": "mainnet://f233dcee88fe0abe.Burner", "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", @@ -168,7 +168,7 @@ "FlowFees": { "source": "mainnet://f919ee77447b7497.FlowFees", "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "e5a8b7f23e8b548f", "mainnet": "f919ee77447b7497", @@ -179,7 +179,7 @@ "FlowStorageFees": { "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -190,7 +190,7 @@ "FlowToken": { "source": "mainnet://1654653399040a61.FlowToken", "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -201,7 +201,7 @@ "FlowTransactionScheduler": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", "hash": "23157cf7d70534e45b0ab729133232d0ffb3cdae52661df1744747cb1f8c0495", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -221,7 +221,7 @@ "FungibleToken": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -232,7 +232,7 @@ "FungibleTokenMetadataViews": { "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -243,7 +243,7 @@ "MetadataViews": { "source": "mainnet://1d7e57aa55817448.MetadataViews", "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -254,7 +254,7 @@ "NonFungibleToken": { "source": "mainnet://1d7e57aa55817448.NonFungibleToken", "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -265,7 +265,7 @@ "ViewResolver": { "source": "mainnet://1d7e57aa55817448.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", - "block_height": 139085361, + "block_height": 141019535, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", From 0df9e00818c675408084f907eb54e1e56c38e75e Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Wed, 25 Feb 2026 21:47:04 +0100 Subject: [PATCH 10/25] fix block height in flow.json, add actions/cache for ./imports keyed on flow.json hash --- .github/workflows/cadence_tests.yml | 4 ++++ flow.json | 30 ++++++++++++++--------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/workflows/cadence_tests.yml b/.github/workflows/cadence_tests.yml index dd67efb5..d0fe6a17 100644 --- a/.github/workflows/cadence_tests.yml +++ b/.github/workflows/cadence_tests.yml @@ -28,6 +28,10 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- + - uses: actions/cache@v4 + with: + path: ./imports + key: flow-deps-${{ hashFiles('flow.json') }} - name: Install Flow CLI env: FLOW_CLI_VERSION: v2.7.2 diff --git a/flow.json b/flow.json index 2d5bbcd4..feb834b0 100644 --- a/flow.json +++ b/flow.json @@ -122,7 +122,7 @@ "Burner": { "source": "mainnet://f233dcee88fe0abe.Burner", "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", @@ -133,7 +133,7 @@ "EVM": { "source": "mainnet://e467b9dd11fa00df.EVM", "hash": "960b0c7df7ee536956af196fba8c8d5dd4f7a89a4ecc61467e31287c4617b0dd", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -144,7 +144,7 @@ "FlowCron": { "source": "mainnet://6dec6e64a13b881e.FlowCron", "hash": "ab570aabfb4d3ee01537ad85ad789ed13ac193ba447bc037365d51d748272cd5", - "block_height": 141024643, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "6dec6e64a13b881e", @@ -156,7 +156,7 @@ "FlowCronUtils": { "source": "mainnet://6dec6e64a13b881e.FlowCronUtils", "hash": "498c32c1345b9b1db951a18e4ea94325b8c9c05ea691f2d9b6af75b886ab51a2", - "block_height": 141024643, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "6dec6e64a13b881e", @@ -168,7 +168,7 @@ "FlowFees": { "source": "mainnet://f919ee77447b7497.FlowFees", "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "e5a8b7f23e8b548f", "mainnet": "f919ee77447b7497", @@ -179,7 +179,7 @@ "FlowStorageFees": { "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -190,7 +190,7 @@ "FlowToken": { "source": "mainnet://1654653399040a61.FlowToken", "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -201,7 +201,7 @@ "FlowTransactionScheduler": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", "hash": "23157cf7d70534e45b0ab729133232d0ffb3cdae52661df1744747cb1f8c0495", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -212,7 +212,7 @@ "FlowTransactionSchedulerUtils": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionSchedulerUtils", "hash": "71a1febab6b9ba76abec36dab1e61b1c377e44fbe627e5fac649deb71b727877", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "mainnet": "e467b9dd11fa00df", "mainnet-fork": "e467b9dd11fa00df" @@ -221,7 +221,7 @@ "FungibleToken": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -232,7 +232,7 @@ "FungibleTokenMetadataViews": { "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -243,7 +243,7 @@ "MetadataViews": { "source": "mainnet://1d7e57aa55817448.MetadataViews", "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -254,7 +254,7 @@ "NonFungibleToken": { "source": "mainnet://1d7e57aa55817448.NonFungibleToken", "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -265,7 +265,7 @@ "ViewResolver": { "source": "mainnet://1d7e57aa55817448.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", - "block_height": 141019535, + "block_height": 143000000, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -375,7 +375,6 @@ "MockDexSwapper", "MockOracle" ] - }, "mainnet-fork": { "mainnet-fork-deployer": [ @@ -395,7 +394,6 @@ "MockDexSwapper", "MockOracle" ] - }, "testnet": { "testnet-deployer": [ From c9cc89448c2f97121e2b494a4b9a4bdeb198ca41 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Thu, 26 Feb 2026 12:23:23 +0100 Subject: [PATCH 11/25] add more description comments to test --- .../fork_multiple_positions_per_user_test.cdc | 245 ++++++++++-------- cadence/tests/test_helpers.cdc | 18 +- 2 files changed, 152 insertions(+), 111 deletions(-) diff --git a/cadence/tests/fork_multiple_positions_per_user_test.cdc b/cadence/tests/fork_multiple_positions_per_user_test.cdc index 25756026..77de8083 100644 --- a/cadence/tests/fork_multiple_positions_per_user_test.cdc +++ b/cadence/tests/fork_multiple_positions_per_user_test.cdc @@ -9,10 +9,12 @@ import "MOET" import "FlowALPv0" import "test_helpers.cdc" -// Protocol account: in fork mode, Test.deployContract() deploys to the contract's mainnet -// alias address. FlowALPv0's mainnet alias is 0x47f544294e3b7656, so PoolFactory and all -// pool admin resources are stored there. Note: this is the same address as wbtcHolder. -access(all) let protocolAccount = Test.getAccount(0x47f544294e3b7656) +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) let MAINNET_USDC_HOLDER = Test.getAccount(MAINNET_USDC_HOLDER_ADDRESS) access(all) var snapshot: UInt64 = 0 @@ -80,19 +82,19 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MAINNET_MOET_TOKEN_IDENTIFIER, beFailed: false) + createAndStorePool(signer: MAINNET_PROTOCOL_ACCOUNT, defaultTokenIdentifier: MAINNET_MOET_TOKEN_IDENTIFIER, beFailed: false) // Setup pool with real mainnet token prices - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, price: 3500.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 50000.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_MOET_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 1.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, price: 3500.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 50000.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_MOET_TOKEN_IDENTIFIER, price: 1.0) // Add multiple token types as supported collateral (FLOW, USDC, USDF, WETH, WBTC) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, collateralFactor: 0.8, borrowFactor: 1.0, @@ -101,7 +103,7 @@ access(all) fun setup() { ) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, collateralFactor: 0.85, borrowFactor: 1.0, @@ -110,7 +112,7 @@ access(all) fun setup() { ) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, collateralFactor: 0.85, borrowFactor: 1.0, @@ -119,7 +121,7 @@ access(all) fun setup() { ) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, collateralFactor: 0.75, borrowFactor: 1.0, @@ -128,10 +130,10 @@ access(all) fun setup() { ) // Set minimum deposit for WETH to 0.01 (since holder only has 0.07032) - setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, minimum: 0.01) + setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, minimum: 0.01) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, collateralFactor: 0.75, borrowFactor: 1.0, @@ -139,18 +141,31 @@ access(all) fun setup() { depositCapacityCap: 1_000_000.0 ) // Set minimum deposit for WBTC to 0.00001 (since holder only has 0.0005) - setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, minimum: 0.00001) + setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, minimum: 0.00001) snapshot = getCurrentBlockHeight() } -// Test Multiple Positions Per User +// ============================================================================= +// Multiple Positions Per User // -// Validates requirements: -// 1. User creates 5+ positions with different collateral types -// 2. Each position has different health factors -// 3. Operations on one position should not affect others (isolation) +// Validates that a single user can hold 5 independent positions with distinct +// collateral types, and that operations on one position have no effect on any +// other (isolation guarantee). // +// Pool liquidity: 800 FLOW LP deposit +// +// Positions (all borrow FLOW as debt): +// pos 1: 500 FLOW @ $1.00 (CF=0.80), borrow 100 → health = 500*1.0*0.80/100 = 4.000 +// pos 2: 1500 USDF @ $1.00 (CF=0.85), borrow 150 → health = 1500*1.0*0.85/150 = 8.500 +// pos 3: 10 USDC @ $1.00 (CF=0.85), borrow 5 → health = 10*1.0*0.85/5 = 1.700 +// pos 4: 0.05 WETH @ $3500 (CF=0.75), borrow 50 → health = 0.05*3500*0.75/50 = 2.625 +// pos 5: 0.0004 WBTC @ $50000 (CF=0.75), borrow 8 → health = 0.0004*50000*0.75/8 = 1.875 +// +// Isolation test: borrow 100 more FLOW from pos 2 (USDF) +// new debt = 150 + 100 = 250 → health = 1500*1.0*0.85/250 = 5.100 (lower) +// pos 1, 3, 4, 5: unchanged +// ============================================================================= access(all) fun testMultiplePositionsPerUser() { safeReset() @@ -165,7 +180,7 @@ access(all) fun testMultiplePositionsPerUser() { transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: liquidityAmount) // LP deposits FLOW to create liquidity for borrowing - createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) //////////// Position creation /////////////////// log("Create 5 Positions with Different Collateral Types\n") @@ -200,7 +215,7 @@ access(all) fun testMultiplePositionsPerUser() { // Transfer tokens from holder to user transferTokensWithSetup(tokenIdentifier: collateralType, from: holder, to: user, amount: collateralAmount) - createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) userPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) @@ -251,13 +266,34 @@ access(all) fun testMultiplePositionsPerUser() { Test.assert(healthsAfterBorrow[4] == healths[4], message: "Position 5 should be unchanged") } -// Test Position Interactions Through Shared Liquidity Pools +// ============================================================================= +// Position Interactions Through Shared Liquidity Pool +// +// Validates cross-position effects mediated by a shared FLOW supply. Position A +// and B compete for the same limited liquidity; a repayment by one restores it +// for the other. A price crash on A's collateral leaves B's health unaffected. +// +// Pool liquidity: 400 FLOW LP deposit +// +// Position A: 90 USDC @ $1.00 (CF=0.85), borrow 60 FLOW +// health = 90*1.0*0.85 / 60 = 76.5/60 = 1.275 +// pool remaining = 400 - 60 = 340 FLOW // -// Validates that multiple positions interact through shared pool resources: -// 1. Multiple positions compete for limited deposit capacity -// 2. Position A's borrowing reduces available liquidity for Position B -// 3. Shared liquidity pools create cross-position effects -// 4. Pool capacity constraints affect all positions +// Position B: 500 USDF @ $1.00 (CF=0.85), borrow 340 FLOW (drains pool) +// health = 500*1.0*0.85 / 340 = 425/340 = 1.250 +// pool remaining = 0 → Position B borrow of 1 FLOW fails +// +// Position A repays 40 FLOW: +// debt = 60 - 40 = 20 → health = 76.5/20 = 3.825 +// pool remaining = 40 FLOW +// +// USDC price crash $1.00 → $0.50 (Position A's collateral only): +// Position A health = 90*0.50*0.85 / 20 = 38.25/20 = 1.913 (still healthy) +// Position B health: unchanged (USDF collateral unaffected) +// +// Position B borrows 30 FLOW from restored pool: +// health = 500*1.0*0.85 / (340 + 30) = 425/370 = 1.149 +// ============================================================================= access(all) fun testPositionInteractionsSharedLiquidity() { safeReset() @@ -272,7 +308,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: liquidityAmount) // LP deposits FLOW - this creates the shared liquidity pool - createPosition(admin: protocolAccount, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) log(" Liquidity Provider deposited: \(liquidityAmount) FLOW\n") //////////// Create Position A with USDC collateral /////////////////// @@ -280,7 +316,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let userACollateral = 90.0 // 90 USDC log("Creating Position A with \(userACollateral) USDC collateral\n") transferTokensWithSetup(tokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, from: MAINNET_USDC_HOLDER, to: user, amount: userACollateral) - createPosition(admin: protocolAccount, signer: user, amount: userACollateral, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: userACollateral, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) var openEvts = Test.eventsOfType(Type()) let positionA_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid @@ -289,14 +325,14 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let userBCollateral = 500.0 // 500 USDF log("Creating Position B with \(userBCollateral) USDF collateral\n") transferTokensWithSetup(tokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, from: MAINNET_USDF_HOLDER, to: user, amount: userBCollateral) - createPosition(admin: protocolAccount, signer: user, amount: userBCollateral, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: userBCollateral, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) openEvts = Test.eventsOfType(Type()) let positionB_id = (openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid //////////// 1. Position A borrows heavily, affecting available liquidity /////////////////// log("Position A borrows heavily from shared pool\n") - // Formula: Effective Collateral = (debitAmount * price) * collateralFactor = (90 × 1.0) × 0.85 = 76.50 + // Formula: Effective Collateral = (collateralAmount * price) * collateralFactor = (90 × 1.0) × 0.85 = 76.50 // Max Borrow = 76.50 / 1.1 (minHealth) = 69.55 FLOW // Health after borrow = 76.50 / 60 = 1.275 let positionA_borrow1 = 60.0 // Borrow 60 FLOW (within max 69.55) @@ -358,7 +394,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log(" Position B health: \(healthB_before_priceChange)") // Crash USDC price (Position A's collateral) from $1.0 to $0.5 - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.5) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.5) let healthA_after_crash = getPositionHealth(pid: positionA_id, beFailed: false) log(" Position A health after price crash: \(healthA_after_crash)\n") @@ -386,12 +422,47 @@ access(all) fun testPositionInteractionsSharedLiquidity() { } -// Test Batch Liquidations +// ============================================================================= +// Batch Liquidations — 2 Full + 2 Partial in One Transaction +// +// Validates that multiple unhealthy positions can be liquidated atomically in a +// single transaction via the batch DEX helper. Full liquidations bring positions +// above health 1.0; partial liquidations improve health without fully recovering. // -// Validates batch liquidation capabilities: -// 1. Multiple unhealthy positions liquidated in SINGLE transaction -// 2. Partial liquidation of multiple positions -// 3. Gas cost optimization through batch processing +// Pool liquidity: 600 FLOW LP deposit +// +// Positions (all borrow FLOW as debt): +// pid 0: 500 USDF @ $1.00 (CF=0.85), borrow 200 → health = 500*1.0*0.85/200 = 2.125 +// pid 1: 0.06 WETH @ $3500 (CF=0.75), borrow 90 → health = 0.06*3500*0.75/90 = 1.750 +// pid 2: 80 USDC @ $1.00 (CF=0.85), borrow 40 → health = 80*1.0*0.85/40 = 1.700 +// pid 3: 0.0004 WBTC @ $50000 (CF=0.75), borrow 10 → health = 0.0004*50000*0.75/10 = 1.500 +// pid 4: 200 FLOW @ $1.00 (CF=0.80), borrow 80 → health = 200*1.0*0.80/80 = 2.000 +// +// Price crash: +// USDF: $1.00 → $0.30 (-70%) | WETH: $3500 → $1050 (-70%) +// USDC: $1.00 → $0.50 (-50%) | WBTC: $50000 → $25000 (-50%) | FLOW: unchanged +// +// Health after crash: +// pid 0 (USDF): 500*0.30*0.85/200 = 127.5/200 = 0.638 (unhealthy) +// pid 1 (WETH): 0.06*1050*0.75/90 = 47.25/90 = 0.525 (unhealthy) +// pid 2 (USDC): 80*0.50*0.85/40 = 34/40 = 0.850 (unhealthy) +// pid 3 (WBTC): 0.0004*25000*0.75/10 = 7.5/10 = 0.750 (unhealthy) +// pid 4 (FLOW): 200*1.00*0.80/80 = 160/80 = 2.000 (healthy, not liquidated) +// +// Batch liquidation (target health 1.05, post ≈1.03 for full, <1.0 for partial): +// pid 1 FULL: seize 0.035 WETH, repay 71 FLOW +// post = (0.06-0.035)*1050*0.75 / (90-71) = 19.6875/19 ≈ 1.036 +// DEX: 0.035 < 71/1050 = 0.0676 +// pid 0 FULL: seize 147 USDF, repay 113 FLOW +// post = (500-147)*0.30*0.85 / (200-113) = 90.015/87 ≈ 1.034 +// DEX: 147 < 113/0.30 = 376.7 +// pid 3 PARTIAL: seize 0.00011 WBTC, repay 4 FLOW +// post = (0.0004-0.00011)*25000*0.75 / (10-4) = 5.4375/6 ≈ 0.906 (still unhealthy) +// DEX: 0.00011 < 4/25000 = 0.00016 +// pid 2 PARTIAL: seize 17 USDC, repay 12 FLOW +// post = (80-17)*0.50*0.85 / (40-12) = 26.775/28 ≈ 0.956 (still unhealthy) +// DEX: 17 < 12/0.50 = 24.0 +// ============================================================================= access(all) fun testBatchLiquidations() { safeReset() @@ -403,7 +474,7 @@ access(all) fun testBatchLiquidations() { // LP deposits 600 FLOW to provide borrowing liquidity // (total borrows = 200+90+40+10+80 = 420 FLOW < 600) transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: 600.0) - createPosition(admin: protocolAccount, signer: lpUser, amount: 600.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 600.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) // 5 positions with distinct collateral types: // @@ -435,7 +506,7 @@ access(all) fun testBatchLiquidations() { let holder = position["holder"]! as! Test.TestAccount transferTokensWithSetup(tokenIdentifier: collateralType, from: holder, to: user, amount: collateralAmount) - createPosition(admin: protocolAccount, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: collateralAmount, vaultStoragePath: storagePath, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) userPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } @@ -456,10 +527,10 @@ access(all) fun testBatchLiquidations() { // Crash collateral prices. FLOW stays at $1.0 so userPids[4] stays healthy. log("\nCrashing collateral prices to trigger liquidations\n") - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 0.3) // -70% - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, price: 1050.0) // -70% - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.5) // -50% - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 25000.0) // -50% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 0.3) // -70% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, price: 1050.0) // -70% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.5) // -50% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 25000.0) // -50% log("\nPosition health after price crash:\n") for i in InclusiveRange(0, 4) { @@ -485,32 +556,32 @@ access(all) fun testBatchLiquidations() { // Setup protocol account FLOW vault as the DEX output source. // priceRatio = Pc_crashed / Pd = post-crash collateral price / FLOW price. // This must match the oracle prices exactly to pass the DEX/oracle deviation check. - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: protocolAccount, amount: 300.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: MAINNET_PROTOCOL_ACCOUNT, amount: 300.0) log("\nSetting up DEX swappers (priceRatio = post-crash Pc / Pd)\n") setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.3 // $0.30 USDF / $1.00 FLOW ) setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WETH_TOKEN_IDENTIFIER, outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 1050.0 // $1050 WETH / $1.00 FLOW ) setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.5 // $0.50 USDC / $1.00 FLOW ) setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, @@ -593,47 +664,13 @@ access(all) fun testBatchLiquidations() { Test.assert(healthAfterFlow == healths[4], message: "FLOW position health should be unchanged") } -// Test Mass Simultaneous Unhealthy Positions – 100-Position Multi-Collateral Stress Test -// -// System-wide stress test validating protocol behavior under mass position failure -// across three collateral types — all crashing 40% simultaneously: -// -// 100 positions (all borrowing FLOW as debt): -// Group A: 50 USDF positions (10 USDF each) — 25 high-risk + 25 moderate -// Group B: 45 USDC positions (2 USDC each) — 23 high-risk + 22 moderate -// Group C: 5 WBTC positions (0.00009 WBTC ea) — 5 uniform (same risk tier) -// -// Health before crash (CF_USDF=CF_USDC=0.85, CF_WBTC=0.75): -// USDF high-risk: borrow 7.0 FLOW → (10×1.0×0.85)/7.0 = 1.214 -// USDF moderate: borrow 6.0 FLOW → (10×1.0×0.85)/6.0 = 1.417 -// USDC high-risk: borrow 1.4 FLOW → (2×1.0×0.85)/1.4 = 1.214 -// USDC moderate: borrow 1.2 FLOW → (2×1.0×0.85)/1.2 = 1.417 -// WBTC uniform: borrow 2.5 FLOW → (0.00009×50000×0.75)/2.5 = 1.350 -// -// All collateral crashes 40% simultaneously: -// USDF: $1.00 → $0.60 | USDC: $1.00 → $0.60 | WBTC: $50000 → $30000 +// ============================================================================= +// Mass Simultaneous Unhealthy Liquidations — 100-Position Stress Test // -// Health after crash: -// USDF high: (10×0.60×0.85)/7.0 = 0.729 USDF mod: (10×0.60×0.85)/6.0 = 0.850 -// USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 -// WBTC: (0.00009×30000×0.75)/2.5 = 0.810 +// System-wide stress test: 100 positions across three collateral types all crash +// 40% simultaneously, requiring a chunked batch DEX liquidation of every position. // -// Liquidation (liquidationTargetHF=1.05, post target≈1.02–1.04): -// USDF high: seize 4.0 USDF, repay 4.0 FLOW → post = (10-4)×0.6×0.85/(7-4) = 1.02 -// DEX: 4.0 < 4.0/0.6 = 6.67 -// USDF mod: seize 4.0 USDF, repay 3.0 FLOW → post = (10-4)×0.6×0.85/(6-3) = 1.02 -// DEX: 4.0 < 3.0/0.6 = 5.00 -// USDC high: seize 0.8 USDC, repay 0.8 FLOW → post = (2-0.8)×0.6×0.85/(1.4-0.8) = 1.02 -// DEX: 0.8 < 0.8/0.6 = 1.33 -// USDC mod: seize 0.8 USDC, repay 0.6 FLOW → post = (2-0.8)×0.6×0.85/(1.2-0.6) = 1.02 -// DEX: 0.8 < 0.6/0.6 = 1.00 -// WBTC: seize 0.00003 WBTC, repay 1.18 FLOW → post = (0.00006)×22500/(2.5-1.18) = 1.023 -// DEX: 0.00003 < 1.18/30000 = 0.0000393 -// Token budget (mainnet): -// flowHolder (1921 FLOW): 450 LP + 230 DEX source = 680 FLOW total -// usdfHolder (25000 USDF): 500 USDF for 50 positions -// usdcHolder (97 USDC): 90 USDC for 45 positions -// wbtcHolder (0.0005 WBTC): 0.00045 WBTC for 5 positions (holder has 0.00049998) +// ============================================================================= access(all) fun testMassUnhealthyLiquidations() { safeReset() @@ -648,7 +685,7 @@ access(all) fun testMassUnhealthyLiquidations() { // LP deposits 450 FLOW — covers the ~397 FLOW of total borrows with headroom. log("LP depositing 450 FLOW to shared liquidity pool\n") transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: 450.0) - createPosition(admin: protocolAccount, signer: lpUser, amount: 450.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 450.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) //////////// Transfer collateral to user /////////////////// @@ -667,7 +704,7 @@ access(all) fun testMassUnhealthyLiquidations() { // Group A — 50 USDF positions log("Creating 50 USDF positions (10 USDF each)...\n") for i in InclusiveRange(0, 49) { - createPosition(admin: protocolAccount, signer: user, amount: 10.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 10.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } @@ -675,7 +712,7 @@ access(all) fun testMassUnhealthyLiquidations() { // Group B — 45 USDC positions log("Creating 45 USDC positions (2 USDC each)...\n") for i in InclusiveRange(50, 94) { - createPosition(admin: protocolAccount, signer: user, amount: 2.0, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 2.0, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } @@ -683,7 +720,7 @@ access(all) fun testMassUnhealthyLiquidations() { // Group C — 5 WBTC positions log("Creating 5 WBTC positions (0.00009 WBTC each)...\n") for i in InclusiveRange(95, 99) { - createPosition(admin: protocolAccount, signer: user, amount: 0.00009, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 0.00009, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, pushToDrawDownSink: false) let openEvts = Test.eventsOfType(Type()) allPids.append((openEvts[openEvts.length - 1] as! FlowALPv0.Opened).pid) } @@ -736,9 +773,9 @@ access(all) fun testMassUnhealthyLiquidations() { // USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 // WBTC: (0.00009×30000×0.75)/2.5 = 0.810 log("All three collateral types crash 40% simultaneously\n") - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 0.6) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.6) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 30000.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, price: 0.6) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, price: 0.6) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, price: 30000.0) // Capture post-crash health by token type and verify all positions are unhealthy var usdfHealths: [UFix128] = [] @@ -773,7 +810,7 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// DEX setup /////////////////// - // Three DEX pairs (all source FLOW from protocolAccount's vault): + // Three DEX pairs (all source FLOW from MAINNET_PROTOCOL_ACCOUNT's vault): // USDF→FLOW at priceRatio=0.6 ($0.60 USDF / $1.00 FLOW) // USDC→FLOW at priceRatio=0.6 ($0.60 USDC / $1.00 FLOW) // WBTC→FLOW at priceRatio=30000 ($30000 WBTC / $1.00 FLOW) @@ -781,23 +818,23 @@ access(all) fun testMassUnhealthyLiquidations() { // Total DEX FLOW: 25×4.0 + 25×3.0 + 23×0.8 + 22×0.6 + 5×1.18 // = 100 + 75 + 18.4 + 13.2 + 5.90 = 212.50; transfer 230 for headroom log("Configuring DEX pairs: USDF→FLOW, USDC→FLOW, WBTC→FLOW\n") - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: protocolAccount, amount: 230.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, from: MAINNET_FLOW_HOLDER, to: MAINNET_PROTOCOL_ACCOUNT, amount: 230.0) setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDF_TOKEN_IDENTIFIER, outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.6 // $0.60 USDF / $1.00 FLOW ) setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDC_TOKEN_IDENTIFIER, outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, priceRatio: 0.6 // $0.60 USDC / $1.00 FLOW ) setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WBTC_TOKEN_IDENTIFIER, outVaultIdentifier: MAINNET_FLOW_TOKEN_IDENTIFIER, vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, @@ -858,7 +895,7 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// Batch liquidation — 100 positions in chunks of 10 /////////////////// // Split into chunks of 10 to stay within the computation limit (single tx of 100 exceeds it). - // DEX sources FLOW from protocolAccount's vault; liquidator needs no tokens upfront. + // DEX sources FLOW from MAINNET_PROTOCOL_ACCOUNT's vault; liquidator needs no tokens upfront. log("Liquidating all 100 positions via DEX in chunks of 10...\n") batchLiquidateViaMockDex( pids: batchPids, diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index ae1ac8c5..0ecbeeaf 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -23,7 +23,7 @@ access(all) let MAX_HEALTH = 1.5 access(all) let INT_MIN_HEALTH: UFix128 = 1.1 access(all) let INT_TARGET_HEALTH: UFix128 = 1.3 access(all) let INT_MAX_HEALTH: UFix128 = 1.5 -access(all) let CEILING_HEALTH: UFix128 = UFix128.max // infinite health when debt ~ 0.0 +access(all) let CEILING_HEALTH = UFix128.max // infinite health when debt ~ 0.0 // Time constants access(all) let DAY: Fix64 = 86_400.0 @@ -50,12 +50,16 @@ access(all) let MAINNET_USDF_STORAGE_PATH = /storage/EVMVMBridgedToken_2aabea205 access(all) let MAINNET_WETH_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Vault access(all) let MAINNET_WBTC_STORAGE_PATH = /storage/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Vault -// Biggest token holders -access(all) let MAINNET_USDF_HOLDER = Test.getAccount(0xf18b50870aed46ad) // 25000 -access(all) let MAINNET_WETH_HOLDER = Test.getAccount(0xf62e3381a164f993) // 0.07032 -access(all) let MAINNET_WBTC_HOLDER = Test.getAccount(0x47f544294e3b7656) // 0.0005 -access(all) let MAINNET_FLOW_HOLDER = Test.getAccount(0xe467b9dd11fa00df) // 1921 -access(all) let MAINNET_USDC_HOLDER = Test.getAccount(0xec6119051f7adc31) // 97 +// Mainnet account addresses (used in fork tests) +// Note: MAINNET_PROTOCOL_ACCOUNT shares its address with MAINNET_WBTC_HOLDER (0x47f544294e3b7656). +// In fork mode, Test.deployContract() deploys FlowALPv0 to its mainnet alias, so PoolFactory +// and all pool admin resources are stored at MAINNET_PROTOCOL_ACCOUNT_ADDRESS. +access(all) let MAINNET_PROTOCOL_ACCOUNT_ADDRESS: Address = 0x47f544294e3b7656 +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 = 0xe467b9dd11fa00df +access(all) let MAINNET_USDC_HOLDER_ADDRESS: Address = 0xec6119051f7adc31 /* --- Test execution helpers --- */ From 0e5577c71393a18606e691ae38fcee7e04827405 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Thu, 26 Feb 2026 13:25:19 +0100 Subject: [PATCH 12/25] move scripts only for test to "cadence/test/transactions" --- cadence/tests/test_helpers.cdc | 2 +- .../flow-alp/pool-management/batch_manual_liquidation.cdc | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename cadence/{ => tests}/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc (100%) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 0ecbeeaf..3e89482c 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -861,7 +861,7 @@ access(all) fun batchManualLiquidation( signer: Test.TestAccount ) { let res = _executeTransaction( - "../transactions/flow-alp/pool-management/batch_manual_liquidation.cdc", + "./transactions/flow-alp/pool-management/batch_manual_liquidation.cdc", [pids, debtVaultIdentifier, seizeVaultIdentifiers, seizeAmounts, repayAmounts], signer ) diff --git a/cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc b/cadence/tests/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc similarity index 100% rename from cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc rename to cadence/tests/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc From 5bc3cf1c4ed7148785cd3df0bb2637ea4670ea19 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Thu, 26 Feb 2026 13:32:17 +0100 Subject: [PATCH 13/25] remove useless transaction from test --- .../test/transfer_tokens_with_setup.cdc | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 cadence/transactions/test/transfer_tokens_with_setup.cdc diff --git a/cadence/transactions/test/transfer_tokens_with_setup.cdc b/cadence/transactions/test/transfer_tokens_with_setup.cdc deleted file mode 100644 index 455fa6da..00000000 --- a/cadence/transactions/test/transfer_tokens_with_setup.cdc +++ /dev/null @@ -1,31 +0,0 @@ -import FungibleToken from "FungibleToken" - -/// Transfer tokens from holder to recipient -/// Sets up recipient's vault if it doesn't exist -transaction(amount: UFix64, vaultPath: StoragePath) { - prepare(holder: auth(BorrowValue, Storage) &Account, recipient: auth(BorrowValue, Storage, Capabilities) &Account) { - - log("\(holder.address.toString())") - // Borrow holder's vault - let holderVault = holder.storage.borrow(from: vaultPath) - ?? panic("Could not borrow holder vault") - - // Setup recipient's vault if it doesn't exist - if recipient.storage.borrow<&{FungibleToken.Vault}>(from: vaultPath) == nil { - // Create empty vault - let emptyVault <- holderVault.withdraw(amount: 0.0) - recipient.storage.save(<-emptyVault, to: vaultPath) - - // Create and publish public capability - let pathIdentifier = vaultPath.toString().slice(from: 9, upTo: vaultPath.toString().length) - let publicPath = PublicPath(identifier: pathIdentifier)! - let cap = recipient.capabilities.storage.issue<&{FungibleToken.Receiver}>(vaultPath) - recipient.capabilities.publish(cap, at: publicPath) - } - - // Transfer tokens - let recipientVault = recipient.storage.borrow<&{FungibleToken.Receiver}>(from: vaultPath)! - let tokens <- holderVault.withdraw(amount: amount) - recipientVault.deposit(from: <-tokens) - } -} From d98a0aa73a1023fb5fdb878efc68dd6c3ecda803 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Tue, 10 Mar 2026 08:45:49 +0100 Subject: [PATCH 14/25] fix borrow asset to MOET instead of FLOW, fix liqudate transactions to deposit collateral back to liquidator --- .../fork_multiple_positions_per_user_test.cdc | 309 +++++++++--------- .../scripts}/get_oracle_price.cdc | 0 cadence/tests/test_helpers.cdc | 2 +- .../batch_liquidate_via_mock_dex.cdc | 19 +- .../batch_manual_liquidation.cdc | 42 ++- 5 files changed, 202 insertions(+), 170 deletions(-) rename cadence/{scripts/flow-alp => tests/scripts}/get_oracle_price.cdc (100%) diff --git a/cadence/tests/fork_multiple_positions_per_user_test.cdc b/cadence/tests/fork_multiple_positions_per_user_test.cdc index 2fff6690..aabf7d4c 100644 --- a/cadence/tests/fork_multiple_positions_per_user_test.cdc +++ b/cadence/tests/fork_multiple_positions_per_user_test.cdc @@ -101,16 +101,16 @@ access(all) fun setup() { // collateral types, and that operations on one position have no effect on any // other (isolation guarantee). // -// Pool liquidity: 800 FLOW LP deposit +// Pool liquidity: 800 MOET LP deposit // -// Positions (all borrow FLOW as debt): -// pos 1: 500 FLOW @ $1.00 (CF=0.80), borrow 100 → health = 500*1.0*0.80/100 = 4.000 -// pos 2: 1500 USDF @ $1.00 (CF=0.85), borrow 150 → health = 1500*1.0*0.85/150 = 8.500 -// pos 3: 10 USDC @ $1.00 (CF=0.85), borrow 5 → health = 10*1.0*0.85/5 = 1.700 -// pos 4: 0.05 WETH @ $3500 (CF=0.75), borrow 50 → health = 0.05*3500*0.75/50 = 2.625 -// pos 5: 0.0004 WBTC @ $50000 (CF=0.75), borrow 8 → health = 0.0004*50000*0.75/8 = 1.875 +// Positions (all borrow MOET as debt): +// pos 1: 500 FLOW @ 1.00 MOET (CF=0.80), borrow 100 → health = 500*1.0*0.80/100 = 4.000 +// pos 2: 1500 USDF @ 1.00 MOET (CF=0.85), borrow 150 → health = 1500*1.0*0.85/150 = 8.500 +// pos 3: 10 USDC @ 1.00 MOET (CF=0.85), borrow 5 → health = 10*1.0*0.85/5 = 1.700 +// pos 4: 0.05 WETH @ 3500 MOET (CF=0.75), borrow 50 → health = 0.05*3500*0.75/50 = 2.625 +// pos 5: 0.0004 WBTC @ 50000 MOET (CF=0.75), borrow 8 → health = 0.0004*50000*0.75/8 = 1.875 // -// Isolation test: borrow 100 more FLOW from pos 2 (USDF) +// Isolation test: borrow 100 more MOET from pos 2 (USDF) // new debt = 150 + 100 = 250 → health = 1500*1.0*0.85/250 = 5.100 (lower) // pos 1, 3, 4, 5: unchanged // ============================================================================= @@ -122,24 +122,25 @@ access(all) fun testMultiplePositionsPerUser() { let lpUser = Test.createAccount() let user = Test.createAccount() - // Transfer FLOW from holder to LP - log("Setting up liquidity provider with FLOW\n") + // Mint MOET to LP to create liquidity for borrowing + log("Setting up liquidity provider with MOET\n") let liquidityAmount = 800.0 - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: liquidityAmount) + setupMoetVault(lpUser, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: liquidityAmount, beFailed: false) - // LP deposits FLOW to create liquidity for borrowing - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // LP deposits MOET to create liquidity for borrowing + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: liquidityAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) //////////// Position creation /////////////////// log("Create 5 Positions with Different Collateral Types\n") // Define positions with different collateral types // Token holder balances and prices: - // - flowHolder: 1921 FLOW x $1 = $1921 - // - usdfHolder: 25000 USDF x $1 = $25000 - // - usdcHolder: 97 USDC x $1 = $97 - // - wethHolder: 0.07032 WETH x $3500 = $246.12 - // - wbtcHolder: 0.0005 WBTC x $50000 = $25 + // - flowHolder: 1921 FLOW x 1 = 1921 MOET + // - usdfHolder: 25000 USDF x 1 = 25000 MOET + // - usdcHolder: 97 USDC x 1 = 97 MOET + // - wethHolder: 0.07032 WETH x 3500 = 246.12 MOET + // - wbtcHolder: 0.0005 WBTC x 50000 = 25 MOET let positions = [ {"type": MAINNET_FLOW_TOKEN_ID, "amount": 500.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": MAINNET_FLOW_HOLDER}, @@ -178,7 +179,7 @@ access(all) fun testMultiplePositionsPerUser() { var healths: [UFix128] = [] for i, debt in debts { let pid = userPids[i] - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: debt, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: debt, beFailed: false) // Get health factor let health = getPositionHealth(pid: pid, beFailed: false) @@ -195,8 +196,8 @@ access(all) fun testMultiplePositionsPerUser() { log("Testing isolation by borrowing more from Position \(isolationTestPid)\n") - log("\n Action: Borrow 100 more FLOW from Position \(isolationTestPid)\n") - borrowFromPosition(signer: user, positionId: isolationTestPid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: additionalDebt, beFailed: false) + log("\n Action: Borrow 100 more MOET from Position \(isolationTestPid)\n") + borrowFromPosition(signer: user, positionId: isolationTestPid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: additionalDebt, beFailed: false) // Get health of all positions after var healthsAfterBorrow: [UFix128] = [] @@ -220,25 +221,25 @@ access(all) fun testMultiplePositionsPerUser() { // and B compete for the same limited liquidity; a repayment by one restores it // for the other. A price crash on A's collateral leaves B's health unaffected. // -// Pool liquidity: 400 FLOW LP deposit +// Pool liquidity: 400 MOET LP deposit // -// Position A: 90 USDC @ $1.00 (CF=0.85), borrow 60 FLOW +// Position A: 90 USDC @ 1.00 MOET (CF=0.85), borrow 60 MOET // health = 90*1.0*0.85 / 60 = 76.5/60 = 1.275 -// pool remaining = 400 - 60 = 340 FLOW +// pool remaining = 400 - 60 = 340 MOET // -// Position B: 500 USDF @ $1.00 (CF=0.85), borrow 340 FLOW (drains pool) +// Position B: 500 USDF @ 1.00 MOET (CF=0.85), borrow 340 MOET (drains pool) // health = 500*1.0*0.85 / 340 = 425/340 = 1.250 -// pool remaining = 0 → Position B borrow of 1 FLOW fails +// pool remaining = 0 → Position B borrow of 1 MOET fails // -// Position A repays 40 FLOW: +// Position A repays 40 MOET: // debt = 60 - 40 = 20 → health = 76.5/20 = 3.825 -// pool remaining = 40 FLOW +// pool remaining = 40 MOET // -// USDC price crash $1.00 → $0.50 (Position A's collateral only): +// USDC price crash 1.00 MOET → 0.50 MOET (Position A's collateral only): // Position A health = 90*0.50*0.85 / 20 = 38.25/20 = 1.913 (still healthy) // Position B health: unchanged (USDF collateral unaffected) // -// Position B borrows 30 FLOW from restored pool: +// Position B borrows 30 MOET from restored pool: // health = 500*1.0*0.85 / (340 + 30) = 425/370 = 1.149 // ============================================================================= access(all) fun testPositionInteractionsSharedLiquidity() { @@ -252,11 +253,12 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log("Setting up shared liquidity pool with limited capacity\n") let liquidityAmount = 400.0 - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: liquidityAmount) + setupMoetVault(lpUser, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: liquidityAmount, beFailed: false) - // LP deposits FLOW - this creates the shared liquidity pool - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: liquidityAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - log(" Liquidity Provider deposited: \(liquidityAmount) FLOW\n") + // LP deposits MOET - this creates the shared liquidity pool + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: liquidityAmount, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + log(" Liquidity Provider deposited: \(liquidityAmount) MOET\n") //////////// Create Position A with USDC collateral /////////////////// @@ -278,37 +280,37 @@ access(all) fun testPositionInteractionsSharedLiquidity() { log("Position A borrows heavily from shared pool\n") // Formula: Effective Collateral = (collateralAmount * price) * collateralFactor = (90 × 1.0) × 0.85 = 76.50 - // Max Borrow = 76.50 / 1.1 (minHealth) = 69.55 FLOW + // Max Borrow = 76.50 / 1.1 (minHealth) = 69.55 MOET // Health after borrow = 76.50 / 60 = 1.275 - let positionA_borrow1 = 60.0 // Borrow 60 FLOW (within max 69.55) - borrowFromPosition(signer: user, positionId: positionA_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionA_borrow1, beFailed: false) + let positionA_borrow1 = 60.0 // Borrow 60 MOET (within max 69.55) + borrowFromPosition(signer: user, positionId: positionA_id, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: positionA_borrow1, beFailed: false) let healthA_after1 = getPositionHealth(pid: positionA_id, beFailed: false) - log(" Position A borrowed \(positionA_borrow1) FLOW - Health: \(healthA_after1)\n") + log(" Position A borrowed \(positionA_borrow1) MOET - Health: \(healthA_after1)\n") - // Check remaining liquidity in pool: liquidityAmount - positionA_borrow1 = 400.0 - 60.0 = 340.0 FLOW - log(" Remaining liquidity in pool: 340.0 FLOW\n") + // Check remaining liquidity in pool: liquidityAmount - positionA_borrow1 = 400.0 - 60.0 = 340.0 MOET + log(" Remaining liquidity in pool: 340.0 MOET\n") //////////// 2. Position B borrows successfully from shared pool /////////////////// log("Position B borrows from shared pool\n") // Formula: Effective Collateral = (collateralAmount * price) * collateralFactor = (500 × 1.0) × 0.85 = 425.00 - // Max Borrow = 425.00 / 1.1 (minHealth) = 386.36 FLOW - let positionB_borrow1 = 340.0 // Borrow 340 FLOW (within max 386.36 borrow and 340 remaining liquidity) - log(" Attempting to borrow \(positionB_borrow1) FLOW...") - borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow1, beFailed: false) - log(" Success - Position B borrowed \(positionB_borrow1) FLOW") + // Max Borrow = 425.00 / 1.1 (minHealth) = 386.36 MOET + let positionB_borrow1 = 340.0 // Borrow 340 MOET (within max 386.36 borrow and 340 remaining liquidity) + log(" Attempting to borrow \(positionB_borrow1) MOET...") + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: positionB_borrow1, beFailed: false) + log(" Success - Position B borrowed \(positionB_borrow1) MOET") let healthB_after1 = getPositionHealth(pid: positionB_id, beFailed: false) log(" Position B Health: \(healthB_after1)\n") - log(" Remaining liquidity in pool: 0.0 FLOW\n") + log(" Remaining liquidity in pool: 0.0 MOET\n") //////////// 3. Position B tries to exceed max borrowing capacity - expects failure /////////////////// log("Position B tries to borrow beyond its capacity - EXPECTS FAILURE\n") // Position B can't borrow more because remaining liquidity is 0 let positionB_borrow2_attempt = 1.0 - log(" Attempting to borrow \(positionB_borrow2_attempt) FLOW...") - borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow2_attempt, beFailed: true) + log(" Attempting to borrow \(positionB_borrow2_attempt) MOET...") + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: positionB_borrow2_attempt, beFailed: true) log(" Failed as expected - remaining liquidity is 0\n") let healthB_after2 = getPositionHealth(pid: positionB_id, beFailed: false) @@ -316,15 +318,15 @@ access(all) fun testPositionInteractionsSharedLiquidity() { //////////// 4. Position A repayment increases available liquidity /////////////////// log("Position A repays debt, freeing liquidity back to pool\n") - // Position A repays substantial debt by depositing borrowed FLOW back + // Position A repays substantial debt by depositing borrowed MOET back let repayAmount = 40.0 - // Deposit FLOW back to position (repays debt using previously borrowed funds) - depositToPosition(signer: user, positionID: positionA_id, amount: repayAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // Deposit MOET back to position (repays debt using previously borrowed funds) + depositToPosition(signer: user, positionID: positionA_id, amount: repayAmount, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false) let healthA_after2 = getPositionHealth(pid: positionA_id, beFailed: false) - log(" Position A repaid \(repayAmount) FLOW - Health: \(healthA_after2)\n") - log(" Remaining liquidity in pool after repayment: \(repayAmount) FLOW\n") + log(" Position A repaid \(repayAmount) MOET - Health: \(healthA_after2)\n") + log(" Remaining liquidity in pool after repayment: \(repayAmount) MOET\n") //////////// Verify cross-position effects /////////////////// @@ -338,7 +340,7 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let healthB_before_priceChange = getPositionHealth(pid: positionB_id, beFailed: false) log(" Position B health: \(healthB_before_priceChange)") - // Crash USDC price (Position A's collateral) from $1.0 to $0.5 + // Crash USDC price (Position A's collateral) from 1.0 MOET to 0.5 MOET setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: 0.5) let healthA_after_crash = getPositionHealth(pid: positionA_id, beFailed: false) @@ -356,9 +358,9 @@ access(all) fun testPositionInteractionsSharedLiquidity() { // Position B can still borrow from the shared pool (liquidity is independent of Position A's health) // Position B has: 425 effective collateral, 340 borrowed, can borrow up to 46.36 more - let positionB_borrow3 = 30.0 // Well within remaining capacity (40 FLOW available, 46.36 max allowed) - log(" Position B attempts to borrow \(positionB_borrow3) FLOW after Position A's health deterioration...") - borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: positionB_borrow3, beFailed: false) + let positionB_borrow3 = 30.0 // Well within remaining capacity (40 MOET available, 46.36 max allowed) + log(" Position B attempts to borrow \(positionB_borrow3) MOET after Position A's health deterioration...") + borrowFromPosition(signer: user, positionId: positionB_id, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: positionB_borrow3, beFailed: false) log(" Success - Position B can still borrow despite Position A's poor health\n") let healthB_final = getPositionHealth(pid: positionB_id, beFailed: false) @@ -374,18 +376,18 @@ access(all) fun testPositionInteractionsSharedLiquidity() { // single transaction via the batch DEX helper. Full liquidations bring positions // above health 1.0; partial liquidations improve health without fully recovering. // -// Pool liquidity: 600 FLOW LP deposit +// Pool liquidity: 600 MOET LP deposit // -// Positions (all borrow FLOW as debt): -// pid 0: 500 USDF @ $1.00 (CF=0.85), borrow 200 → health = 500*1.0*0.85/200 = 2.125 -// pid 1: 0.06 WETH @ $3500 (CF=0.75), borrow 90 → health = 0.06*3500*0.75/90 = 1.750 -// pid 2: 80 USDC @ $1.00 (CF=0.85), borrow 40 → health = 80*1.0*0.85/40 = 1.700 -// pid 3: 0.0004 WBTC @ $50000 (CF=0.75), borrow 10 → health = 0.0004*50000*0.75/10 = 1.500 -// pid 4: 200 FLOW @ $1.00 (CF=0.80), borrow 80 → health = 200*1.0*0.80/80 = 2.000 +// Positions (all borrow MOET as debt): +// pid 0: 500 USDF @ 1.00 MOET (CF=0.85), borrow 200 → health = 500*1.0*0.85/200 = 2.125 +// pid 1: 0.06 WETH @ 3500 MOET (CF=0.75), borrow 90 → health = 0.06*3500*0.75/90 = 1.750 +// pid 2: 80 USDC @ 1.00 MOET (CF=0.85), borrow 40 → health = 80*1.0*0.85/40 = 1.700 +// pid 3: 0.0004 WBTC @ 50000 MOET (CF=0.75), borrow 10 → health = 0.0004*50000*0.75/10 = 1.500 +// pid 4: 200 FLOW @ 1.00 MOET (CF=0.80), borrow 80 → health = 200*1.0*0.80/80 = 2.000 // // Price crash: -// USDF: $1.00 → $0.30 (-70%) | WETH: $3500 → $1050 (-70%) -// USDC: $1.00 → $0.50 (-50%) | WBTC: $50000 → $25000 (-50%) | FLOW: unchanged +// USDF: 1.00 → 0.30 (-70%) | WETH: 3500 → 1050 (-70%) +// USDC: 1.00 → 0.50 (-50%) | WBTC: 50000 → 25000 (-50%) | FLOW: unchanged // // Health after crash: // pid 0 (USDF): 500*0.30*0.85/200 = 127.5/200 = 0.638 (unhealthy) @@ -416,20 +418,21 @@ access(all) fun testBatchLiquidations() { let lpUser = Test.createAccount() let user = Test.createAccount() - // LP deposits 600 FLOW to provide borrowing liquidity - // (total borrows = 200+90+40+10+80 = 420 FLOW < 600) - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: 600.0) - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 600.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // LP deposits 600 MOET to provide borrowing liquidity + // (total borrows = 200+90+40+10+80 = 420 MOET < 600) + setupMoetVault(lpUser, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: 600.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 600.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // 5 positions with distinct collateral types: // // pid | Collateral| Amount | Borrow | Crash price | Health after | Action // ----|-----------|-------------|----------|-------------|--------------|-------- - // 1 | USDF | 500 USDF | 200 FLOW | $0.30 (-70%)| 0.638 | FULL liquidation - // 2 | WETH | 0.06 WETH | 90 FLOW | $1050 (-70%)| 0.525 | FULL liquidation - // 3 | USDC | 80 USDC | 40 FLOW | $0.50 (-50%)| 0.850 | PARTIAL liquidation - // 4 | WBTC | 0.0004 WBTC | 10 FLOW | $25000(-50%)| 0.750 | PARTIAL liquidation - // 5 | FLOW | 200 FLOW | 80 FLOW | $1.00 (0%) | 2.000 | NOT liquidated + // 1 | USDF | 500 USDF | 200 MOET | 0.30 (-70%)| 0.638 | FULL liquidation + // 2 | WETH | 0.06 WETH | 90 MOET | 1050 (-70%)| 0.525 | FULL liquidation + // 3 | USDC | 80 USDC | 40 MOET | 0.50 (-50%)| 0.850 | PARTIAL liquidation + // 4 | WBTC | 0.0004 WBTC | 10 MOET | 25000(-50%)| 0.750 | PARTIAL liquidation + // 5 | FLOW | 200 FLOW | 80 MOET | 1.00 (0%) | 2.000 | NOT liquidated // log("Creating 5 positions with different collateral types\n") @@ -455,21 +458,21 @@ access(all) fun testBatchLiquidations() { userPids.append(getLastPositionId()) } - log("Borrowing FLOW from each position\n") + log("Borrowing MOET from each position\n") var healths: [UFix128] = [] for i, position in positions { let pid = userPids[i] let borrowAmount = position["borrow"]! as! UFix64 let collateralName = position["name"]! as! String - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: borrowAmount, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: borrowAmount, beFailed: false) let health = getPositionHealth(pid: pid, beFailed: false) healths.append(health) - log(" Position \(pid) (\(collateralName)): Borrowed \(borrowAmount) FLOW - Health: \(health)") + log(" Position \(pid) (\(collateralName)): Borrowed \(borrowAmount) MOET - Health: \(health)") } - // Crash collateral prices. FLOW stays at $1.0 so userPids[4] stays healthy. + // Crash collateral prices. FLOW stays at 1.0 so userPids[4] stays healthy. log("\nCrashing collateral prices to trigger liquidations\n") setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 0.3) // -70% setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 1050.0) // -70% @@ -497,52 +500,56 @@ access(all) fun testBatchLiquidations() { Test.assert(healths[0] < healths[3], message: "USDF should be worse than WBTC") Test.assert(healths[3] < healths[2], message: "WBTC should be worse than USDC") - // Setup protocol account FLOW vault as the DEX output source. - // priceRatio = Pc_crashed / Pd = post-crash collateral price / FLOW price. + // Setup protocol account MOET vault as the DEX output source. + // priceRatio = Pc_crashed / Pd = post-crash collateral price / MOET price. // This must match the oracle prices exactly to pass the DEX/oracle deviation check. - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: MAINNET_PROTOCOL_ACCOUNT, amount: 300.0) + setupMoetVault(MAINNET_PROTOCOL_ACCOUNT, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: MAINNET_PROTOCOL_ACCOUNT.address, amount: 300.0, beFailed: false) log("\nSetting up DEX swappers (priceRatio = post-crash Pc / Pd)\n") setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDF_TOKEN_ID, - outVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, - priceRatio: 0.3 // $0.30 USDF / $1.00 FLOW + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, + priceRatio: 0.3 // 0.30 USDF / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WETH_TOKEN_ID, - outVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, - priceRatio: 1050.0 // $1050 WETH / $1.00 FLOW + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, + priceRatio: 1050.0 // 1050 WETH / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDC_TOKEN_ID, - outVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, - priceRatio: 0.5 // $0.50 USDC / $1.00 FLOW + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, + priceRatio: 0.5 // 0.50 USDC / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WBTC_TOKEN_ID, - outVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, - priceRatio: 25000.0 // $25000 WBTC / $1.00 FLOW + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, + priceRatio: 25000.0 // 25000 WBTC / 1.00 MOET ) - // Liquidator setup: transfer FLOW for debt repayment (total needed: 71+113+4+12 = 200 FLOW) + // Liquidator setup: mint MOET for debt repayment (total needed: 71+113+4+12 = 200 MOET) // and 1 unit of each collateral token to initialize vault storage paths. // // Repay amounts derived from: repay = debt - (collat - seize) * CF * P_crashed / H_target + // let chose target health factor H_target ≈ 1.034 (randomly chosen ~1.03-1.04, close to 1.05 target) + // // WETH=71: debt=90, (0.06-0.035)*0.75*1050 = 19.6875, H≈1.034 → 90 - 19.6875/1.034 ≈ 71 // USDF=113: debt=200, (500-147)*0.85*0.3 = 90.015, H≈1.034 → 200 - 90.015/1.034 ≈ 113 // WBTC=4: partial; (0.0004-0.00011)*0.75*25000 = 5.4375 → repay=4 → postHealth=5.4375/6≈0.906 // USDC=12: partial; (80-17)*0.85*0.5 = 26.775 → repay=12 → postHealth=26.775/28≈0.956 log("\nSetting up liquidator account\n") let liquidator = Test.createAccount() - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: liquidator, amount: 250.0) + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: liquidator.address, amount: 250.0, beFailed: false) transferTokensWithSetup(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: liquidator, amount: 1.0) transferTokensWithSetup(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: liquidator, amount: 0.001) transferTokensWithSetup(tokenIdentifier: MAINNET_USDC_TOKEN_ID, from: MAINNET_USDC_HOLDER, to: liquidator, amount: 1.0) @@ -553,19 +560,22 @@ access(all) fun testBatchLiquidations() { // 2. postHealth <= 1.05 (liquidationTargetHF default) // 3. postHealth > pre-liq health (position improves) // + // postHealth = (collateral*CF - seize*price*CF) / (debt - repay) + // DEX check: seize < repay / priceRatio where priceRatio = collateralPrice / debtPrice + // // Full liquidations — bring health up to ~1.03-1.04 (as close to 1.05 target as possible): - // pid=WETH: repay 71 FLOW, seize 0.035 WETH + // pid=WETH: repay 71 MOET, seize 0.035 WETH // postHealth = (47.25 - 0.035*787.5) / (90 - 71) = 19.6875/19 ≈ 1.036 // DEX check: 0.035 < 71/1050 = 0.0676 - // pid=USDF: repay 113 FLOW, seize 147 USDF + // pid=USDF: repay 113 MOET, seize 147 USDF // postHealth = (127.5 - 147*0.255) / (200 - 113) = 90.015/87 ≈ 1.034 // DEX check: 147 < 113/0.3 = 376.7 // // Partial liquidations — improve health without reaching 1.05: - // pid=WBTC: repay 4 FLOW, seize 0.00011 WBTC + // pid=WBTC: repay 4 MOET, seize 0.00011 WBTC // postHealth = (7.5 - 0.00011*18750) / (10 - 4) = 5.4375/6 ≈ 0.906 // DEX check: 0.00011 < 4/25000 = 0.00016 - // pid=USDC: repay 12 FLOW, seize 17 USDC + // pid=USDC: repay 12 MOET, seize 17 USDC // postHealth = (34 - 17*0.425) / (40 - 12) = 26.775/28 ≈ 0.956 // DEX check: 17 < 12/0.5 = 24 @@ -577,7 +587,7 @@ access(all) fun testBatchLiquidations() { batchManualLiquidation( pids: batchPids, - debtVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, seizeVaultIdentifiers: batchSeizeTypes, seizeAmounts: batchSeizeAmounts, repayAmounts: batchRepayAmounts, @@ -626,10 +636,11 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// LP setup /////////////////// - // LP deposits 450 FLOW — covers the ~397 FLOW of total borrows with headroom. - log("LP depositing 450 FLOW to shared liquidity pool\n") - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: lpUser, amount: 450.0) - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 450.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + // LP deposits 450 MOET — covers the ~397 MOET of total borrows with headroom. + log("LP depositing 450 MOET to shared liquidity pool\n") + setupMoetVault(lpUser, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: 450.0, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 450.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) //////////// Transfer collateral to user /////////////////// @@ -671,32 +682,32 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// Borrow FLOW from each position /////////////////// // Group A — USDF positions: - // high-risk [0..24]: borrow 7.0 FLOW → health = (10×1.0×0.85)/7.0 = 1.214 - // moderate [25..49]: borrow 6.0 FLOW → health = (10×1.0×0.85)/6.0 = 1.417 - log("Borrowing FLOW from 50 USDF positions...\n") + // high-risk [0..24]: borrow 7.0 MOET → health = (10×1.0×0.85)/7.0 = 1.214 + // moderate [25..49]: borrow 6.0 MOET → health = (10×1.0×0.85)/6.0 = 1.417 + log("Borrowing MOET from 50 USDF positions...\n") for i in InclusiveRange(0, 24) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 7.0, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 7.0, beFailed: false) } for i in InclusiveRange(25, 49) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 6.0, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 6.0, beFailed: false) } // Group B — USDC positions: - // high-risk [50..72]: borrow 1.4 FLOW → health = (2×1.0×0.85)/1.4 = 1.214 - // moderate [73..94]: borrow 1.2 FLOW → health = (2×1.0×0.85)/1.2 = 1.417 - log("Borrowing FLOW from 45 USDC positions...\n") + // high-risk [50..72]: borrow 1.4 MOET → health = (2×1.0×0.85)/1.4 = 1.214 + // moderate [73..94]: borrow 1.2 MOET → health = (2×1.0×0.85)/1.2 = 1.417 + log("Borrowing MOET from 45 USDC positions...\n") for i in InclusiveRange(50, 72) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.4, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1.4, beFailed: false) } for i in InclusiveRange(73, 94) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 1.2, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1.2, beFailed: false) } // Group C — WBTC positions: - // uniform [95..99]: borrow 2.5 FLOW → health = (0.00009×50000×0.75)/2.5 = 1.350 - log("Borrowing FLOW from 5 WBTC positions...\n") + // uniform [95..99]: borrow 2.5 MOET → health = (0.00009×50000×0.75)/2.5 = 1.350 + log("Borrowing MOET from 5 WBTC positions...\n") for i in InclusiveRange(95, 99) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 2.5, beFailed: false) + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 2.5, beFailed: false) } // Confirm all 100 positions are healthy before the crash @@ -707,7 +718,7 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// Simulate 40% price crash across all three collateral types /////////////////// - // USDF/USDC: $1.00 → $0.60 (-40%) | WBTC: $50000 → $30000 (-40%) + // USDF/USDC: 1.00 MOET → 0.60 MOET (-40%) | WBTC: 50000 MOET → 30000 MOET (-40%) // // Health after crash: // USDF high: (10×0.60×0.85)/7.0 = 0.729 USDF mod: (10×0.60×0.85)/6.0 = 0.850 @@ -751,45 +762,46 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// DEX setup /////////////////// - // Three DEX pairs (all source FLOW from MAINNET_PROTOCOL_ACCOUNT's vault): - // USDF→FLOW at priceRatio=0.6 ($0.60 USDF / $1.00 FLOW) - // USDC→FLOW at priceRatio=0.6 ($0.60 USDC / $1.00 FLOW) - // WBTC→FLOW at priceRatio=30000 ($30000 WBTC / $1.00 FLOW) + // Three DEX pairs (all source MOET from MAINNET_PROTOCOL_ACCOUNT's vault): + // USDF→MOET at priceRatio=0.6 (0.60 USDF / 1.00 MOET) + // USDC→MOET at priceRatio=0.6 (0.60 USDC / 1.00 MOET) + // WBTC→MOET at priceRatio=30000 (30000 WBTC / 1.00 MOET) // - // Total DEX FLOW: 25×4.0 + 25×3.0 + 23×0.8 + 22×0.6 + 5×1.18 - // = 100 + 75 + 18.4 + 13.2 + 5.90 = 212.50; transfer 230 for headroom - log("Configuring DEX pairs: USDF→FLOW, USDC→FLOW, WBTC→FLOW\n") - transferTokensWithSetup(tokenIdentifier: MAINNET_FLOW_TOKEN_ID, from: MAINNET_FLOW_HOLDER, to: MAINNET_PROTOCOL_ACCOUNT, amount: 230.0) + // Total DEX MOET: 25×4.0 + 25×3.0 + 23×0.8 + 22×0.6 + 5×1.18 + // = 100 + 75 + 18.4 + 13.2 + 5.90 = 212.50; mint 230 for headroom + log("Configuring DEX pairs: USDF→MOET, USDC→MOET, WBTC→MOET\n") + setupMoetVault(MAINNET_PROTOCOL_ACCOUNT, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: MAINNET_PROTOCOL_ACCOUNT.address, amount: 230.0, beFailed: false) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDF_TOKEN_ID, - outVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, - priceRatio: 0.6 // $0.60 USDF / $1.00 FLOW + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, + priceRatio: 0.6 // 0.60 USDF / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDC_TOKEN_ID, - outVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, - priceRatio: 0.6 // $0.60 USDC / $1.00 FLOW + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, + priceRatio: 0.6 // 0.60 USDC / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WBTC_TOKEN_ID, - outVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH, - priceRatio: 30000.0 // $30000 WBTC / $1.00 FLOW + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, + priceRatio: 30000.0 // 30000 WBTC / 1.00 MOET ) //////////// Build batch parameters (ordered worst health first) /////////////////// // // Seize/repay parameters: - // USDF high [0..24]: seize 4.0 USDF, repay 4.0 FLOW post=1.02, DEX: 4<6.67 - // USDC high [50..72]: seize 0.8 USDC, repay 0.8 FLOW post=1.02, DEX: 0.8<1.33 - // WBTC [95..99]: seize 0.00003 WBTC, repay 1.18 FLOW post=1.023, DEX: 0.00003<0.0000393 - // USDF mod [25..49]: seize 4.0 USDF, repay 3.0 FLOW post=1.02, DEX: 4<5.00 - // USDC mod [73..94]: seize 0.8 USDC, repay 0.6 FLOW post=1.02, DEX: 0.8<1.00 + // USDF high [0..24]: seize 4.0 USDF, repay 4.0 MOET post=1.02, DEX: 4<6.67 + // USDC high [50..72]: seize 0.8 USDC, repay 0.8 MOET post=1.02, DEX: 0.8<1.33 + // WBTC [95..99]: seize 0.00003 WBTC, repay 1.18 MOET post=1.023, DEX: 0.00003<0.0000393 + // USDF mod [25..49]: seize 4.0 USDF, repay 3.0 MOET post=1.02, DEX: 4<5.00 + // USDC mod [73..94]: seize 0.8 USDC, repay 0.6 MOET post=1.02, DEX: 0.8<1.00 var batchPids: [UInt64] = [] var batchSeize: [String] = [] var batchAmounts: [UFix64] = [] @@ -835,12 +847,17 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// Batch liquidation — 100 positions in chunks of 10 /////////////////// + // Setup liquidator vaults for seized collateral tokens (required to receive seized amounts). + transferTokensWithSetup(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: liquidator, amount: 1.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_USDC_TOKEN_ID, from: MAINNET_USDC_HOLDER, to: liquidator, amount: 1.0) + transferTokensWithSetup(tokenIdentifier: MAINNET_WBTC_TOKEN_ID, from: MAINNET_WBTC_HOLDER, to: liquidator, amount: 0.00001) + // Split into chunks of 10 to stay within the computation limit (single tx of 100 exceeds it). - // DEX sources FLOW from MAINNET_PROTOCOL_ACCOUNT's vault; liquidator needs no tokens upfront. + // DEX sources MOET from MAINNET_PROTOCOL_ACCOUNT's vault; liquidator receives seized collateral. log("Liquidating all 100 positions via DEX in chunks of 10...\n") batchLiquidateViaMockDex( pids: batchPids, - debtVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, seizeVaultIdentifiers: batchSeize, seizeAmounts: batchAmounts, repayAmounts: batchRepay, @@ -875,7 +892,7 @@ access(all) fun testMassUnhealthyLiquidations() { } // Protocol solvency: FLOW reserve must remain positive after mass liquidation - let reserveBalance = getReserveBalance(vaultIdentifier: MAINNET_FLOW_TOKEN_ID) - log("Protocol FLOW reserve after mass liquidation: \(reserveBalance)\n") - Test.assert(reserveBalance > 0.0, message: "Protocol must remain solvent (positive FLOW reserve) after mass liquidation") + let reserveBalance = getReserveBalance(vaultIdentifier: MAINNET_MOET_TOKEN_ID) + log("Protocol MOET reserve after mass liquidation: \(reserveBalance)\n") + Test.assert(reserveBalance > 0.0, message: "Protocol must remain solvent (positive MOET reserve) after mass liquidation") } diff --git a/cadence/scripts/flow-alp/get_oracle_price.cdc b/cadence/tests/scripts/get_oracle_price.cdc similarity index 100% rename from cadence/scripts/flow-alp/get_oracle_price.cdc rename to cadence/tests/scripts/get_oracle_price.cdc diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 272c9077..1a1c2c0f 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -426,7 +426,7 @@ fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, pri access(all) fun getOraclePrice(tokenIdentifier: String): UFix64 { let result = Test.executeScript( - Test.readFile("../scripts/flow-alp/get_oracle_price.cdc"), + Test.readFile("./scripts/get_oracle_price.cdc"), [tokenIdentifier] ) diff --git a/cadence/tests/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc b/cadence/tests/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc index 389477cb..34c61486 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc @@ -18,21 +18,24 @@ import "MockDexSwapper" /// repayAmounts: Array of debt amounts to repay for each position (sourced from the DEX) transaction( pids: [UInt64], - debtVaultIdentifier: String, + repayVaultIdentifier: String, seizeVaultIdentifiers: [String], seizeAmounts: [UFix64], repayAmounts: [UFix64] ) { let pool: &FlowALPv0.Pool let debtType: Type + let signerAccount: auth(BorrowValue) &Account - prepare(signer: &Account) { + prepare(signer: auth(BorrowValue) &Account) { let protocolAddress = Type<@FlowALPv0.Pool>().address! self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolPublicPath)") - self.debtType = CompositeType(debtVaultIdentifier) - ?? panic("Invalid debtVaultIdentifier: \(debtVaultIdentifier)") + self.debtType = CompositeType(repayVaultIdentifier) + ?? panic("Invalid debtVaultIdentifier: \(repayVaultIdentifier)") + + self.signerAccount = signer } execute { @@ -55,7 +58,7 @@ transaction( // Retrieve the stored MockDexSwapper for this collateral → debt pair. // The swapper's vaultSource (protocolAccount's vault) provides the debt tokens. let swapper = MockDexSwapper.getSwapper(inType: seizeType, outType: self.debtType) - ?? panic("No MockDexSwapper configured for \(seizeVaultIdentifier) -> \(debtVaultIdentifier)") + ?? panic("No MockDexSwapper configured for \(seizeVaultIdentifier) -> \(repayVaultIdentifier)") // Build an exact quote for the repayAmount we need from the swapper's vaultSource let swapQuote = MockDexSwapper.BasicQuote( @@ -87,7 +90,11 @@ transaction( ) totalRepaid = totalRepaid + repayAmount - destroy seizedVault + + // Deposit seized collateral back to liquidator + let liquidatorVault = self.signerAccount.storage.borrow<&{FungibleToken.Vault}>(from: seizeVaultData.storagePath) + ?? panic("No vault at \(seizeVaultData.storagePath) to deposit seized collateral") + liquidatorVault.deposit(from: <-seizedVault) } log("Batch DEX liquidation completed: \(numPositions) positions, total repaid: \(totalRepaid)") diff --git a/cadence/tests/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc b/cadence/tests/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc index a5f7933d..e57882bc 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc +++ b/cadence/tests/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc @@ -7,36 +7,39 @@ import "FlowALPv0" /// Batch liquidate multiple positions in a single transaction /// /// pids: Array of position IDs to liquidate -/// debtVaultIdentifier: e.g., Type<@FlowToken.Vault>().identifier +/// repaymentVaultIdentifier: e.g., Type<@FlowToken.Vault>().identifier /// seizeVaultIdentifiers: Array of collateral vault identifiers to seize /// seizeAmounts: Array of max seize amounts for each position /// repayAmounts: Array of repay amounts for each position transaction( pids: [UInt64], - debtVaultIdentifier: String, + repaymentVaultIdentifier: String, seizeVaultIdentifiers: [String], seizeAmounts: [UFix64], repayAmounts: [UFix64] ) { let pool: &FlowALPv0.Pool - let debtType: Type - let debtVaultRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + let repaymentType: Type + let repaymentVaultRef: auth(FungibleToken.Withdraw) &{FungibleToken.Vault} + let signerAccount: auth(BorrowValue) &Account prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account) { + self.signerAccount = signer + let protocolAddress = Type<@FlowALPv0.Pool>().address! self.pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) ?? panic("Could not borrow Pool at \(FlowALPv0.PoolPublicPath)") - self.debtType = CompositeType(debtVaultIdentifier) ?? panic("Invalid debtVaultIdentifier: \(debtVaultIdentifier)") + self.repaymentType = CompositeType(repaymentVaultIdentifier) ?? panic("Invalid repaymentVaultIdentifier: \(repaymentVaultIdentifier)") - let debtVaultData = MetadataViews.resolveContractViewFromTypeIdentifier( - resourceTypeIdentifier: debtVaultIdentifier, + let repaymentVaultData = MetadataViews.resolveContractViewFromTypeIdentifier( + resourceTypeIdentifier: repaymentVaultIdentifier, viewType: Type() ) as? FungibleTokenMetadataViews.FTVaultData - ?? panic("Could not construct valid FT type and view from identifier \(debtVaultIdentifier)") + ?? panic("Could not construct valid FT type and view from identifier \(repaymentVaultIdentifier)") - self.debtVaultRef = signer.storage.borrow(from: debtVaultData.storagePath) - ?? panic("no debt vault in storage at path \(debtVaultData.storagePath)") + self.repaymentVaultRef = signer.storage.borrow(from: repaymentVaultData.storagePath) + ?? panic("no repayment vault in storage at path \(repaymentVaultData.storagePath)") } execute { @@ -56,14 +59,14 @@ transaction( let seizeType = CompositeType(seizeVaultIdentifier) ?? panic("Invalid seizeVaultIdentifier: \(seizeVaultIdentifier)") - assert(self.debtVaultRef.balance >= repayAmount, - message: "Insufficient debt token balance for position \(pid)") + assert(self.repaymentVaultRef.balance >= repayAmount, + message: "Insufficient repayment token balance for position \(pid)") - let repay <- self.debtVaultRef.withdraw(amount: repayAmount) + let repay <- self.repaymentVaultRef.withdraw(amount: repayAmount) let seizedVault <- self.pool.manualLiquidation( pid: pid, - debtType: self.debtType, + debtType: self.repaymentType, seizeType: seizeType, seizeAmount: seizeAmount, repayment: <-repay @@ -72,9 +75,14 @@ transaction( totalRepaid = totalRepaid + repayAmount // Deposit seized collateral back to liquidator - // For simplicity, we'll just destroy it in this test transaction - // In production, you'd want to properly handle the seized collateral - destroy seizedVault + let seizeVaultData = MetadataViews.resolveContractViewFromTypeIdentifier( + resourceTypeIdentifier: seizeVaultIdentifier, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Could not resolve FTVaultData for \(seizeVaultIdentifier)") + let liquidatorVault = self.signerAccount.storage.borrow<&{FungibleToken.Vault}>(from: seizeVaultData.storagePath) + ?? panic("No vault at \(seizeVaultData.storagePath) to deposit seized collateral") + liquidatorVault.deposit(from: <-seizedVault) } log("Batch liquidation completed: \(numPositions) positions, total repaid: \(totalRepaid)") From 828600abeaf69f9b2dbe0a93575af74546672d6b Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Tue, 10 Mar 2026 08:53:04 +0100 Subject: [PATCH 15/25] use constants instead of magic numbers in fork_multiple_positions_per_user_test --- .../fork_multiple_positions_per_user_test.cdc | 317 +++++++++++------- 1 file changed, 203 insertions(+), 114 deletions(-) diff --git a/cadence/tests/fork_multiple_positions_per_user_test.cdc b/cadence/tests/fork_multiple_positions_per_user_test.cdc index aabf7d4c..971873e4 100644 --- a/cadence/tests/fork_multiple_positions_per_user_test.cdc +++ b/cadence/tests/fork_multiple_positions_per_user_test.cdc @@ -19,6 +19,27 @@ access(all) let MAINNET_USDC_HOLDER = Test.getAccount(MAINNET_USDC_HOLDER_ADDRES access(all) var snapshot: UInt64 = 0 +// ─── Protocol constants (set once in setup(), referenced in formula comments) ── + +// Initial oracle prices +access(all) let PRICE_FLOW = 1.0 +access(all) let PRICE_USDC = 1.0 +access(all) let PRICE_USDF = 1.0 +access(all) let PRICE_WETH = 3500.0 +access(all) let PRICE_WBTC = 50000.0 +access(all) let PRICE_MOET = 1.0 + +// Collateral factors +access(all) let CF_FLOW = 0.80 +access(all) let CF_USDC = 0.85 +access(all) let CF_USDF = 0.85 +access(all) let CF_WETH = 0.75 +access(all) let CF_WBTC = 0.75 + +// Minimum token balance per position +access(all) let MIN_BAL_WETH = 0.01 +access(all) let MIN_BAL_WBTC = 0.00001 + access(all) fun safeReset() { let cur = getCurrentBlockHeight() @@ -33,18 +54,18 @@ access(all) fun setup() { createAndStorePool(signer: MAINNET_PROTOCOL_ACCOUNT, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false) // Set oracle prices - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: 1.0) - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 1.0) - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 3500.0) - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: 50000.0) - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_MOET_TOKEN_ID, price: 1.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: PRICE_FLOW) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: PRICE_USDC) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: PRICE_USDF) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: PRICE_WETH) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: PRICE_WBTC) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_MOET_TOKEN_ID, price: PRICE_MOET) // Add multiple token types as supported collateral (FLOW, USDC, USDF, WETH, WBTC) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, - collateralFactor: 0.8, + collateralFactor: CF_FLOW, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 @@ -53,7 +74,7 @@ access(all) fun setup() { addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_USDC_TOKEN_ID, - collateralFactor: 0.85, + collateralFactor: CF_USDC, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 @@ -62,7 +83,7 @@ access(all) fun setup() { addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, - collateralFactor: 0.85, + collateralFactor: CF_USDF, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 @@ -71,25 +92,25 @@ access(all) fun setup() { addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, - collateralFactor: 0.75, + collateralFactor: CF_WETH, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - // Set minimum deposit for WETH to 0.01 (since holder only has 0.07032) - setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: 0.01) + // Set minimum deposit for WETH to MIN_BAL_WETH (since holder only has 0.07032) + setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: MIN_BAL_WETH) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_ID, - collateralFactor: 0.75, + collateralFactor: CF_WBTC, borrowFactor: 1.0, depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - // Set minimum deposit for WBTC to 0.00001 (since holder only has 0.0005) - setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_ID, minimum: 0.00001) + // Set minimum deposit for WBTC to MIN_BAL_WBTC (since holder only has 0.0005) + setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_ID, minimum: MIN_BAL_WBTC) snapshot = getCurrentBlockHeight() } @@ -141,16 +162,23 @@ access(all) fun testMultiplePositionsPerUser() { // - usdcHolder: 97 USDC x 1 = 97 MOET // - wethHolder: 0.07032 WETH x 3500 = 246.12 MOET // - wbtcHolder: 0.0005 WBTC x 50000 = 25 MOET + // + // health = col * PRICE * CF / debt + let flowCol = 500.0; let flowDebt = 100.0 // health = 500.0 * PRICE_FLOW * CF_FLOW / 100.0 = 4.000 + let usdfCol = 1500.0; let usdfDebt = 150.0 // health = 1500.0 * PRICE_USDF * CF_USDF / 150.0 = 8.500 + let usdcCol = 10.0; let usdcDebt = 5.0 // health = 10.0 * PRICE_USDC * CF_USDC / 5.0 = 1.700 + let wethCol = 0.05; let wethDebt = 50.0 // health = 0.05 * PRICE_WETH * CF_WETH / 50.0 = 2.625 + let wbtcCol = 0.0004; let wbtcDebt = 8.0 // health = 0.0004 * PRICE_WBTC * CF_WBTC / 8.0 = 1.875 let positions = [ - {"type": MAINNET_FLOW_TOKEN_ID, "amount": 500.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": MAINNET_FLOW_HOLDER}, - {"type": MAINNET_USDF_TOKEN_ID, "amount": 1500.0, "storagePath": MAINNET_USDF_STORAGE_PATH, "name": "USDF", "holder": MAINNET_USDF_HOLDER}, - {"type": MAINNET_USDC_TOKEN_ID, "amount": 10.0, "storagePath": MAINNET_USDC_STORAGE_PATH, "name": "USDC", "holder": MAINNET_USDC_HOLDER}, - {"type": MAINNET_WETH_TOKEN_ID, "amount": 0.05, "storagePath": MAINNET_WETH_STORAGE_PATH, "name": "WETH", "holder": MAINNET_WETH_HOLDER}, - {"type": MAINNET_WBTC_TOKEN_ID, "amount": 0.0004, "storagePath": MAINNET_WBTC_STORAGE_PATH, "name": "WBTC", "holder": MAINNET_WBTC_HOLDER} + {"type": MAINNET_FLOW_TOKEN_ID, "amount": flowCol, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": MAINNET_FLOW_HOLDER}, + {"type": MAINNET_USDF_TOKEN_ID, "amount": usdfCol, "storagePath": MAINNET_USDF_STORAGE_PATH, "name": "USDF", "holder": MAINNET_USDF_HOLDER}, + {"type": MAINNET_USDC_TOKEN_ID, "amount": usdcCol, "storagePath": MAINNET_USDC_STORAGE_PATH, "name": "USDC", "holder": MAINNET_USDC_HOLDER}, + {"type": MAINNET_WETH_TOKEN_ID, "amount": wethCol, "storagePath": MAINNET_WETH_STORAGE_PATH, "name": "WETH", "holder": MAINNET_WETH_HOLDER}, + {"type": MAINNET_WBTC_TOKEN_ID, "amount": wbtcCol, "storagePath": MAINNET_WBTC_STORAGE_PATH, "name": "WBTC", "holder": MAINNET_WBTC_HOLDER} ] - let debts = [100.0, 150.0, 5.0, 50.0, 8.0] + let debts = [flowDebt, usdfDebt, usdcDebt, wethDebt, wbtcDebt] var userPids: [UInt64] = [] @@ -340,8 +368,9 @@ access(all) fun testPositionInteractionsSharedLiquidity() { let healthB_before_priceChange = getPositionHealth(pid: positionB_id, beFailed: false) log(" Position B health: \(healthB_before_priceChange)") - // Crash USDC price (Position A's collateral) from 1.0 MOET to 0.5 MOET - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: 0.5) + // Crash USDC price (Position A's collateral) −50% + let usdcCrashPrice = 0.5 // PRICE_USDC * 0.50 + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: usdcCrashPrice) let healthA_after_crash = getPositionHealth(pid: positionA_id, beFailed: false) log(" Position A health after price crash: \(healthA_after_crash)\n") @@ -418,11 +447,32 @@ access(all) fun testBatchLiquidations() { let lpUser = Test.createAccount() let user = Test.createAccount() - // LP deposits 600 MOET to provide borrowing liquidity - // (total borrows = 200+90+40+10+80 = 420 MOET < 600) + // Collateral deposits and target debts (health = col * PRICE * CF / debt): + let usdfCol = 500.0; let usdfDebt = 200.0 // health = 500.0 * PRICE_USDF * CF_USDF / 200.0 = 2.125 + let wethCol = 0.06; let wethDebt = 90.0 // health = 0.06 * PRICE_WETH * CF_WETH / 90.0 = 1.750 + let usdcCol = 80.0; let usdcDebt = 40.0 // health = 80.0 * PRICE_USDC * CF_USDC / 40.0 = 1.700 + let wbtcCol = 0.0004; let wbtcDebt = 10.0 // health = 0.0004 * PRICE_WBTC * CF_WBTC / 10.0 = 1.500 + let flowCol = 200.0; let flowDebt = 80.0 // health = 200.0 * PRICE_FLOW * CF_FLOW / 80.0 = 2.000 + + // Crashed prices (−70% for USDF/WETH, −50% for USDC/WBTC; FLOW unchanged) + let usdfCrashPrice = 0.3 // PRICE_USDF * 0.30 + let wethCrashPrice = 1050.0 // PRICE_WETH * 0.30 + let usdcCrashPrice = 0.5 // PRICE_USDC * 0.50 + let wbtcCrashPrice = 25000.0 // PRICE_WBTC * 0.50 + // DEX priceRatio == crashed oracle price (required to pass deviation check) + + // Seize / repay per position (postHealth = (col*CF - seize*P*CF) / (debt - repay)) + let usdfSeize = 147.0; let usdfRepay = 113.0 // postHealth ≈ 1.034 (full) + let wethSeize = 0.035; let wethRepay = 71.0 // postHealth ≈ 1.036 (full) + let usdcSeize = 17.0; let usdcRepay = 12.0 // postHealth ≈ 0.956 (partial) + let wbtcSeize = 0.00011; let wbtcRepay = 4.0 // postHealth ≈ 0.906 (partial) + + // LP deposits lpLiquidity MOET to provide borrowing liquidity + // (total borrows = usdfDebt+wethDebt+usdcDebt+wbtcDebt+flowDebt = 420 MOET < lpLiquidity) + let lpLiquidity = 600.0 setupMoetVault(lpUser, beFailed: false) - mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: 600.0, beFailed: false) - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 600.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: lpLiquidity, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: lpLiquidity, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // 5 positions with distinct collateral types: // @@ -437,11 +487,11 @@ access(all) fun testBatchLiquidations() { log("Creating 5 positions with different collateral types\n") let positions = [ - {"type": MAINNET_USDF_TOKEN_ID, "amount": 500.0, "storagePath": MAINNET_USDF_STORAGE_PATH, "name": "USDF", "holder": MAINNET_USDF_HOLDER, "borrow": 200.0}, - {"type": MAINNET_WETH_TOKEN_ID, "amount": 0.06, "storagePath": MAINNET_WETH_STORAGE_PATH, "name": "WETH", "holder": MAINNET_WETH_HOLDER, "borrow": 90.0}, - {"type": MAINNET_USDC_TOKEN_ID, "amount": 80.0, "storagePath": MAINNET_USDC_STORAGE_PATH, "name": "USDC", "holder": MAINNET_USDC_HOLDER, "borrow": 40.0}, - {"type": MAINNET_WBTC_TOKEN_ID, "amount": 0.0004, "storagePath": MAINNET_WBTC_STORAGE_PATH, "name": "WBTC", "holder": MAINNET_WBTC_HOLDER, "borrow": 10.0}, - {"type": MAINNET_FLOW_TOKEN_ID, "amount": 200.0, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": MAINNET_FLOW_HOLDER, "borrow": 80.0} + {"type": MAINNET_USDF_TOKEN_ID, "amount": usdfCol, "storagePath": MAINNET_USDF_STORAGE_PATH, "name": "USDF", "holder": MAINNET_USDF_HOLDER, "borrow": usdfDebt}, + {"type": MAINNET_WETH_TOKEN_ID, "amount": wethCol, "storagePath": MAINNET_WETH_STORAGE_PATH, "name": "WETH", "holder": MAINNET_WETH_HOLDER, "borrow": wethDebt}, + {"type": MAINNET_USDC_TOKEN_ID, "amount": usdcCol, "storagePath": MAINNET_USDC_STORAGE_PATH, "name": "USDC", "holder": MAINNET_USDC_HOLDER, "borrow": usdcDebt}, + {"type": MAINNET_WBTC_TOKEN_ID, "amount": wbtcCol, "storagePath": MAINNET_WBTC_STORAGE_PATH, "name": "WBTC", "holder": MAINNET_WBTC_HOLDER, "borrow": wbtcDebt}, + {"type": MAINNET_FLOW_TOKEN_ID, "amount": flowCol, "storagePath": FLOW_VAULT_STORAGE_PATH, "name": "FLOW", "holder": MAINNET_FLOW_HOLDER, "borrow": flowDebt} ] var userPids: [UInt64] = [] @@ -474,10 +524,10 @@ access(all) fun testBatchLiquidations() { // Crash collateral prices. FLOW stays at 1.0 so userPids[4] stays healthy. log("\nCrashing collateral prices to trigger liquidations\n") - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 0.3) // -70% - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 1050.0) // -70% - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: 0.5) // -50% - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: 25000.0) // -50% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: usdfCrashPrice) // -70% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: wethCrashPrice) // -70% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: usdcCrashPrice) // -50% + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: wbtcCrashPrice) // -50% log("\nPosition health after price crash:\n") for i in InclusiveRange(0, 4) { @@ -512,28 +562,28 @@ access(all) fun testBatchLiquidations() { inVaultIdentifier: MAINNET_USDF_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, - priceRatio: 0.3 // 0.30 USDF / 1.00 MOET + priceRatio: usdfCrashPrice // usdfCrashPrice USDF / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WETH_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, - priceRatio: 1050.0 // 1050 WETH / 1.00 MOET + priceRatio: wethCrashPrice // wethCrashPrice WETH / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDC_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, - priceRatio: 0.5 // 0.50 USDC / 1.00 MOET + priceRatio: usdcCrashPrice // usdcCrashPrice USDC / 1.00 MOET ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WBTC_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, - priceRatio: 25000.0 // 25000 WBTC / 1.00 MOET + priceRatio: wbtcCrashPrice // wbtcCrashPrice WBTC / 1.00 MOET ) // Liquidator setup: mint MOET for debt repayment (total needed: 71+113+4+12 = 200 MOET) @@ -582,8 +632,8 @@ access(all) fun testBatchLiquidations() { log("\nExecuting batch liquidation of 4 positions (2 full, 2 partial) in SINGLE transaction...\n") let batchPids = [userPids[0], userPids[1], userPids[2], userPids[3] ] let batchSeizeTypes = [MAINNET_USDF_TOKEN_ID, MAINNET_WETH_TOKEN_ID, MAINNET_USDC_TOKEN_ID, MAINNET_WBTC_TOKEN_ID] - let batchSeizeAmounts = [147.0, 0.035, 17.0, 0.00011 ] - let batchRepayAmounts = [113.0, 71.0, 12.0, 4.0 ] + let batchSeizeAmounts = [usdfSeize, wethSeize, usdcSeize, wbtcSeize] + let batchRepayAmounts = [usdfRepay, wethRepay, usdcRepay, wbtcRepay] batchManualLiquidation( pids: batchPids, @@ -634,19 +684,56 @@ access(all) fun testMassUnhealthyLiquidations() { let user = Test.createAccount() let liquidator = Test.createAccount() + // ── Group index ranges ────────────────────────────────────────────────────── + // Group A — USDF: indices 0..49 (50 positions) + let usdfHighStart = 0; let usdfHighEnd = 24 // high-risk: 25 positions + let usdfModStart = 25; let usdfModEnd = 49 // moderate: 25 positions + // Group B — USDC: indices 50..94 (45 positions) + let usdcHighStart = 50; let usdcHighEnd = 72 // high-risk: 23 positions + let usdcModStart = 73; let usdcModEnd = 94 // moderate: 22 positions + // Group C — WBTC: indices 95..99 (5 positions) + let wbtcStart = 95; let wbtcEnd = 99 + + // Collateral per position (health = colPerPos * PRICE * CF / debt) + let usdfColPerPos = 10.0 // 50 × usdfColPerPos = 500 USDF transferred + let usdcColPerPos = 2.0 // 45 × usdcColPerPos = 90 USDC transferred + let wbtcColPerPos = 0.00009 // 5 × wbtcColPerPos = 0.00045 WBTC transferred + + // Borrow amounts per position + let usdfHighDebt = 7.0 // health = usdfColPerPos * PRICE_USDF * CF_USDF / usdfHighDebt = 1.214 + let usdfModDebt = 6.0 // health = usdfColPerPos * PRICE_USDF * CF_USDF / usdfModDebt = 1.417 + let usdcHighDebt = 1.4 // health = usdcColPerPos * PRICE_USDC * CF_USDC / usdcHighDebt = 1.214 + let usdcModDebt = 1.2 // health = usdcColPerPos * PRICE_USDC * CF_USDC / usdcModDebt = 1.417 + let wbtcDebt = 2.5 // health = wbtcColPerPos * PRICE_WBTC * CF_WBTC / wbtcDebt = 1.350 + + // Crashed prices (−40% across all three collateral types) + let usdfCrashPrice = 0.6 // PRICE_USDF * 0.60 + let usdcCrashPrice = 0.6 // PRICE_USDC * 0.60 + let wbtcCrashPrice = 30000.0 // PRICE_WBTC * 0.60 + + // Seize / repay per position (postHealth = (col*CF - seize*P*CF) / (debt - repay)) + let usdfHighSeize = 4.0; let usdfHighRepay = 4.0 // postHealth ≈ 1.02 + let usdcHighSeize = 0.8; let usdcHighRepay = 0.8 // postHealth ≈ 1.02 + let wbtcSeize = 0.00003; let wbtcRepay = 1.18 // postHealth ≈ 1.023 + let usdfModSeize = 4.0; let usdfModRepay = 3.0 // postHealth ≈ 1.02 + let usdcModSeize = 0.8; let usdcModRepay = 0.6 // postHealth ≈ 1.02 + + let batchChunkSize = 10 + //////////// LP setup /////////////////// - // LP deposits 450 MOET — covers the ~397 MOET of total borrows with headroom. - log("LP depositing 450 MOET to shared liquidity pool\n") + // LP deposits lpLiquidity MOET — covers the ~397 MOET of total borrows with headroom. + let lpLiquidity = 450.0 + log("LP depositing \(lpLiquidity) MOET to shared liquidity pool\n") setupMoetVault(lpUser, beFailed: false) - mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: 450.0, beFailed: false) - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: 450.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: lpUser.address, amount: lpLiquidity, beFailed: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: lpUser, amount: lpLiquidity, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) //////////// Transfer collateral to user /////////////////// - // Group A: 50 positions × 10 USDF = 500 USDF - // Group B: 45 positions × 2 USDC = 90 USDC - // Group C: 5 positions × 0.00009 WBTC = 0.00045 WBTC + // Group A: 50 positions × usdfColPerPos = 500 USDF + // Group B: 45 positions × usdcColPerPos = 90 USDC + // Group C: 5 positions × wbtcColPerPos = 0.00045 WBTC log("Transferring collateral: 500 USDF + 90 USDC + 0.00045 WBTC\n") transferTokensWithSetup(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: user, amount: 500.0) transferTokensWithSetup(tokenIdentifier: MAINNET_USDC_TOKEN_ID, from: MAINNET_USDC_HOLDER, to: user, amount: 90.0) @@ -657,23 +744,23 @@ access(all) fun testMassUnhealthyLiquidations() { var allPids: [UInt64] = [] // Group A — 50 USDF positions - log("Creating 50 USDF positions (10 USDF each)...\n") - for i in InclusiveRange(0, 49) { - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 10.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) + log("Creating 50 USDF positions (\(usdfColPerPos) USDF each)...\n") + for i in InclusiveRange(usdfHighStart, usdfModEnd) { + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: usdfColPerPos, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) allPids.append(getLastPositionId()) } // Group B — 45 USDC positions - log("Creating 45 USDC positions (2 USDC each)...\n") - for i in InclusiveRange(50, 94) { - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 2.0, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) + log("Creating 45 USDC positions (\(usdcColPerPos) USDC each)...\n") + for i in InclusiveRange(usdcHighStart, usdcModEnd) { + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: usdcColPerPos, vaultStoragePath: MAINNET_USDC_STORAGE_PATH, pushToDrawDownSink: false) allPids.append(getLastPositionId()) } // Group C — 5 WBTC positions - log("Creating 5 WBTC positions (0.00009 WBTC each)...\n") - for i in InclusiveRange(95, 99) { - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 0.00009, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, pushToDrawDownSink: false) + log("Creating 5 WBTC positions (\(wbtcColPerPos) WBTC each)...\n") + for i in InclusiveRange(wbtcStart, wbtcEnd) { + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: wbtcColPerPos, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, pushToDrawDownSink: false) allPids.append(getLastPositionId()) } @@ -682,32 +769,32 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// Borrow FLOW from each position /////////////////// // Group A — USDF positions: - // high-risk [0..24]: borrow 7.0 MOET → health = (10×1.0×0.85)/7.0 = 1.214 - // moderate [25..49]: borrow 6.0 MOET → health = (10×1.0×0.85)/6.0 = 1.417 + // high-risk [usdfHighStart..usdfHighEnd]: borrow usdfHighDebt → health = 1.214 + // moderate [usdfModStart..usdfModEnd]: borrow usdfModDebt → health = 1.417 log("Borrowing MOET from 50 USDF positions...\n") - for i in InclusiveRange(0, 24) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 7.0, beFailed: false) + for i in InclusiveRange(usdfHighStart, usdfHighEnd) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: usdfHighDebt, beFailed: false) } - for i in InclusiveRange(25, 49) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 6.0, beFailed: false) + for i in InclusiveRange(usdfModStart, usdfModEnd) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: usdfModDebt, beFailed: false) } // Group B — USDC positions: - // high-risk [50..72]: borrow 1.4 MOET → health = (2×1.0×0.85)/1.4 = 1.214 - // moderate [73..94]: borrow 1.2 MOET → health = (2×1.0×0.85)/1.2 = 1.417 + // high-risk [usdcHighStart..usdcHighEnd]: borrow usdcHighDebt → health = 1.214 + // moderate [usdcModStart..usdcModEnd]: borrow usdcModDebt → health = 1.417 log("Borrowing MOET from 45 USDC positions...\n") - for i in InclusiveRange(50, 72) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1.4, beFailed: false) + for i in InclusiveRange(usdcHighStart, usdcHighEnd) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: usdcHighDebt, beFailed: false) } - for i in InclusiveRange(73, 94) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1.2, beFailed: false) + for i in InclusiveRange(usdcModStart, usdcModEnd) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: usdcModDebt, beFailed: false) } // Group C — WBTC positions: - // uniform [95..99]: borrow 2.5 MOET → health = (0.00009×50000×0.75)/2.5 = 1.350 + // uniform [wbtcStart..wbtcEnd]: borrow wbtcDebt → health = 1.350 log("Borrowing MOET from 5 WBTC positions...\n") - for i in InclusiveRange(95, 99) { - borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 2.5, beFailed: false) + for i in InclusiveRange(wbtcStart, wbtcEnd) { + borrowFromPosition(signer: user, positionId: allPids[i], tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: wbtcDebt, beFailed: false) } // Confirm all 100 positions are healthy before the crash @@ -718,16 +805,18 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// Simulate 40% price crash across all three collateral types /////////////////// - // USDF/USDC: 1.00 MOET → 0.60 MOET (-40%) | WBTC: 50000 MOET → 30000 MOET (-40%) + // USDF/USDC: PRICE_USDF → usdfCrashPrice (-40%) | WBTC: PRICE_WBTC → wbtcCrashPrice (-40%) // // Health after crash: - // USDF high: (10×0.60×0.85)/7.0 = 0.729 USDF mod: (10×0.60×0.85)/6.0 = 0.850 - // USDC high: (2×0.60×0.85)/1.4 = 0.729 USDC mod: (2×0.60×0.85)/1.2 = 0.850 - // WBTC: (0.00009×30000×0.75)/2.5 = 0.810 + // USDF high: (usdfColPerPos×usdfCrashPrice×CF_USDF)/usdfHighDebt = 0.729 + // USDF mod: (usdfColPerPos×usdfCrashPrice×CF_USDF)/usdfModDebt = 0.850 + // USDC high: (usdcColPerPos×usdcCrashPrice×CF_USDC)/usdcHighDebt = 0.729 + // USDC mod: (usdcColPerPos×usdcCrashPrice×CF_USDC)/usdcModDebt = 0.850 + // WBTC: (wbtcColPerPos×wbtcCrashPrice×CF_WBTC)/wbtcDebt = 0.810 log("All three collateral types crash 40% simultaneously\n") - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 0.6) - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: 0.6) - setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: 30000.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: usdfCrashPrice) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDC_TOKEN_ID, price: usdcCrashPrice) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: wbtcCrashPrice) // Capture post-crash health by token type and verify all positions are unhealthy var usdfHealths: [UFix128] = [] @@ -763,11 +852,11 @@ access(all) fun testMassUnhealthyLiquidations() { //////////// DEX setup /////////////////// // Three DEX pairs (all source MOET from MAINNET_PROTOCOL_ACCOUNT's vault): - // USDF→MOET at priceRatio=0.6 (0.60 USDF / 1.00 MOET) - // USDC→MOET at priceRatio=0.6 (0.60 USDC / 1.00 MOET) - // WBTC→MOET at priceRatio=30000 (30000 WBTC / 1.00 MOET) + // USDF→MOET at priceRatio=usdfCrashPrice + // USDC→MOET at priceRatio=usdcCrashPrice + // WBTC→MOET at priceRatio=wbtcCrashPrice // - // Total DEX MOET: 25×4.0 + 25×3.0 + 23×0.8 + 22×0.6 + 5×1.18 + // Total DEX MOET: 25×usdfHighRepay + 25×usdfModRepay + 23×usdcHighRepay + 22×usdcModRepay + 5×wbtcRepay // = 100 + 75 + 18.4 + 13.2 + 5.90 = 212.50; mint 230 for headroom log("Configuring DEX pairs: USDF→MOET, USDC→MOET, WBTC→MOET\n") setupMoetVault(MAINNET_PROTOCOL_ACCOUNT, beFailed: false) @@ -777,70 +866,70 @@ access(all) fun testMassUnhealthyLiquidations() { inVaultIdentifier: MAINNET_USDF_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, - priceRatio: 0.6 // 0.60 USDF / 1.00 MOET + priceRatio: usdfCrashPrice ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_USDC_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, - priceRatio: 0.6 // 0.60 USDC / 1.00 MOET + priceRatio: usdcCrashPrice ) setMockDexPriceForPair( signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_WBTC_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH, - priceRatio: 30000.0 // 30000 WBTC / 1.00 MOET + priceRatio: wbtcCrashPrice ) //////////// Build batch parameters (ordered worst health first) /////////////////// // - // Seize/repay parameters: - // USDF high [0..24]: seize 4.0 USDF, repay 4.0 MOET post=1.02, DEX: 4<6.67 - // USDC high [50..72]: seize 0.8 USDC, repay 0.8 MOET post=1.02, DEX: 0.8<1.33 - // WBTC [95..99]: seize 0.00003 WBTC, repay 1.18 MOET post=1.023, DEX: 0.00003<0.0000393 - // USDF mod [25..49]: seize 4.0 USDF, repay 3.0 MOET post=1.02, DEX: 4<5.00 - // USDC mod [73..94]: seize 0.8 USDC, repay 0.6 MOET post=1.02, DEX: 0.8<1.00 + // Seize/repay parameters (ordered worst health first): + // USDF high [usdfHighStart..usdfHighEnd]: seize usdfHighSeize, repay usdfHighRepay post=1.02 + // USDC high [usdcHighStart..usdcHighEnd]: seize usdcHighSeize, repay usdcHighRepay post=1.02 + // WBTC [wbtcStart..wbtcEnd]: seize wbtcSeize, repay wbtcRepay post=1.023 + // USDF mod [usdfModStart..usdfModEnd]: seize usdfModSeize, repay usdfModRepay post=1.02 + // USDC mod [usdcModStart..usdcModEnd]: seize usdcModSeize, repay usdcModRepay post=1.02 var batchPids: [UInt64] = [] var batchSeize: [String] = [] var batchAmounts: [UFix64] = [] var batchRepay: [UFix64] = [] - // USDF high-risk [0..24] - for i in InclusiveRange(0, 24) { + // USDF high-risk [usdfHighStart..usdfHighEnd] + for i in InclusiveRange(usdfHighStart, usdfHighEnd) { batchPids.append(allPids[i]) batchSeize.append(MAINNET_USDF_TOKEN_ID) - batchAmounts.append(4.0) - batchRepay.append(4.0) + batchAmounts.append(usdfHighSeize) + batchRepay.append(usdfHighRepay) } - // USDC high-risk [50..72] - for i in InclusiveRange(50, 72) { + // USDC high-risk [usdcHighStart..usdcHighEnd] + for i in InclusiveRange(usdcHighStart, usdcHighEnd) { batchPids.append(allPids[i]) batchSeize.append(MAINNET_USDC_TOKEN_ID) - batchAmounts.append(0.8) - batchRepay.append(0.8) + batchAmounts.append(usdcHighSeize) + batchRepay.append(usdcHighRepay) } - // WBTC uniform [95..99] - for i in InclusiveRange(95, 99) { + // WBTC uniform [wbtcStart..wbtcEnd] + for i in InclusiveRange(wbtcStart, wbtcEnd) { batchPids.append(allPids[i]) batchSeize.append(MAINNET_WBTC_TOKEN_ID) - batchAmounts.append(0.00003) - batchRepay.append(1.18) + batchAmounts.append(wbtcSeize) + batchRepay.append(wbtcRepay) } - // USDF moderate [25..49] - for i in InclusiveRange(25, 49) { + // USDF moderate [usdfModStart..usdfModEnd] + for i in InclusiveRange(usdfModStart, usdfModEnd) { batchPids.append(allPids[i]) batchSeize.append(MAINNET_USDF_TOKEN_ID) - batchAmounts.append(4.0) - batchRepay.append(3.0) + batchAmounts.append(usdfModSeize) + batchRepay.append(usdfModRepay) } - // USDC moderate [73..94] - for i in InclusiveRange(73, 94) { + // USDC moderate [usdcModStart..usdcModEnd] + for i in InclusiveRange(usdcModStart, usdcModEnd) { batchPids.append(allPids[i]) batchSeize.append(MAINNET_USDC_TOKEN_ID) - batchAmounts.append(0.8) - batchRepay.append(0.6) + batchAmounts.append(usdcModSeize) + batchRepay.append(usdcModRepay) } Test.assert(batchPids.length == 100, message: "Expected 100 batch entries, got \(batchPids.length)") @@ -861,7 +950,7 @@ access(all) fun testMassUnhealthyLiquidations() { seizeVaultIdentifiers: batchSeize, seizeAmounts: batchAmounts, repayAmounts: batchRepay, - chunkSize: 10, + chunkSize: batchChunkSize, signer: liquidator ) From 1e0b09162d9fabf81a1b2d5fe6fd19e980b33310 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Tue, 10 Mar 2026 14:18:09 -0400 Subject: [PATCH 16/25] docs: clarify nominal interest rate semantics --- cadence/contracts/FlowALPEvents.cdc | 8 +- cadence/contracts/FlowALPInterestRates.cdc | 6 +- cadence/lib/FlowALPMath.cdc | 8 +- cadence/tests/TEST_COVERAGE.md | 2 +- .../interest_accrual_integration_test.cdc | 85 ++++++++++--------- .../tests/interest_curve_advanced_test.cdc | 48 +++++------ cadence/tests/interest_curve_test.cdc | 4 +- ...kink_curve_utilization_regression_test.cdc | 2 +- cadence/tests/update_interest_rate_test.cdc | 2 +- docs/interest_rate_and_protocol_fees.md | 24 +++--- 10 files changed, 97 insertions(+), 92 deletions(-) diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc index e46fd39c..610c724e 100644 --- a/cadence/contracts/FlowALPEvents.cdc +++ b/cadence/contracts/FlowALPEvents.cdc @@ -141,11 +141,11 @@ access(all) contract FlowALPEvents { ) /// Emitted when the insurance rate for a token is updated by governance. - /// The insurance rate is an annual fraction of debit interest diverted to the insurance fund. + /// The insurance rate is a fee fraction of accrued debit interest diverted to the insurance fund. /// /// @param poolUUID the UUID of the pool containing the token /// @param tokenType the type identifier string of the token whose rate changed - /// @param insuranceRate the new annual insurance rate (e.g. 0.001 for 0.1%) + /// @param insuranceRate the new insurance fee fraction (e.g. 0.001 for 0.1%) access(all) event InsuranceRateUpdated( poolUUID: UInt64, tokenType: String, @@ -167,11 +167,11 @@ access(all) contract FlowALPEvents { ) /// Emitted when the stability fee rate for a token is updated by governance. - /// The stability fee rate is an annual fraction of debit interest diverted to the stability fund. + /// The stability fee rate is a fee fraction of accrued debit interest diverted to the stability fund. /// /// @param poolUUID the UUID of the pool containing the token /// @param tokenType the type identifier string of the token whose rate changed - /// @param stabilityFeeRate the new annual stability fee rate (e.g. 0.05 for 5%) + /// @param stabilityFeeRate the new stability fee fraction (e.g. 0.05 for 5%) access(all) event StabilityFeeRateUpdated( poolUUID: UInt64, tokenType: String, diff --git a/cadence/contracts/FlowALPInterestRates.cdc b/cadence/contracts/FlowALPInterestRates.cdc index e509e204..18b5a3c8 100644 --- a/cadence/contracts/FlowALPInterestRates.cdc +++ b/cadence/contracts/FlowALPInterestRates.cdc @@ -4,7 +4,7 @@ access(all) contract FlowALPInterestRates { /// /// A simple interface to calculate interest rate for a token type. access(all) struct interface InterestCurve { - /// Returns the annual interest rate for the given credit and debit balance, for some token T. + /// Returns the annual nominal interest rate for the given credit and debit balance, for some token T. /// @param creditBalance The credit (deposit) balance of token T /// @param debitBalance The debit (withdrawal) balance of token T access(all) fun interestRate(creditBalance: UFix128, debitBalance: UFix128): UFix128 { @@ -22,7 +22,7 @@ access(all) contract FlowALPInterestRates { /// A fixed-rate interest curve implementation that returns a constant yearly interest rate /// regardless of utilization. This is suitable for stable assets like MOET where predictable /// rates are desired. - /// @param yearlyRate The fixed yearly interest rate as a UFix128 (e.g., 0.05 for 5% APY) + /// @param yearlyRate The fixed yearly nominal rate as a UFix128 (e.g., 0.05 for a 5% nominal yearly rate) access(all) struct FixedCurve: InterestCurve { access(all) let yearlyRate: UFix128 @@ -62,7 +62,7 @@ access(all) contract FlowALPInterestRates { /// This matches the live TokenState accounting used by FlowALP. /// /// @param optimalUtilization The target utilization ratio (e.g., 0.80 for 80%) - /// @param baseRate The minimum yearly interest rate (e.g., 0.01 for 1% APY) + /// @param baseRate The minimum yearly nominal rate (e.g., 0.01 for a 1% nominal yearly rate) /// @param slope1 The total rate increase from 0% to optimal utilization (e.g., 0.04 for 4%) /// @param slope2 The total rate increase from optimal to 100% utilization (e.g., 0.60 for 60%) access(all) struct KinkCurve: InterestCurve { diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 1a753d89..27f60e83 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -99,9 +99,11 @@ access(all) contract FlowALPMath { return diffBps <= maxDeviationBps } - /// Converts a yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed point - /// number with 18 decimal places). The input to this function will be just the relative annual interest rate - /// (e.g. 0.05 for 5% interest), and the result will be the per-second multiplier (e.g. 1.000000000001). + /// Converts a nominal yearly interest rate to a per-second multiplication factor (stored in a UFix128 as a fixed + /// point number with 18 decimal places). The input to this function is the relative nominal annual rate + /// (e.g. 0.05 for a 5% nominal yearly rate), and the result is the per-second multiplier + /// (e.g. 1.000000000001). For positive rates, the effective one-year growth will be slightly higher than the + /// nominal rate because interest compounds over time. access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 assert( diff --git a/cadence/tests/TEST_COVERAGE.md b/cadence/tests/TEST_COVERAGE.md index 6c81ec3c..2a8b7239 100644 --- a/cadence/tests/TEST_COVERAGE.md +++ b/cadence/tests/TEST_COVERAGE.md @@ -204,7 +204,7 @@ The `test_helpers.cdc` file provides: 3. **FLOW Debit Interest** - KinkCurve-based interest rates - Variable rates based on utilization - - Interest compounds continuously + - Interest compounds via discrete per-second updates 4. **FLOW Credit Interest** - LP earnings with insurance spread diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index eaef69b3..0302c932 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -28,7 +28,7 @@ import "test_helpers.cdc" // - Focuses on protocol solvency and insurance mechanics // // Interest Rate Configuration: -// - MOET: FixedCurve at 4% APY (rate independent of utilization) +// - MOET: FixedCurve at a 4% nominal yearly rate (rate independent of utilization) // - Flow: KinkCurve with Aave v3 Volatile One parameters // (45% optimal utilization, 0% base, 4% slope1, 300% slope2) // ============================================================================= @@ -40,18 +40,18 @@ access(all) var snapshot: UInt64 = 0 // Interest Rate Parameters // ============================================================================= -// MOET: FixedCurve (Spread Model) +// MOET: FixedCurve (Protocol-Fee Spread Model) // ----------------------------------------------------------------------------- -// In the spread model, the curve defines the DEBIT rate (what borrowers pay). -// The CREDIT rate is derived as: creditRate = debitRate - insuranceRate +// In the fixed-curve path, the curve defines the DEBIT rate (what borrowers pay). +// The CREDIT rate is derived from the debit rate after protocol fees. // This ensures lenders always earn less than borrowers pay, with the -// difference going to the insurance pool for protocol solvency. +// difference allocated by the configured protocol fee settings. // -// Example at 4% debit rate with 0.1% insurance: -// - Borrowers pay: 4.0% APY -// - Lenders earn: 3.9% APY -// - Insurance: 0.1% APY (collected by protocol) -access(all) let moetFixedRate: UFix128 = 0.04 // 4% APY debit rate +// Example at a 4% nominal yearly debit rate: +// - Borrowers pay: 4.0% nominal yearly debit rate +// - Lenders earn: a lower nominal yearly credit rate after protocol fees +// - Insurance/stability: configured fee fractions of accrued debit interest +access(all) let moetFixedRate: UFix128 = 0.04 // 4% nominal yearly debit rate // FlowToken: KinkCurve (Aave v3 Volatile One Parameters) // ----------------------------------------------------------------------------- @@ -64,10 +64,10 @@ access(all) let moetFixedRate: UFix128 = 0.04 // 4% APY debit rate // - If utilization > optimal: rate = baseRate + slope1 + ((util-optimal)/(1-optimal)) × slope2 // // At 40% utilization (below 45% optimal): -// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% APY +// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% nominal yearly rate // // At 80% utilization (above 45% optimal): -// - Rate = 0% + 4% + ((80%-45%)/(100%-45%)) × 300% ≈ 195% APY +// - Rate = 0% + 4% + ((80%-45%)/(100%-45%)) × 300% ≈ 195% nominal yearly rate 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 @@ -160,7 +160,7 @@ fun test_moet_debit_accrues_interest() { // ------------------------------------------------------------------------- // STEP 4: Configure MOET Interest Rate // ------------------------------------------------------------------------- - // Set MOET to use a FixedCurve at 4% APY. + // Set MOET to use a FixedCurve at a 4% nominal yearly rate. // This rate is independent of utilization - borrowers always pay 4%. // Note: Interest curve must be set AFTER LP deposit to ensure credit exists. setInterestCurveFixed( @@ -168,7 +168,7 @@ fun test_moet_debit_accrues_interest() { tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: moetFixedRate ) - log("Set MOET interest rate to 4% APY (after LP deposit)") + log("Set MOET interest rate to 4% nominal yearly rate (after LP deposit)") let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, @@ -305,7 +305,7 @@ fun test_moet_debit_accrues_interest() { // Expected Growth Calculation // ------------------------------------------------------------------------- // Per-second compounding: (1 + r / 31_557_600) ^ seconds - 1 - // At 4% APY for 30 days (2,592,000 seconds): + // At a 4% nominal yearly rate for 30 days (2,592,000 seconds): // Growth = (1 + 0.04 / 31_557_600) ^ 2_592_000 - 1 ≈ 0.328% // // We use a wide tolerance range because: @@ -337,10 +337,10 @@ fun test_moet_debit_accrues_interest() { // - Time advances 30 days // - Verify: LP credit increased, growth rate is in expected range // -// Key Insight (FixedCurve Spread Model): -// - debitRate = 4.0% (what borrowers pay, defined by curve) -// - insuranceRate = 0.1% (protocol reserve) -// - creditRate = debitRate - insuranceRate = 3.9% (what lenders earn) +// Key Insight (FixedCurve Protocol-Fee Spread): +// - debitRate is defined by the curve +// - creditRate is the debit rate after protocol fees +// - creditRate remains below debitRate // ============================================================================= access(all) fun test_moet_credit_accrues_interest_with_insurance() { @@ -394,7 +394,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // ------------------------------------------------------------------------- // STEP 4: Configure MOET Interest Rate // ------------------------------------------------------------------------- - // Set 4% APY debit rate. Credit rate will be ~3.9% after insurance deduction. + // Set a 4% nominal yearly debit rate. Credit rate will be lower after protocol fees. setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, @@ -485,9 +485,9 @@ fun test_moet_credit_accrues_interest_with_insurance() { // ------------------------------------------------------------------------- // Expected Credit Growth Calculation // ------------------------------------------------------------------------- - // Debit rate: 4% APY (what borrowers pay) - // Insurance: 0.1% APY (protocol reserve) - // Credit rate: 4% - 0.1% = 3.9% APY (what LPs earn) + // Debit rate: 4% nominal yearly rate (what borrowers pay) + // Protocol fees: configured insurance plus stability fee fractions + // Credit rate: lower than the debit rate after protocol fees // // 30-day credit growth ≈ 3.9% × (30/365) ≈ 0.32% // @@ -523,7 +523,7 @@ fun test_moet_credit_accrues_interest_with_insurance() { // Key Insight (KinkCurve): // At 40% utilization (below 45% optimal kink): // - Rate = baseRate + (utilization/optimal) × slope1 -// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% APY +// - Rate = 0% + (40%/45%) × 4% ≈ 3.56% nominal yearly rate // ============================================================================= access(all) fun test_flow_debit_accrues_interest() { @@ -685,7 +685,7 @@ fun test_flow_debit_accrues_interest() { // ------------------------------------------------------------------------- // Utilization = 4,000 / 10,000 = 40% (below 45% optimal) // Rate = baseRate + (util/optimal) × slope1 - // = 0% + (40%/45%) × 4% ≈ 3.56% APY + // = 0% + (40%/45%) × 4% ≈ 3.56% nominal yearly rate // // 30-day growth ≈ 3.56% × (30/365) ≈ 0.29% let minExpectedDebtGrowth: UFix64 = 0.002 // 0.2% @@ -891,15 +891,15 @@ fun test_flow_credit_accrues_interest_with_insurance() { // - LP deposits 10,000 MOET // - Borrower deposits 10,000 FLOW and borrows MOET // - Insurance rate set to 1% (higher than default 0.1% for visibility) -// - Debit rate set to 10% APY +// - Debit rate set to a 10% nominal yearly rate // - Time advances 1 YEAR // - Verify: Insurance spread ≈ 1% (debit rate - credit rate) // -// Key Insight (FixedCurve Spread Model): -// - debitRate = 10% (what borrowers pay) -// - insuranceRate = 1% (protocol reserve) -// - creditRate = debitRate - insuranceRate = 9% (what LPs earn) -// - Spread = debitRate - creditRate = 1% +// Key Insight (FixedCurve Protocol-Fee Spread): +// - debitRate is set by the fixed curve +// - insurance/stability remain configured fee fractions +// - creditRate is reduced relative to debitRate by those protocol fees +// - the realized spread shows up as a lower lender growth rate than borrower growth rate // ============================================================================= access(all) fun test_insurance_deduction_verification() { @@ -952,7 +952,7 @@ fun test_insurance_deduction_verification() { // // Insurance Rate: 1% (vs default 0.1%) // Debit Rate: 10% (vs default 4%) - // Expected Credit Rate: 10% - 1% = 9% + // Expected Credit Rate: lower than 10% after protocol fees let res = setInsuranceSwapper( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, @@ -1012,8 +1012,8 @@ fun test_insurance_deduction_verification() { // ========================================================================= // Using 1 year (31,557,600 seconds for 365.25 days) makes the percentage calculations // straightforward. With per-second discrete compounding: - // - 10% APY → (1 + 0.10 / 31_557_600) ^ 31_557_600 - 1 ≈ 10.52% effective rate - // - 9% APY → (1 + 0.09 / 31_557_600) ^ 31_557_600 - 1 ≈ 9.42% effective rate + // - 10% nominal yearly rate → (1 + 0.10 / 31_557_600) ^ 31_557_600 - 1 ≈ 10.52% effective rate + // - 9% nominal yearly rate → (1 + 0.09 / 31_557_600) ^ 31_557_600 - 1 ≈ 9.42% effective rate // - Spread should be approximately 1% Test.moveTime(by: ONE_YEAR) Test.commitBlock() @@ -1049,9 +1049,10 @@ fun test_insurance_deduction_verification() { // ========================================================================= // ASSERTION: Verify Insurance Spread // ========================================================================= - // For FixedCurve (spread model): - // - debitRate = creditRate + insuranceRate - // - insuranceSpread = debitRate - creditRate ≈ insuranceRate + // For FixedCurve: + // - debitRate is the curve-defined nominal yearly rate + // - creditRate is the debit rate after protocol fees + // - insuranceSpread = actualDebtRate - actualCreditRate // // With 10% debit and 1% insurance, spread should be ~1% // (Slight variation due to per-second compounding effects) @@ -1158,12 +1159,12 @@ fun test_combined_all_interest_scenarios() { // ------------------------------------------------------------------------- // STEP 5: Configure Interest Curves for Both Tokens // ------------------------------------------------------------------------- - // MOET: FixedCurve at 4% APY (spread model) + // MOET: FixedCurve at a 4% nominal yearly rate (fixed-curve spread model) // Flow: KinkCurve with Aave v3 Volatile One parameters setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, - yearlyRate: moetFixedRate // 4% APY + yearlyRate: moetFixedRate // 4% nominal yearly rate ) setInterestCurveKink( signer: PROTOCOL_ACCOUNT, @@ -1324,14 +1325,14 @@ fun test_combined_all_interest_scenarios() { // Assertion Group 2: Health Factor Changes // ------------------------------------------------------------------------- // Borrower1 (Flow collateral, MOET debt): - // - MOET debit rate: 4% APY + // - MOET debit rate: 4% nominal yearly rate // - Flow credit rate: lower than Flow debit rate due to insurance spread // - Net effect: Debt grows faster than collateral → Health DECREASES Test.assert(b1HealthAfter < b1HealthBefore, message: "Borrower1 health should decrease") // Borrower2 (MOET collateral, Flow debt): - // - MOET credit rate: ~3.9% APY (4% debit - 0.1% insurance) - // - Flow debit rate: ~2.5% APY (at 28.6% utilization) + // - MOET credit rate: lower than the MOET debit rate after protocol fees + // - Flow debit rate: ~2.5% nominal yearly rate (at 28.6% utilization) // - Collateral (3,000 MOET) earning more absolute interest than debt (2,000 Flow) // - Net effect: Health INCREASES Test.assert(b2HealthAfter > b2HealthBefore, message: "Borrower2 health should increase (collateral interest > debt interest)") diff --git a/cadence/tests/interest_curve_advanced_test.cdc b/cadence/tests/interest_curve_advanced_test.cdc index c1005703..aac2f935 100644 --- a/cadence/tests/interest_curve_advanced_test.cdc +++ b/cadence/tests/interest_curve_advanced_test.cdc @@ -39,9 +39,9 @@ fun setup() { // 4. Rate change ratios are mathematically correct // // Scenario using a single pool that evolves over time: -// - Phase 1: 10 days at 5% APY -// - Phase 2: 10 days at 15% APY (3x rate) -// - Phase 3: 10 days at 10% APY (2x original rate) +// - Phase 1: 10 days at a 5% nominal yearly rate +// - Phase 2: 10 days at a 15% nominal yearly rate (3x rate) +// - Phase 3: 10 days at a 10% nominal yearly rate (2x original rate) // ============================================================================= access(all) fun test_curve_change_mid_accrual_and_rate_segmentation() { @@ -105,7 +105,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // ------------------------------------------------------------------------- // STEP 4: Set Initial Interest Rate (Phase 1 Configuration) // ------------------------------------------------------------------------- - // Configure MOET with a fixed 5% APY interest rate. + // Configure MOET with a fixed 5% nominal yearly interest rate. // This is the baseline rate we'll compare other phases against. // Using FixedCurve means rate doesn't depend on utilization. let rate1: UFix128 = 0.05 @@ -114,7 +114,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: rate1 ) - log("Set MOET interest rate to 5% APY (Phase 1)") + log("Set MOET interest rate to 5% nominal yearly rate (Phase 1)") // set insurance swapper let res = setInsuranceSwapper( @@ -169,7 +169,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { log("Initial debt: \(debtT0.toString())") // 6153.84615384 MOET // ========================================================================= - // PHASE 1: 10 Days at 5% APY + // PHASE 1: 10 Days at a 5% Nominal Yearly Rate // ========================================================================= // Advance blockchain time by 10 days and observe interest accrual. // Formula: perSecondRate = 1 + 0.05/31_557_600, factor = perSecondRate^864000 @@ -196,9 +196,9 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { Test.assert(diff1 <= tolerance, message: "Phase 1 growth should be ~8.42992491. Actual: \(growth1)") // ------------------------------------------------------------------------- - // STEP 7: Change Interest Rate to 15% APY (Phase 2 Configuration) + // STEP 7: Change Interest Rate to a 15% Nominal Yearly Rate (Phase 2 Configuration) // ------------------------------------------------------------------------- - // Triple the interest rate to 15% APY. This tests that: + // Triple the nominal yearly rate to 15%. This tests that: // 1. Interest accrued at old rate (5%) is finalized before curve change // 2. New rate (15%) is applied correctly for subsequent accrual // 3. The ratio of growth reflects the ratio of rates (15%/5% = 3x) @@ -209,10 +209,10 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { yearlyRate: rate2 ) - log("Changed MOET interest rate to 15% APY (Phase 2)") + log("Changed MOET interest rate to 15% nominal yearly rate (Phase 2)") // ========================================================================= - // PHASE 2: 10 Days at 15% APY + // PHASE 2: 10 Days at a 15% Nominal Yearly Rate // ========================================================================= // Advance another 10 days at the higher rate. // Expected: growth2 should be approximately 3x growth1 (since 15%/5% = 3). @@ -234,7 +234,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { Test.assert(diff2 <= tolerance, message: "Phase 2 growth should be ~25.35912505. Actual: \(growth2)") // ------------------------------------------------------------------------- - // STEP 8: Change Interest Rate to 10% APY (Phase 3 Configuration) + // STEP 8: Change Interest Rate to a 10% Nominal Yearly Rate (Phase 3 Configuration) // ------------------------------------------------------------------------- // Set rate to 10% (2x the original 5%, 0.67x Phase 2's 15%). // This validates that multiple consecutive rate changes work correctly. @@ -244,12 +244,12 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: rate3 ) - log("Changed MOET interest rate to 10% APY (Phase 3)") + log("Changed MOET interest rate to 10% nominal yearly rate (Phase 3)") // ========================================================================= - // PHASE 3: 10 Days at 10% APY + // PHASE 3: 10 Days at a 10% Nominal Yearly Rate // ========================================================================= - // Final 10-day period at 10% APY. + // Final 10-day period at a 10% nominal yearly rate. // Expected: growth3 should be approximately 2x growth1 (since 10%/5% = 2). Test.moveTime(by: TEN_DAYS) Test.commitBlock() @@ -338,7 +338,7 @@ fun test_curve_change_mid_accrual_and_rate_segmentation() { // Formula: FinalBalance = InitialBalance × (1 + r/n)^(n×t) for per-second compounding // The protocol uses discrete per-second compounding with exponentiation by squaring. // -// Expected: 10% APY should yield ~10.52% effective rate ((1 + 0.10/31_557_600)^31_557_600 ≈ 1.10517) +// Expected: a 10% nominal yearly rate should yield ~10.52% effective one-year growth ((1 + 0.10/31_557_600)^31_557_600 ≈ 1.10517) // ============================================================================= access(all) fun test_exact_compounding_verification_one_year() { @@ -354,16 +354,16 @@ fun test_exact_compounding_verification_one_year() { // ------------------------------------------------------------------------- // STEP 1: Configure a Known Interest Rate for Mathematical Verification // ------------------------------------------------------------------------- - // Set MOET to exactly 10% APY. This round number makes it easy to verify + // Set MOET to exactly a 10% nominal yearly rate. This round number makes it easy to verify // that the compounding formula is working correctly. - // 10% APY with per-second compounding yields: (1 + 0.10/31_557_600)^31_557_600 - 1 ≈ 10.517% effective rate + // A 10% nominal yearly rate with per-second compounding yields: (1 + 0.10/31_557_600)^31_557_600 - 1 ≈ 10.517% effective rate let yearlyRate: UFix128 = 0.10 setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: yearlyRate ) - log("Set MOET interest rate to 10% APY for compounding verification") + log("Set MOET interest rate to 10% nominal yearly rate for compounding verification") // ------------------------------------------------------------------------- // STEP 2: Record Starting Debt Before Time Advancement @@ -408,7 +408,7 @@ fun test_exact_compounding_verification_one_year() { // MATHEMATICAL BACKGROUND: Per-Second Compounding // ========================================================================= // Formula: factor = (1 + r/31_557_600)^31_557_600 - // At 10% APY: factor = (1 + 0.10/31_557_600)^31_557_600 ≈ 1.10517092 + // At a 10% nominal yearly rate: factor = (1 + 0.10/31_557_600)^31_557_600 ≈ 1.10517092 // This is discrete per-second compounding with exponentiation by squaring. // Note: Using 31557600 seconds/year (365.25 days) // ========================================================================= @@ -422,7 +422,7 @@ fun test_exact_compounding_verification_one_year() { // Expected growth = debtBefore * (factor - 1) = 6204.59926707 * 0.105170918 ≈ 652.54340074 MOET // Note: Tests run sequentially with accumulated interest, so exact values depend on debtBefore - // Verify growth rate is approximately 10.52% (the effective rate from 10% APY compounded per-second) + // Verify growth rate is approximately 10.52% (the effective yield from a 10% nominal yearly rate compounded per second) let expectedGrowthRate: UFix64 = 0.10517091 let expectedGrowth: UFix64 = 652.54340074 let tolerance: UFix64 = 0.001 @@ -589,15 +589,15 @@ fun test_credit_rate_changes_with_curve() { // ------------------------------------------------------------------------- // STEP 1: Set a Known Interest Rate for Credit Verification // ------------------------------------------------------------------------- - // Configure MOET with 8% APY. This will be the debit rate. + // Configure MOET with an 8% nominal yearly rate. This will be the debit rate. // The LP should earn slightly less (approximately 7.9% after insurance). - let testRate: UFix128 = 0.08 // 8% APY + let testRate: UFix128 = 0.08 // 8% nominal yearly rate setInterestCurveFixed( signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: MOET_TOKEN_IDENTIFIER, yearlyRate: testRate ) - log("Set MOET interest rate to 8% APY") + log("Set MOET interest rate to 8% nominal yearly rate") // ------------------------------------------------------------------------- // STEP 2: Record LP's Credit Balance Before Time Advancement @@ -648,7 +648,7 @@ fun test_credit_rate_changes_with_curve() { // Verify credit growth rate equals expected value // Formula: creditRate = debitRate * (1 - protocolFeeRate) // where protocolFeeRate = insuranceRate + stabilityFeeRate = 0.001 + 0.05 = 0.051 - // creditRate = 0.08 * (1 - 0.051) = 0.08 * 0.949 = 0.07592 APY (7.592%) + // creditRate = 0.08 * (1 - 0.051) = 0.08 * 0.949 = 0.07592 nominal yearly credit rate (7.592%) // perSecondRate = 1 + (0.07592/31557600), factor = perSecondRate^2592000 // Expected 30-day growth rate = factor - 1 ≈ 0.00625521141 // Expected credit growth = creditBefore * 0.00625950922 ≈ 346.58 MOET diff --git a/cadence/tests/interest_curve_test.cdc b/cadence/tests/interest_curve_test.cdc index adbfa692..afa9c78f 100644 --- a/cadence/tests/interest_curve_test.cdc +++ b/cadence/tests/interest_curve_test.cdc @@ -18,7 +18,7 @@ fun setup() { access(all) fun test_FixedCurve_returns_constant_rate() { - // Create a fixed rate curve with 5% APY + // Create a fixed rate curve with a 5% nominal yearly rate let fixedRate: UFix128 = 0.05 let curve = FlowALPInterestRates.FixedCurve(yearlyRate: fixedRate) @@ -32,7 +32,7 @@ fun test_FixedCurve_returns_constant_rate() { access(all) fun test_FixedCurve_accepts_zero_rate() { - // Zero rate should be valid (0% APY) + // Zero rate should be valid (0% nominal yearly rate) let curve = FlowALPInterestRates.FixedCurve(yearlyRate: 0.0) let rate = curve.interestRate(creditBalance: 100.0, debitBalance: 50.0) Test.assertEqual(0.0 as UFix128, rate) diff --git a/cadence/tests/kink_curve_utilization_regression_test.cdc b/cadence/tests/kink_curve_utilization_regression_test.cdc index b7c5924f..17f4011b 100644 --- a/cadence/tests/kink_curve_utilization_regression_test.cdc +++ b/cadence/tests/kink_curve_utilization_regression_test.cdc @@ -89,6 +89,6 @@ fun test_regression_TokenState_90_borrow_of_100_supply_should_price_at_90_percen Test.assert( actualYearlyRate == 0.35, message: - "Regression: 100 supplied / 90 borrowed should price at 90% utilization (0.35 APY), but current accounting passed creditBalance=\(tokenState.getTotalCreditBalance()) and debitBalance=\(tokenState.getTotalDebitBalance()), producing \(actualYearlyRate) instead" + "Regression: 100 supplied / 90 borrowed should price at 90% utilization (0.35 nominal yearly rate), but current accounting passed creditBalance=\(tokenState.getTotalCreditBalance()) and debitBalance=\(tokenState.getTotalDebitBalance()), producing \(actualYearlyRate) instead" ) } diff --git a/cadence/tests/update_interest_rate_test.cdc b/cadence/tests/update_interest_rate_test.cdc index 4dab8f76..bab33343 100644 --- a/cadence/tests/update_interest_rate_test.cdc +++ b/cadence/tests/update_interest_rate_test.cdc @@ -27,7 +27,7 @@ fun setup() { } // ============================================================================= -// FixedCurve Tests (Spread Model: creditRate = debitRate - insuranceRate) +// FixedCurve Tests (Spread Model: creditRate = debitRate * (1 - protocolFeeRate)) // ============================================================================= access(all) diff --git a/docs/interest_rate_and_protocol_fees.md b/docs/interest_rate_and_protocol_fees.md index 95173312..84380fa0 100644 --- a/docs/interest_rate_and_protocol_fees.md +++ b/docs/interest_rate_and_protocol_fees.md @@ -20,7 +20,7 @@ Both fees are deducted from interest income to protect the protocol and fund ope #### Insurance Fund -The insurance fund serves as the protocol's **reserve for covering bad debt**, acting as the liquidator of last resort. A percentage of lender interest income is continuously collected and swapped to MOET, building a safety buffer that grows over time. When there are liquidations that aren't able to be covered and would normally create bad debt in the protocol, the MOET is swapped for that specific asset to cover that delta of bad debt. These funds are **never withdrawable** by governance and exist solely to protect lenders from losses. +The insurance fund serves as the protocol's **reserve for covering bad debt**, acting as the liquidator of last resort. A percentage of lender interest income is collected as interest accrues and swapped to MOET, building a safety buffer that grows over time. When there are liquidations that aren't able to be covered and would normally create bad debt in the protocol, the MOET is swapped for that specific asset to cover that delta of bad debt. These funds are **never withdrawable** by governance and exist solely to protect lenders from losses. #### Stability Fund @@ -66,7 +66,7 @@ creditRate = (debitIncome - protocolFeeAmount) / totalCreditBalance ### 3. Per-Second Rate Conversion -Both credit and debit rates are converted from annual rates to per-second compounding rates: +Both credit and debit rates are converted from nominal annual rates to per-second compounding rates: ``` perSecondRate = (yearlyRate / secondsInYear) + 1.0 @@ -74,7 +74,9 @@ perSecondRate = (yearlyRate / secondsInYear) + 1.0 Where `secondsInYear = 31_557_600` (365.25 days × 24 hours × 60 minutes × 60 seconds). -This conversion allows for continuous compounding of interest over time. +This conversion allows for discrete per-second compounding of interest over time. + +Important terminology: the configured `yearlyRate` is a **nominal yearly rate**, not a promise that a balance will grow by exactly that percentage over one calendar year. For positive fixed rates, the effective one-year growth is slightly higher because of compounding. For variable curves, realized growth also depends on when utilization changes and the rate is recomputed. ## Interest Accrual Mechanism @@ -86,7 +88,7 @@ The protocol uses **interest indices** to track how interest accrues over time. ### Compounding Interest -Interest compounds continuously using the formula: +Interest compounds via discrete per-second updates using the formula: ``` newIndex = oldIndex * (perSecondRate ^ elapsedSeconds) @@ -119,7 +121,7 @@ Interest indices are updated whenever: 2. `updateForTimeChange()` is called explicitly 3. `updateInterestRatesAndCollectInsurance()` or `updateInterestRatesAndCollectStability()` is called -The update calculates the time elapsed since `lastUpdate` and compounds the interest indices accordingly. +The update calculates the time elapsed since `lastUpdate` and compounds the interest indices accordingly. When rates are variable, realized growth over a period depends on the sequence of utilization changes and the rate recomputations they trigger, so the displayed yearly rate should not be interpreted as an exact promised one-year payoff. ## Insurance Collection Mechanism @@ -129,7 +131,7 @@ The insurance mechanism collects a percentage of interest income over time, swap ### Insurance Rate -Each token has a configurable `insuranceRate` (default: 0.0) that represents the annual percentage of interest income that should be collected as insurance. +Each token has a configurable `insuranceRate` (default: 0.0) that represents the fraction of accrued interest income that should be collected as insurance when fees are settled. ### Collection Process @@ -153,7 +155,7 @@ Insurance is collected through the `collectInsurance()` function on `TokenState` 4. **Deposits to Insurance Fund**: - The collected MOET is deposited into the protocol's insurance fund - - This fund grows continuously and is never withdrawable + - This fund grows as insurance is collected and is never withdrawable ### Integration with Rate Updates @@ -213,7 +215,7 @@ The stability fee mechanism collects a percentage of interest income over time a ### Stability Fee Rate -Each token has a configurable `stabilityFeeRate` (default: 0.05 or 5%) that represents the percentage of interest income that should be collected as stability fees. +Each token has a configurable `stabilityFeeRate` (default: 0.05 or 5%) that represents the fraction of accrued interest income that should be collected as stability fees when they are settled. ### Collection Process @@ -264,7 +266,7 @@ This emits a `StabilityFundWithdrawn` event for transparency and accountability. 1. **Initial State**: - Total credit balance (lender deposits): 10,000 FLOW - Total debit balance (borrower debt): 8,000 FLOW - - Debit rate: 10% APY + - Debit rate: 10% nominal yearly rate - Insurance rate: 0.1% (of interest income) - Stability fee rate: 5% (of interest income) @@ -274,7 +276,7 @@ This emits a `StabilityFundWithdrawn` event for transparency and accountability. - Insurance collection: 841.37 × 0.001 = 0.841 FLOW → converted to MOET - Stability collection: 841.37 × 0.05 = 42.07 FLOW → kept as FLOW - Net to lenders: 841.37 - 0.841 - 42.07 = 798.46 FLOW - - Effective lender APY: 798.46 / 10,000 = 7.98% + - Effective lender yield over the year: 798.46 / 10,000 = 7.98% 3. **Fund Accumulation**: - Insurance fund: +0.841 FLOW worth of MOET (permanent, for bad debt coverage) @@ -282,7 +284,7 @@ This emits a `StabilityFundWithdrawn` event for transparency and accountability. ## Key Design Decisions -1. **Continuous Compounding**: Interest compounds continuously using per-second rates, providing fair and accurate interest accrual. +1. **Discrete Per-Second Compounding**: Interest compounds via per-second updates, providing fair and accurate interest accrual. 2. **Scaled vs True Balances**: Storing scaled balances (principal) separately from interest indices allows efficient storage while maintaining precision. From 2d40ba53e7057771f7ae7663ffed0ceab1a8ea56 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Tue, 10 Mar 2026 14:23:13 -0400 Subject: [PATCH 17/25] docs: align fixed-rate transaction wording --- cadence/contracts/FlowALPInterestRates.cdc | 2 +- .../pool-governance/add_supported_token_fixed_rate_curve.cdc | 2 +- .../pool-governance/add_supported_token_zero_rate_curve.cdc | 2 +- .../flow-alp/pool-governance/set_interest_curve_fixed.cdc | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/FlowALPInterestRates.cdc b/cadence/contracts/FlowALPInterestRates.cdc index 18b5a3c8..9d5d4226 100644 --- a/cadence/contracts/FlowALPInterestRates.cdc +++ b/cadence/contracts/FlowALPInterestRates.cdc @@ -19,7 +19,7 @@ access(all) contract FlowALPInterestRates { /// FixedCurve /// - /// A fixed-rate interest curve implementation that returns a constant yearly interest rate + /// A fixed-rate interest curve implementation that returns a constant nominal yearly interest rate /// regardless of utilization. This is suitable for stable assets like MOET where predictable /// rates are desired. /// @param yearlyRate The fixed yearly nominal rate as a UFix128 (e.g., 0.05 for a 5% nominal yearly rate) diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc index c8cddf63..259c3051 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_fixed_rate_curve.cdc @@ -3,7 +3,7 @@ import "FlowALPModels" import "FlowALPInterestRates" /// Adds a token type as supported to the stored pool with a fixed-rate interest curve. -/// This uses FixedCurve for a constant yearly interest rate regardless of utilization. +/// This uses FixedCurve for a constant nominal yearly interest rate regardless of utilization. /// transaction( tokenTypeIdentifier: String, diff --git a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc index af16a460..eced40c2 100644 --- a/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc +++ b/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc @@ -2,7 +2,7 @@ import "FlowALPv0" import "FlowALPModels" import "FlowALPInterestRates" -/// Adds a token type as supported to the stored pool with a zero-rate interest curve (0% APY). +/// Adds a token type as supported to the stored pool with a zero-rate interest curve (0% nominal yearly rate). /// This uses FixedCurve with yearlyRate: 0.0, suitable for testing or /// scenarios where no interest should accrue. /// diff --git a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc index 88059754..5aad08fc 100644 --- a/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc +++ b/cadence/transactions/flow-alp/pool-governance/set_interest_curve_fixed.cdc @@ -3,7 +3,7 @@ import "FlowALPModels" import "FlowALPInterestRates" /// Updates the interest curve for an existing supported token to a FixedCurve. -/// This sets a constant yearly interest rate regardless of utilization. +/// This sets a constant nominal yearly interest rate regardless of utilization. /// transaction( tokenTypeIdentifier: String, From 6f760e866bfad4f360484fc0d51b44604e28d2d3 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Tue, 10 Mar 2026 16:51:44 -0400 Subject: [PATCH 18/25] docs: finish nominal interest terminology cleanup --- cadence/contracts/FlowALPModels.cdc | 4 ++-- cadence/lib/FlowALPMath.cdc | 4 ++-- cadence/tests/interest_curve_test.cdc | 14 ++++++------ cadence/tests/update_interest_rate_test.cdc | 10 ++++----- docs/interest_rate_and_protocol_fees.md | 24 ++++++++++----------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 6a38868e..194773da 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -1426,8 +1426,8 @@ access(all) contract FlowALPModels { } } - self.currentCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: creditRate) - self.currentDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + self.currentCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: creditRate) + self.currentDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) } /// Updates the credit and debit interest indices for elapsed time since last update. diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 27f60e83..3a8cad59 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -104,8 +104,8 @@ access(all) contract FlowALPMath { /// (e.g. 0.05 for a 5% nominal yearly rate), and the result is the per-second multiplier /// (e.g. 1.000000000001). For positive rates, the effective one-year growth will be slightly higher than the /// nominal rate because interest compounds over time. - access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { - let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 + access(all) view fun perSecondInterestRate(nominalYearlyRate: UFix128): UFix128 { + let perSecondScaledValue = nominalYearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 assert( perSecondScaledValue < UFix128.max, message: "Per-second interest rate \(perSecondScaledValue) is too high" diff --git a/cadence/tests/interest_curve_test.cdc b/cadence/tests/interest_curve_test.cdc index afa9c78f..0b81949c 100644 --- a/cadence/tests/interest_curve_test.cdc +++ b/cadence/tests/interest_curve_test.cdc @@ -187,7 +187,7 @@ fun test_TokenState_with_FixedCurve() { tokenState.increaseDebitBalance(by: 50.0) // Debit rate should be the per-second conversion of 10% yearly - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.10) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.10) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // For FixedCurve, credit rate uses the SPREAD MODEL: @@ -196,7 +196,7 @@ fun test_TokenState_with_FixedCurve() { // debitRate = 0.10 // protocolFeeRate = 0.0 + 0.05 = 0.05 (default insuranceRate = 0.0, default stabilityFeeRate = 0.05) // creditYearly = 0.10 * (1 - 0.05) = 0.095 - let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.095) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.095) Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } @@ -226,7 +226,7 @@ fun test_TokenState_with_KinkCurve() { // Verify the debit rate let expectedYearlyRate: UFix128 = 0.0575 - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedYearlyRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: expectedYearlyRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) } @@ -250,7 +250,7 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.increaseCreditBalance(by: 100.0) - let rateAtZeroUtilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) + let rateAtZeroUtilization = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.02) Test.assertEqual(rateAtZeroUtilization, tokenState.getCurrentDebitRate()) // Step 2: Add debt to create 50% utilization @@ -258,7 +258,7 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // rate = 0.02 + (0.05 × 0.50 / 0.80) = 0.02 + 0.03125 = 0.05125 tokenState.increaseDebitBalance(by: 50.0) - let rateAt50Utilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.05125) + let rateAt50Utilization = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.05125) Test.assertEqual(rateAt50Utilization, tokenState.getCurrentDebitRate()) // Step 3: Increase utilization to 90% (above kink) @@ -267,14 +267,14 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // rate = 0.02 + 0.05 + (0.50 × 0.50) = 0.32 tokenState.increaseDebitBalance(by: 40.0) - let rateAt90Util = FlowALPMath.perSecondInterestRate(yearlyRate: 0.32) + let rateAt90Util = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.32) Test.assertEqual(rateAt90Util, tokenState.getCurrentDebitRate()) // Step 4: Decrease debt to lower utilization back to 0% // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.decreaseDebitBalance(by: 90.0) - let rateBackToZero = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) + let rateBackToZero = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.02) Test.assertEqual(rateBackToZero, tokenState.getCurrentDebitRate()) } diff --git a/cadence/tests/update_interest_rate_test.cdc b/cadence/tests/update_interest_rate_test.cdc index bab33343..fc01ed10 100644 --- a/cadence/tests/update_interest_rate_test.cdc +++ b/cadence/tests/update_interest_rate_test.cdc @@ -50,12 +50,12 @@ fun test_FixedCurve_uses_spread_model() { tokenState.increaseDebitBalance(by: 500.0) // 50% utilization // Debit rate should match the fixed yearly rate - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = debitRate * (1 - protocolFeeRate) where protocolFeeRate = insuranceRate + stabilityFeeRate let expectedCreditYearly = UFix128(0.0999) // 0.10 * (1 - 0.001) - let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedCreditYearly) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: expectedCreditYearly) Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } @@ -81,7 +81,7 @@ fun test_KinkCurve_uses_reserve_factor_model() { tokenState.increaseDebitBalance(by: 50.0) // 25% utilization // Debit rate should match the curve rate - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = (debitIncome - protocolFeeAmount) / creditBalance @@ -90,7 +90,7 @@ fun test_KinkCurve_uses_reserve_factor_model() { // protocolFeeRate = insuranceRate + stabilityFeeRate = 0.001 + 0.05 = 0.051 // protocolFeeAmount = 10 * 0.051 = 0.51 // creditYearly = (10 - 0.51) / 200 = 0.04745 - let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.04745) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.04745) Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } @@ -111,7 +111,7 @@ fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // No debit balance - zero utilization // Debit rate still follows the curve - let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate should be `one` (multiplicative identity = 0% growth) since no debit income to distribute diff --git a/docs/interest_rate_and_protocol_fees.md b/docs/interest_rate_and_protocol_fees.md index 4c95c732..8d351a45 100644 --- a/docs/interest_rate_and_protocol_fees.md +++ b/docs/interest_rate_and_protocol_fees.md @@ -34,7 +34,7 @@ Both fees are deducted from the interest income that would otherwise go to lende ### 1. Debit Rate Calculation -For each token, the protocol stores one interest curve. The debit rate (borrow APY) is computed from that curve and the current pool utilization: +For each token, the protocol stores one interest curve. The debit rate (borrow nominal yearly rate) is computed from that curve and the current pool utilization: ```text if totalDebitBalance == 0: @@ -64,21 +64,21 @@ Utilization in this model is: - `100%` when debit and credit balances are equal - capped at `100%` in defensive edge cases where debt exceeds supply or supply is zero while debt remains positive -### FixedCurve (constant APY) +### FixedCurve (constant nominal yearly rate) -For `FixedCurve`, debit APY is constant regardless of utilization: +For `FixedCurve`, the debit nominal yearly rate is constant regardless of utilization: ``` debitRate = yearlyRate ``` Example: -- `yearlyRate = 0.05` (5% APY) -- debit APY stays at 5% whether utilization is 10% or 95% +- `yearlyRate = 0.05` (5% nominal yearly rate) +- the debit nominal yearly rate stays at 5% whether utilization is 10% or 95% -### KinkCurve (utilization-based APY) +### KinkCurve (utilization-based nominal yearly rate) -For `KinkCurve`, debit APY follows a two-segment curve: +For `KinkCurve`, the debit nominal yearly rate follows a two-segment curve: - below `optimalUtilization` ("before the kink"), rates rise gently - above `optimalUtilization` ("after the kink"), rates rise steeply @@ -117,15 +117,15 @@ Reference values discussed for volatile assets: - `slope2 = 300%` (`3.0`) Interpretation: -- at or below 45% utilization, borrowers see relatively low/gradual APY increases -- above 45%, APY increases very aggressively to push utilization back down -- theoretical max debit APY at 100% utilization is `304%` (`0% + 4% + 300%`) +- at or below 45% utilization, borrowers see relatively low/gradual nominal-rate increases +- above 45%, the nominal yearly rate increases very aggressively to push utilization back down +- theoretical max debit nominal yearly rate at 100% utilization is `304%` (`0% + 4% + 300%`) This is the mechanism that helps protect withdrawal liquidity under stress. ### 2. Credit Rate Calculation -The credit rate (deposit APY) is derived from debit-side income after protocol fees. +The credit rate (deposit nominal yearly rate) is derived from debit-side income after protocol fees. Shared definitions: @@ -140,7 +140,7 @@ For **FixedCurve** (used for stable assets like MOET): creditRate = debitRate * (1 - protocolFeeRate) ``` -This gives a simple spread model between borrow APY and lend APY. +This gives a simple spread model between borrow and lend nominal yearly rates. For **KinkCurve** and other non-fixed curves: ``` From 6da385123dd08d302f6520920b7d6c5d374a57a6 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Tue, 10 Mar 2026 16:54:08 -0400 Subject: [PATCH 19/25] revert: keep yearlyRate naming in code --- cadence/contracts/FlowALPModels.cdc | 4 ++-- cadence/lib/FlowALPMath.cdc | 4 ++-- cadence/tests/interest_curve_test.cdc | 14 +++++++------- cadence/tests/update_interest_rate_test.cdc | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cadence/contracts/FlowALPModels.cdc b/cadence/contracts/FlowALPModels.cdc index 194773da..6a38868e 100644 --- a/cadence/contracts/FlowALPModels.cdc +++ b/cadence/contracts/FlowALPModels.cdc @@ -1426,8 +1426,8 @@ access(all) contract FlowALPModels { } } - self.currentCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: creditRate) - self.currentDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) + self.currentCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: creditRate) + self.currentDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) } /// Updates the credit and debit interest indices for elapsed time since last update. diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 3a8cad59..27f60e83 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -104,8 +104,8 @@ access(all) contract FlowALPMath { /// (e.g. 0.05 for a 5% nominal yearly rate), and the result is the per-second multiplier /// (e.g. 1.000000000001). For positive rates, the effective one-year growth will be slightly higher than the /// nominal rate because interest compounds over time. - access(all) view fun perSecondInterestRate(nominalYearlyRate: UFix128): UFix128 { - let perSecondScaledValue = nominalYearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 + access(all) view fun perSecondInterestRate(yearlyRate: UFix128): UFix128 { + let perSecondScaledValue = yearlyRate / 31_557_600.0 // 365.25 * 24.0 * 60.0 * 60.0 assert( perSecondScaledValue < UFix128.max, message: "Per-second interest rate \(perSecondScaledValue) is too high" diff --git a/cadence/tests/interest_curve_test.cdc b/cadence/tests/interest_curve_test.cdc index 0b81949c..afa9c78f 100644 --- a/cadence/tests/interest_curve_test.cdc +++ b/cadence/tests/interest_curve_test.cdc @@ -187,7 +187,7 @@ fun test_TokenState_with_FixedCurve() { tokenState.increaseDebitBalance(by: 50.0) // Debit rate should be the per-second conversion of 10% yearly - let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.10) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.10) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // For FixedCurve, credit rate uses the SPREAD MODEL: @@ -196,7 +196,7 @@ fun test_TokenState_with_FixedCurve() { // debitRate = 0.10 // protocolFeeRate = 0.0 + 0.05 = 0.05 (default insuranceRate = 0.0, default stabilityFeeRate = 0.05) // creditYearly = 0.10 * (1 - 0.05) = 0.095 - let expectedCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.095) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.095) Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } @@ -226,7 +226,7 @@ fun test_TokenState_with_KinkCurve() { // Verify the debit rate let expectedYearlyRate: UFix128 = 0.0575 - let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: expectedYearlyRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedYearlyRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) } @@ -250,7 +250,7 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.increaseCreditBalance(by: 100.0) - let rateAtZeroUtilization = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.02) + let rateAtZeroUtilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) Test.assertEqual(rateAtZeroUtilization, tokenState.getCurrentDebitRate()) // Step 2: Add debt to create 50% utilization @@ -258,7 +258,7 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // rate = 0.02 + (0.05 × 0.50 / 0.80) = 0.02 + 0.03125 = 0.05125 tokenState.increaseDebitBalance(by: 50.0) - let rateAt50Utilization = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.05125) + let rateAt50Utilization = FlowALPMath.perSecondInterestRate(yearlyRate: 0.05125) Test.assertEqual(rateAt50Utilization, tokenState.getCurrentDebitRate()) // Step 3: Increase utilization to 90% (above kink) @@ -267,14 +267,14 @@ fun test_KinkCurve_rates_update_automatically_on_balance_change() { // rate = 0.02 + 0.05 + (0.50 × 0.50) = 0.32 tokenState.increaseDebitBalance(by: 40.0) - let rateAt90Util = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.32) + let rateAt90Util = FlowALPMath.perSecondInterestRate(yearlyRate: 0.32) Test.assertEqual(rateAt90Util, tokenState.getCurrentDebitRate()) // Step 4: Decrease debt to lower utilization back to 0% // credit: 100, debit: 0 → utilization = 0% → rate = baseRate = 2% tokenState.decreaseDebitBalance(by: 90.0) - let rateBackToZero = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.02) + let rateBackToZero = FlowALPMath.perSecondInterestRate(yearlyRate: 0.02) Test.assertEqual(rateBackToZero, tokenState.getCurrentDebitRate()) } diff --git a/cadence/tests/update_interest_rate_test.cdc b/cadence/tests/update_interest_rate_test.cdc index fc01ed10..bab33343 100644 --- a/cadence/tests/update_interest_rate_test.cdc +++ b/cadence/tests/update_interest_rate_test.cdc @@ -50,12 +50,12 @@ fun test_FixedCurve_uses_spread_model() { tokenState.increaseDebitBalance(by: 500.0) // 50% utilization // Debit rate should match the fixed yearly rate - let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = debitRate * (1 - protocolFeeRate) where protocolFeeRate = insuranceRate + stabilityFeeRate let expectedCreditYearly = UFix128(0.0999) // 0.10 * (1 - 0.001) - let expectedCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: expectedCreditYearly) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: expectedCreditYearly) Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } @@ -81,7 +81,7 @@ fun test_KinkCurve_uses_reserve_factor_model() { tokenState.increaseDebitBalance(by: 50.0) // 25% utilization // Debit rate should match the curve rate - let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate = (debitIncome - protocolFeeAmount) / creditBalance @@ -90,7 +90,7 @@ fun test_KinkCurve_uses_reserve_factor_model() { // protocolFeeRate = insuranceRate + stabilityFeeRate = 0.001 + 0.05 = 0.051 // protocolFeeAmount = 10 * 0.051 = 0.51 // creditYearly = (10 - 0.51) / 200 = 0.04745 - let expectedCreditRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: 0.04745) + let expectedCreditRate = FlowALPMath.perSecondInterestRate(yearlyRate: 0.04745) Test.assertEqual(expectedCreditRate, tokenState.getCurrentCreditRate()) } @@ -111,7 +111,7 @@ fun test_KinkCurve_zero_credit_rate_when_no_borrowing() { // No debit balance - zero utilization // Debit rate still follows the curve - let expectedDebitRate = FlowALPMath.perSecondInterestRate(nominalYearlyRate: debitRate) + let expectedDebitRate = FlowALPMath.perSecondInterestRate(yearlyRate: debitRate) Test.assertEqual(expectedDebitRate, tokenState.getCurrentDebitRate()) // Credit rate should be `one` (multiplicative identity = 0% growth) since no debit income to distribute From c66d9756cd0b0ee99f3081acd6e072cec1c3d8b6 Mon Sep 17 00:00:00 2001 From: Lionel LIMOL Date: Tue, 10 Mar 2026 22:19:42 -0400 Subject: [PATCH 20/25] Update cadence/contracts/FlowALPEvents.cdc Co-authored-by: Gornutz <90406700+Gornutz@users.noreply.github.com> --- cadence/contracts/FlowALPEvents.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc index 610c724e..74f6458f 100644 --- a/cadence/contracts/FlowALPEvents.cdc +++ b/cadence/contracts/FlowALPEvents.cdc @@ -141,7 +141,7 @@ access(all) contract FlowALPEvents { ) /// Emitted when the insurance rate for a token is updated by governance. - /// The insurance rate is a fee fraction of accrued debit interest diverted to the insurance fund. + /// The insurance rate is a fee of accrued debit interest diverted to the insurance fund. /// /// @param poolUUID the UUID of the pool containing the token /// @param tokenType the type identifier string of the token whose rate changed From 704b945d6804f219526167c9951770c5cd3bb740 Mon Sep 17 00:00:00 2001 From: Lionel LIMOL Date: Tue, 10 Mar 2026 22:19:54 -0400 Subject: [PATCH 21/25] Update cadence/contracts/FlowALPEvents.cdc Co-authored-by: Gornutz <90406700+Gornutz@users.noreply.github.com> --- cadence/contracts/FlowALPEvents.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc index 74f6458f..385b7ad8 100644 --- a/cadence/contracts/FlowALPEvents.cdc +++ b/cadence/contracts/FlowALPEvents.cdc @@ -145,7 +145,7 @@ access(all) contract FlowALPEvents { /// /// @param poolUUID the UUID of the pool containing the token /// @param tokenType the type identifier string of the token whose rate changed - /// @param insuranceRate the new insurance fee fraction (e.g. 0.001 for 0.1%) + /// @param insuranceRate the new insurance fee (e.g. 0.001 for 0.1%) access(all) event InsuranceRateUpdated( poolUUID: UInt64, tokenType: String, From ccdc62cfedabdb582fd9a5de0f7dc7928aed1745 Mon Sep 17 00:00:00 2001 From: Lionel LIMOL Date: Tue, 10 Mar 2026 22:20:01 -0400 Subject: [PATCH 22/25] Update cadence/contracts/FlowALPEvents.cdc Co-authored-by: Gornutz <90406700+Gornutz@users.noreply.github.com> --- cadence/contracts/FlowALPEvents.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc index 385b7ad8..d1342513 100644 --- a/cadence/contracts/FlowALPEvents.cdc +++ b/cadence/contracts/FlowALPEvents.cdc @@ -167,7 +167,7 @@ access(all) contract FlowALPEvents { ) /// Emitted when the stability fee rate for a token is updated by governance. - /// The stability fee rate is a fee fraction of accrued debit interest diverted to the stability fund. + /// The stability fee rate is a fee of accrued debit interest diverted to the stability fund. /// /// @param poolUUID the UUID of the pool containing the token /// @param tokenType the type identifier string of the token whose rate changed From 410e3ecfc08f202cacc9d2806fdc443215cc8bf3 Mon Sep 17 00:00:00 2001 From: Lionel LIMOL Date: Tue, 10 Mar 2026 22:20:10 -0400 Subject: [PATCH 23/25] Update cadence/contracts/FlowALPEvents.cdc Co-authored-by: Gornutz <90406700+Gornutz@users.noreply.github.com> --- cadence/contracts/FlowALPEvents.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowALPEvents.cdc b/cadence/contracts/FlowALPEvents.cdc index d1342513..0b029958 100644 --- a/cadence/contracts/FlowALPEvents.cdc +++ b/cadence/contracts/FlowALPEvents.cdc @@ -171,7 +171,7 @@ access(all) contract FlowALPEvents { /// /// @param poolUUID the UUID of the pool containing the token /// @param tokenType the type identifier string of the token whose rate changed - /// @param stabilityFeeRate the new stability fee fraction (e.g. 0.05 for 5%) + /// @param stabilityFeeRate the new stability fee (e.g. 0.05 for 5%) access(all) event StabilityFeeRateUpdated( poolUUID: UInt64, tokenType: String, From b33a64808d0e0c78afe7d1229a089e5820aa2956 Mon Sep 17 00:00:00 2001 From: Lionel LIMOL Date: Tue, 10 Mar 2026 22:20:24 -0400 Subject: [PATCH 24/25] Update cadence/tests/interest_accrual_integration_test.cdc Co-authored-by: Gornutz <90406700+Gornutz@users.noreply.github.com> --- cadence/tests/interest_accrual_integration_test.cdc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index 0302c932..4a97ff5b 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -50,7 +50,8 @@ access(all) var snapshot: UInt64 = 0 // Example at a 4% nominal yearly debit rate: // - Borrowers pay: 4.0% nominal yearly debit rate // - Lenders earn: a lower nominal yearly credit rate after protocol fees -// - Insurance/stability: configured fee fractions of accrued debit interest +// - Protocol Fees are comprised of two parts - +// - Insurance/Stability: configurable fees of accrued debit interest access(all) let moetFixedRate: UFix128 = 0.04 // 4% nominal yearly debit rate // FlowToken: KinkCurve (Aave v3 Volatile One Parameters) From abe25a74cda304b57c1dcc2aae263c2b0e5ffa30 Mon Sep 17 00:00:00 2001 From: Lionel LIMOL Date: Tue, 10 Mar 2026 22:20:32 -0400 Subject: [PATCH 25/25] Update cadence/tests/interest_accrual_integration_test.cdc Co-authored-by: Gornutz <90406700+Gornutz@users.noreply.github.com> --- cadence/tests/interest_accrual_integration_test.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/tests/interest_accrual_integration_test.cdc b/cadence/tests/interest_accrual_integration_test.cdc index 4a97ff5b..263f3241 100644 --- a/cadence/tests/interest_accrual_integration_test.cdc +++ b/cadence/tests/interest_accrual_integration_test.cdc @@ -898,7 +898,7 @@ fun test_flow_credit_accrues_interest_with_insurance() { // // Key Insight (FixedCurve Protocol-Fee Spread): // - debitRate is set by the fixed curve -// - insurance/stability remain configured fee fractions +// - insurance/stability remain configured fee parameters // - creditRate is reduced relative to debitRate by those protocol fees // - the realized spread shows up as a lower lender growth rate than borrower growth rate // =============================================================================