From 90362831cb25ef6365bf2bdb27e0b598cde0c378 Mon Sep 17 00:00:00 2001 From: Taras Maliarchuk Date: Wed, 18 Feb 2026 20:24:24 +0100 Subject: [PATCH 01/15] 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/15] =?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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 )