From edca7ebfbaa4a0e29deb1c9d54a9fbe5192feda1 Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Mon, 9 Mar 2026 17:26:12 +0100 Subject: [PATCH] attempt topUpSource rebalance before manual liquidation --- cadence/contracts/FlowALPv0.cdc | 4 + .../fork_multi_collateral_position_test.cdc | 341 +++++++++--------- cadence/tests/fork_oracle_failure_test.cdc | 34 +- cadence/tests/liquidation_phase1_test.cdc | 259 ++++++++----- cadence/tests/test_helpers.cdc | 25 +- .../flow-alp/position/provide_source.cdc | 25 ++ 6 files changed, 406 insertions(+), 282 deletions(-) create mode 100644 cadence/transactions/flow-alp/position/provide_source.cdc diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 62133be4..187507de 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -504,6 +504,10 @@ access(all) contract FlowALPv0 { self.lockPosition(pid) + // Attempt to restore position health via topUpSource before evaluating liquidatability. + // This prevents liquidations of positions that have sufficient backup funds configured. + self._rebalancePositionNoLock(pid: pid, force: false) + let positionView = self.buildPositionView(pid: pid) let balanceSheet = self._getUpdatedBalanceSheet(pid: pid) let initialHealth = balanceSheet.health diff --git a/cadence/tests/fork_multi_collateral_position_test.cdc b/cadence/tests/fork_multi_collateral_position_test.cdc index 3c848c2a..7e7d58f7 100644 --- a/cadence/tests/fork_multi_collateral_position_test.cdc +++ b/cadence/tests/fork_multi_collateral_position_test.cdc @@ -41,7 +41,7 @@ fun setup() { setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 2000.0) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_MOET_TOKEN_ID, price: 1.0) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: 40000.0) - + // Add FLOW as supported token (80% CF, 90% BF) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, @@ -51,7 +51,7 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - + // Add USDF as supported token (90% CF, 95% BF) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, @@ -61,7 +61,7 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - + // Add WETH as supported token (75% CF, 85% BF) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, @@ -81,7 +81,7 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - + snapshot = getCurrentBlockHeight() } @@ -93,13 +93,13 @@ fun setup() { access(all) fun test_multi_collateral_position() { safeReset() - + // STEP 1: Setup MOET liquidity provider for borrowing let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + let FLOWAmount = 1000.0 let USDFAmount = 500.0 let WETHAmount = 0.05 @@ -110,7 +110,7 @@ fun test_multi_collateral_position() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + transferFlowTokens(to: user, amount: FLOWAmount) transferFungibleTokens( tokenIdentifier: MAINNET_USDF_TOKEN_ID, @@ -124,10 +124,10 @@ fun test_multi_collateral_position() { to: user, amount: WETHAmount ) - + // STEP 3: Create position with FLOW collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid @@ -139,7 +139,7 @@ fun test_multi_collateral_position() { depositToPosition(signer: user, positionID: pid, amount: USDFAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) // STEP 5: Add WETH collateral depositToPosition(signer: user, positionID: pid, amount: WETHAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 @@ -147,7 +147,7 @@ fun test_multi_collateral_position() { // Total collateral: $1325 // // Debt: $0 - + // Verify all balances let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) @@ -156,18 +156,18 @@ fun test_multi_collateral_position() { Test.assertEqual(USDFAmount, usdfCredit) let wethCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_WETH_TOKEN_ID)!) Test.assertEqual(WETHAmount, wethCredit) - + // Health still infinite (no debt) health = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(CEILING_HEALTH, health) - + // STEP 6: Test weighted collateral factors - calculate max borrowing // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // MOET: maxBorrow = ($1325 / 1.1) * 1.0 / $1.00 = 1204.54545455 MOET let expectedMaxMoet: UFix64 = 1204.54545455 let availableMoet = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_MOET_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) Test.assertEqual(expectedMaxMoet, availableMoet) - + // STEP 7: Borrow 1204 MOET to create debt borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1204.0, beFailed: false) @@ -183,7 +183,7 @@ fun test_multi_collateral_position() { // Total debt: $1204 // // Health = $1325 / $1204 = 1.100498338870431893687707 - + health = getPositionHealth(pid: pid, beFailed: false) let expectedHealth: UFix128 = 1.100498338870431893687707 Test.assertEqual(expectedHealth, health) @@ -196,12 +196,12 @@ fun test_multi_collateral_position() { access(all) fun test_cross_asset_flow_to_usdf_borrowing() { safeReset() - + // STEP 1: Setup USDF liquidity provider let usdfLp = Test.createAccount() var res = setupGenericVault(usdfLp, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + transferFungibleTokens( tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, @@ -209,31 +209,31 @@ fun test_cross_asset_flow_to_usdf_borrowing() { amount: 10000.0 ) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // STEP 2: Setup test user with FLOW let user = Test.createAccount() setupMoetVault(user, beFailed: false) res = setupGenericVault(user, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 transferFlowTokens(to: user, amount: flowAmount) - + // STEP 3: Create position with FLOW collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * CF(0.8) = $800 // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max USDF: ($800 / 1.1) * 0.95 / $1.00 = ~ 690.909 USDF - + // STEP 4: Borrow USDF against FLOW collateral let usdfBorrowAmount: UFix64 = 600.0 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, amount: usdfBorrowAmount, beFailed: false) - + // Position state: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * CF(0.8) = $800 @@ -242,17 +242,17 @@ fun test_cross_asset_flow_to_usdf_borrowing() { // USDF: 600 * $1.00 / BF(0.95) = $631.58 // // Health = $800 / $631.58 = 1.266666666666666666666666 - + let health = getPositionHealth(pid: pid, beFailed: false) let expectedHealth: UFix128 = 1.266666666666666666666666 - + Test.assertEqual(expectedHealth, health) - + // Verify balances let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) Test.assertEqual(flowAmount, flowCredit) - + let usdfDebit = getDebitBalanceForType(details: details, vaultType: CompositeType(MAINNET_USDF_TOKEN_ID)!) Test.assertEqual(usdfBorrowAmount, usdfDebit) } @@ -265,19 +265,19 @@ fun test_cross_asset_flow_to_usdf_borrowing() { access(all) fun test_cross_asset_flow_usdf_weth_borrowing() { safeReset() - + // STEP 1: Setup liquidity providers for USDF and WETH let usdfLp = Test.createAccount() var res = setupGenericVault(usdfLp, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: usdfLp, amount: 10000.0) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + let wethLp = Test.createAccount() res = setupGenericVault(wethLp, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: wethLp, amount: 0.05) - + let tinyDeposit = 0.00000001 setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: wethLp, amount: 0.05, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) @@ -289,17 +289,17 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 transferFlowTokens(to: user, amount: flowAmount) - + // STEP 3: Create position with FLOW createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // @@ -309,18 +309,18 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { // // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max USDF: ($800 / 1.1) * 0.95 = 690.90909091 USDF - + var health: UFix128 = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(CEILING_HEALTH, health) - + let usdfBorrowAmount: UFix64 = 500.0 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, amount: usdfBorrowAmount, beFailed: false) - // Collateral (effectiveCollateral = balance * price * collateralFactor): + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt (effectiveDebt = balance * price / borrowFactor): + // Debt (effectiveDebt = balance * price / borrowFactor): // USDF: 500 * $1.00 / 0.95 = $526.315789474 // // Health = $800 / $526.315789474 = 1.52 @@ -330,34 +330,34 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { // STEP 4: Deposit borrowed USDF as collateral depositToPosition(signer: user, positionID: pid, amount: usdfBorrowAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // New collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 // Total collateral: $1250 // - // Debt (effectiveDebt = balance * price / borrowFactor): + // Debt (effectiveDebt = balance * price / borrowFactor): // USDF: 500 * $1.00 / 0.95 = $526.315789474 // // After netting USDF (credit 500 - debt 500 = 0): // - // Collateral: + // Collateral: // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // // Debt $0 // // Health = $800 / $0 = ∞ (UFix128.max) - + health = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(CEILING_HEALTH, health) - + // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max WETH: ($1250 / 1.1) * 0.85 / $2000 = ~0.48295454545 WETH // But we only have 0.05 WETH on pool available, so borrow 0.04 let wethBorrowAmount: UFix64 = 0.04 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, amount: wethBorrowAmount, beFailed: false) - + // Final position: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 @@ -369,13 +369,13 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { // // After netting USDF (credit 500 - debt 500 = 0): // - // Collateral: - // FLOW: 1000 * $1.00 * 0.8 = $800 + // Collateral: + // FLOW: 1000 * $1.00 * 0.8 = $800 // Debt: // WETH: 0.04 * $2000 / 0.85 = $94.117647059 // // Health = $800 / $94.117647059 = 8.5 - + health = getPositionHealth(pid: pid, beFailed: false) expectedHealth = 8.5 Test.assertEqual(expectedHealth, health) @@ -388,7 +388,7 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { access(all) fun test_cross_asset_chain() { safeReset() - + // STEP 1: Setup all liquidity providers // USDF LP let usdfLp = Test.createAccount() @@ -396,7 +396,7 @@ fun test_cross_asset_chain() { Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: usdfLp, amount: 1000.0) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 1000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // WETH LP (0.05 WETH available) let wethLp = Test.createAccount() res = setupGenericVault(wethLp, vaultIdentifier: MAINNET_WETH_TOKEN_ID) @@ -406,7 +406,7 @@ fun test_cross_asset_chain() { let tinyDeposit = 0.0000001 setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: wethLp, amount: 0.05, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // WBTC LP (0.0004 WBTC available) let wbtcLp = Test.createAccount() res = setupGenericVault(wbtcLp, vaultIdentifier: MAINNET_WBTC_TOKEN_ID) @@ -415,7 +415,7 @@ fun test_cross_asset_chain() { setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_ID, minimum: tinyDeposit) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: wbtcLp, amount: 0.0004, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, pushToDrawDownSink: false) - + // STEP 2: Setup user with FLOW position let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -425,50 +425,50 @@ fun test_cross_asset_chain() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WBTC_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 transferFlowTokens(to: user, amount: flowAmount) - + // STEP 3: Create position and execute complete chain createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Max borrow ((effectiveCollateral / minHealth) * borrowFactor / price): // Max USDF = ($800 / 1.1) * 0.95 / $1.0 = ~690.9090 USDF - + // Step 4: Borrow USDF let usdfBorrow: UFix64 = 600.0 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, amount: usdfBorrow, beFailed: false) - + // Step 5: Deposit USDF, borrow WETH (limited by available liquidity) depositToPosition(signer: user, positionID: pid, amount: usdfBorrow, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt: + // Debt: // USDF (0 netted) // // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max WETH = ($800 / 1.1) * 0.85 / $2000 = ~0.30909090 WETH // limited by available liquidity: 0.0005 max - let wethBorrow: UFix64 = 0.0005 + let wethBorrow: UFix64 = 0.0005 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, amount: wethBorrow, beFailed: false) - + // Step 6: Deposit WETH, borrow WBTC depositToPosition(signer: user, positionID: pid, amount: wethBorrow, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt: + // Debt: // USDF (0 netted) // WETH (0 netted) // @@ -478,7 +478,7 @@ fun test_cross_asset_chain() { // Limited by available liquidity (0.0004 total) let wbtcBorrow: UFix64 = 0.0004 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_ID, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, amount: wbtcBorrow, beFailed: false) - + // Final position: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 @@ -488,23 +488,23 @@ fun test_cross_asset_chain() { // WBTC: 0.0004 * $40000 / 0.8 = $20 // // Health = $800 / $20 = 40 - + let finalHealth = getPositionHealth(pid: pid, beFailed: false) let expectedHealth: UFix128 = 40.0 Test.assertEqual(expectedHealth, finalHealth) - + // Verify all balances let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_FLOW_TOKEN_ID)!) Test.assertEqual(1000.0, flowCredit) - + let usdfCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_USDF_TOKEN_ID)!) Test.assertEqual(0.0, usdfCredit) - + let wethCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_WETH_TOKEN_ID)!) Test.assertEqual(0.0, wethCredit) - + let wbtcDebit = getDebitBalanceForType(details: details, vaultType: CompositeType(MAINNET_WBTC_TOKEN_ID)!) Test.assertEqual(0.0004, wbtcDebit) } @@ -517,13 +517,13 @@ fun test_cross_asset_chain() { access(all) fun test_multi_asset_uncorrelated_price_movements() { safeReset() - + // STEP 1: Setup liquidity providers for MOET let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // STEP 2: Setup test user with FLOW, USDF, and WETH let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -531,25 +531,25 @@ fun test_multi_asset_uncorrelated_price_movements() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 let usdfAmount: UFix64 = 500.0 let wethAmount: UFix64 = 0.05 - + transferFlowTokens(to: user, amount: flowAmount) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: user, amount: usdfAmount) transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: wethAmount) - + // STEP 3: Create position with FLOW collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + // STEP 4: Add USDF and WETH collateral depositToPosition(signer: user, positionID: pid, amount: usdfAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) depositToPosition(signer: user, positionID: pid, amount: wethAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * CF(0.8) = $800 // USDF: 500 * $1.00 * CF(0.9) = $450 @@ -558,20 +558,20 @@ fun test_multi_asset_uncorrelated_price_movements() { // STEP 5: Borrow 1000 MOET to create debt borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1000.0, beFailed: false) - + // Position state after borrow at initial prices: // Collateral (effectiveCollateral = balance * price * collateralFactor): // Total collateral: $1325 (unchanged) // // Debt (effectiveDebt = balance * price / borrowFactor): // MOET: 1000 * $1.00 / BF(1.0) = $1000 - // + // // Health = $1325 / $1000 = 1.325 - + let initialHealth = getPositionHealth(pid: pid, beFailed: false) let expectedInitialHealth: UFix128 = 1.325 Test.assertEqual(expectedInitialHealth, initialHealth) - + // STEP 6: Test uncorrelated price movements // FLOW: $1.00 → $1.10 (+10%) @@ -582,7 +582,7 @@ fun test_multi_asset_uncorrelated_price_movements() { setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 0.95) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 2400.0) // MOET remains at $1.00 - + // New position state with changed prices: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.10 * CF(0.8) = $880 (was $800, +$80) @@ -594,10 +594,10 @@ fun test_multi_asset_uncorrelated_price_movements() { // MOET: 1000 * $1.00 / BF(1.0) = $1000 // // Health = $1397.50 / $1000 = 1.3975 - + let healthAfterChange = getPositionHealth(pid: pid, beFailed: false) let expectedHealthAfterChange: UFix128 = 1.3975 - + Test.assertEqual(expectedHealthAfterChange, healthAfterChange) } @@ -607,39 +607,39 @@ fun test_multi_asset_uncorrelated_price_movements() { access(all) fun test_multi_asset_partial_withdrawal() { safeReset() - + // STEP 1: Setup MOET liquidity provider // We need someone else to deposit MOET so there's liquidity for borrowing let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) - + // MOET LP deposits MOET (creates MOET credit balance = provides liquidity) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // STEP 2: Setup test user let user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: user.address, amount: 500.0, beFailed: false) - + // STEP 3: Create position with FLOW createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + // STEP 4: Add MOET collateral depositToPosition(signer: user, positionID: pid, amount: 500.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // Initial collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // MOET: 500 * $1.00 * 1.0 = $500 // Total collateral: $1300 - + // STEP 5: Borrow 400 MOET borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 400.0, beFailed: false) - + // Position state after borrow: // MOET borrow (netting): // 1) Had 500 MOET credit @@ -656,12 +656,12 @@ fun test_multi_asset_partial_withdrawal() { // MOET: $0 // // Health = $900 / $0 = ∞ (UFix128.max) - + let initialHealth = getPositionHealth(pid: pid, beFailed: false) - + // STEP 6: Withdraw 300 FLOW (partial withdrawal) borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 300.0, beFailed: false) - + // Position state after FLOW withdrawal: // FLOW withdrawal mechanics: // 1) Had 1000 FLOW credit @@ -678,16 +678,16 @@ fun test_multi_asset_partial_withdrawal() { // MOET: $0 // // Health = $660 / $0 = (no debt) - + let newHealth = getPositionHealth(pid: pid, beFailed: false) - + // Both healths are infinite (no debt), so they're equal // We can't test health decrease when there's no debt // Instead verify the collateral decreased let details = getPositionDetails(pid: pid, beFailed: false) let remainingFlow = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) Test.assertEqual(700.0, remainingFlow) - + let remainingMoet = getCreditBalanceForType(details: details, vaultType: Type<@MOET.Vault>()) Test.assertEqual(100.0, remainingMoet) @@ -704,14 +704,14 @@ fun test_multi_asset_partial_withdrawal() { access(all) fun test_cross_collateral_borrowing_capacity() { safeReset() - + // STEP 1: Setup MOET and USDF liquidity providers let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_USDF_HOLDER, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // STEP 2: Setup test user with FLOW + MOET collateral let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -719,15 +719,16 @@ fun test_cross_collateral_borrowing_capacity() { Test.expect(res, Test.beSucceeded()) transferFlowTokens(to: user, amount: 1000.0) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: user.address, amount: 900.0, beFailed: false) - + // STEP 3: Create position with FLOW + MOET collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + provideSource(signer: user, positionId: pid, source: nil) + depositToPosition(signer: user, positionID: pid, amount: 900.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * CF(0.8) = $800 // MOET: 900 * $1.00 * CF(1.0) = $900 @@ -736,7 +737,7 @@ fun test_cross_collateral_borrowing_capacity() { // Debt: $0 // // Health: ∞ (no debt) - + // STEP 4: Calculate position's balance available for withdrawal for each token // maxBorrow = (effectiveCollateral / minHealth) * borrowFactor / price // Using default minHealth = 1.1 @@ -744,17 +745,17 @@ fun test_cross_collateral_borrowing_capacity() { // MOET (credit token) -> 900 MOET (limited by credit balance: withdrawing deposited collateral, not new debt) // USDF (no balance, different from collateral, limited by health factor): maxBorrow = ($1700 / 1.1) * 0.95 / $1.00 = ~1468.18181818 USDF // FLOW (credit token): -> 1000 FLOW (limited by credit balance: withdrawing deposited collateral, not new debt) - + // Test MOET borrowing (limited by credit amount) let expectedMaxMoet: UFix64 = 900.0 let availableMoet = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_MOET_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) Test.assertEqual(expectedMaxMoet, availableMoet) - + // Test USDF borrowing (true cross-collateral calculation) let expectedMaxUsdf: UFix64 = 1468.18181818 let availableUsdf = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_USDF_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) Test.assertEqual(expectedMaxUsdf, availableUsdf) - + // Test FLOW borrowing (limited by credit amount) let expectedMaxFlow: UFix64 = 1000.0 let availableFlow = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_FLOW_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) @@ -769,19 +770,22 @@ fun test_cross_collateral_borrowing_capacity() { access(all) fun test_multi_asset_liquidation_collateral_selection() { safeReset() - + // STEP 1: Setup liquidity providers let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + let usdfLp = Test.createAccount() var res = setupGenericVault(usdfLp, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: usdfLp, amount: 10000.0) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + var openEvents = Test.eventsOfType(Type()) + var pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + provideSource(signer: usdfLp, positionId: pid, source: nil) + // STEP 2: Setup user with 3 collateral types let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -789,30 +793,31 @@ fun test_multi_asset_liquidation_collateral_selection() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 let usdfAmount: UFix64 = 500.0 let wethAmount: UFix64 = 0.05 - + transferFlowTokens(to: user, amount: flowAmount) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: user, amount: usdfAmount) transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: wethAmount) - + // STEP 3: Create position with all collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + + openEvents = Test.eventsOfType(Type()) + pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + provideSource(signer: user, positionId: pid, source: nil) + depositToPosition(signer: user, positionID: pid, amount: usdfAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) depositToPosition(signer: user, positionID: pid, amount: wethAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 // WETH: 0.05 * $2000 * 0.75 = $75 // Total collateral: $1325 - + // STEP 4: Create 2 debt types by borrowing // First borrow MOET // maxBorrow = (effectiveCollateral / minHealth) * borrowFactor / price @@ -849,7 +854,7 @@ fun test_multi_asset_liquidation_collateral_selection() { // Position now has: // Collateral (effectiveCollateral = balance * price * collateralFactor): - // FLOW: 1000 * $1.00 * 0.8 = $800 + // FLOW: 1000 * $1.00 * 0.8 = $800 // WETH: 0.05 * $2000 * 0.75 = $75 // Total collateral: $875 // @@ -859,7 +864,7 @@ fun test_multi_asset_liquidation_collateral_selection() { // Total debt: $752.63 // // Health = $875 / $752.63 = 1.163 (still healthy, will become unhealthy after price drop) - + let healthBefore = getPositionHealth(pid: pid, beFailed: false) Test.assert(healthBefore > 1.0, message: "Position should be healthy before price drop") @@ -873,14 +878,14 @@ fun test_multi_asset_liquidation_collateral_selection() { vaultSourceStoragePath: MOET.VaultStoragePath, priceRatio: 0.70 ) - + // New collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.7 * 0.8 = $560 // WETH: 0.05 * $2000 * 0.75 = $75 // Total collateral: $635 // // Debt (effectiveDebt = balance * price / borrowFactor): - // MOET: 700 * $1.00 / 1.0 = $700, + // MOET: 700 * $1.00 / 1.0 = $700, // USDF: 50 * $1.00 / 0.95 = $52.63 // Total debt: $752.63 // @@ -889,12 +894,12 @@ fun test_multi_asset_liquidation_collateral_selection() { let healthAfterDrop = getPositionHealth(pid: pid, beFailed: false) let expectedHealthAfterDrop: UFix128 = 0.843706293706293706293706 Test.assertEqual(expectedHealthAfterDrop, healthAfterDrop) - + // STEP 6: Liquidator chooses to seize FLOW collateral by repaying MOET debt let liquidator = Test.createAccount() setupMoetVault(liquidator, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: liquidator.address, amount: 1000.0, beFailed: false) - + // Repay 100 MOET, seize FLOW // DEX quote: 100 / 0.70 = 142.86 FLOW // Liquidator offers: 140 FLOW (better price) @@ -902,22 +907,22 @@ fun test_multi_asset_liquidation_collateral_selection() { let seizeAmount: UFix64 = 140.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, - seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + signer:liquidator, + pid: pid, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, + seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) Test.expect(liqRes, Test.beSucceeded()) - + // Verify balances after liquidation let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) let expectedFlowCredit: UFix64 = 860.0 // 1000 - 140 - Test.assertEqual(expectedFlowCredit, flowCredit) - + Test.assertEqual(expectedFlowCredit, flowCredit) + let moetDebit = getDebitBalanceForType(details: details, vaultType: Type<@MOET.Vault>()) let expectedMoetDebit: UFix64 = 600.0 // 700 - 100 Test.assertEqual(expectedMoetDebit, moetDebit) @@ -935,13 +940,13 @@ fun test_multi_asset_liquidation_collateral_selection() { access(all) fun test_multi_asset_complex_workflow() { safeReset() - + // STEP 1: Setup liquidity providers let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // STEP 2: User deposits FLOW collateral let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -949,40 +954,40 @@ fun test_multi_asset_complex_workflow() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + transferFlowTokens(to: user, amount: 1000.0) - + // STEP 3: Create position with FLOW createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - - // Collateral(effectiveCollateral = balance * price * collateralFactor): + + // Collateral(effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 - + // STEP 4: User deposits USDF collateral transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: user, amount: 500.0) depositToPosition(signer: user, positionID: pid, amount: 500.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 // Total collateral: $800 + $450 = $1250 - // + // // Debt: 0 - // + // // Health: ∞ (no debt) - + let healthAfterDeposits = getPositionHealth(pid: pid, beFailed: false) - + // STEP 5: User borrows MOET borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET.VaultStoragePath, amount: 300.0, beFailed: false) - + let healthAfterBorrow = getPositionHealth(pid: pid, beFailed: false) Test.assert(healthAfterBorrow > 1.0, message: "Position should be healthy after borrowing") - + // STEP 6: FLOW price drops 20% ($1.00 → $0.80) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.80) setMockDexPriceForPair( @@ -992,7 +997,7 @@ fun test_multi_asset_complex_workflow() { vaultSourceStoragePath: MOET.VaultStoragePath, priceRatio: 0.80 ) - + // New collateral calculation (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.80 * 0.8 = $640 (was $800) // USDF: 500 * $1.00 * 0.9 = $450 (unchanged) @@ -1003,14 +1008,14 @@ fun test_multi_asset_complex_workflow() { // Total debt: $300 // // Health: $1090 / $300 = 3.633 (still healthy but reduced) - + let healthAfterDrop = getPositionHealth(pid: pid, beFailed: false) - + // STEP 7: User borrows more to approach undercollateralization // Max borrow ((effectiveCollateral / minHealth) * borrowFactor / price): // Max MOET borrow = ($1090 / 1.1) * 1.0 / $1.0 = ~990.9090 MOET borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET.VaultStoragePath, amount: 600.0, beFailed: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.80 * 0.8 = $640 (was $800) // USDF: 500 * $1.00 * 0.9 = $450 (unchanged) @@ -1020,18 +1025,18 @@ fun test_multi_asset_complex_workflow() { // Total debt: $900 // // Health: $1090 / $900 = 1.211111111111111111111111 (close to minimum) - + let healthAfterSecondBorrow = getPositionHealth(pid: pid, beFailed: false) let expectedHealthAfterSecondBorrow:UFix128 = 1.211111111111111111111111 Test.assertEqual(expectedHealthAfterSecondBorrow, healthAfterSecondBorrow) - - // STEP 8: User deposits WETH as additional collateral + + // STEP 8: User deposits WETH as additional collateral // User deposits their WETH (0.05) directly to the position transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: 0.05) depositToPosition(signer: user, positionID: pid, amount: 0.05, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: true) // depositToPosition with pushToDrawDownSink=true: - // + // // 1. WETH is deposited → collateral increases from $1090 to $1165 // 2. Health calculation BEFORE rebalance: $1165 / $900 = 1.294444... // 3. System checks: Is health > targetHealth (1.3)? NO (1.294 < 1.3) @@ -1047,11 +1052,11 @@ fun test_multi_asset_complex_workflow() { // Check if rebalance event was emitted let rebalanceEvents = Test.eventsOfType(Type()) - Test.assertEqual(1, rebalanceEvents.length) + Test.assertEqual(1, rebalanceEvents.length) let lastRebalance = rebalanceEvents[rebalanceEvents.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(pid, lastRebalance.pid) Test.assertEqual(expectedPushedAmount, lastRebalance.amount) - + // After rebalance, position is at targetHealth (1.3) // Updated collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.80 * 0.8 = $640 @@ -1066,7 +1071,7 @@ fun test_multi_asset_complex_workflow() { // Health: $1165 / $896.15384615 = 1.300000000005579399141654 let expectedHealthAfterWethDeposit: UFix128 = 1.300000000005579399141654 let expectedDebtAfterRebalance: UFix64 = 896.15384615 - + let healthAfterWethDeposit = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(expectedHealthAfterWethDeposit, healthAfterWethDeposit) diff --git a/cadence/tests/fork_oracle_failure_test.cdc b/cadence/tests/fork_oracle_failure_test.cdc index 300a5d0a..ffbb6525 100644 --- a/cadence/tests/fork_oracle_failure_test.cdc +++ b/cadence/tests/fork_oracle_failure_test.cdc @@ -109,7 +109,7 @@ fun test_oracle_nil_price() { // ============================================================================= // ----------------------------------------------------------------------------- -/// Verifies that the protocol rejects a position when the PriceOracle returns a zero price. +/// Verifies that the protocol rejects a position when the PriceOracle returns a zero price. /// A zero price would cause division-by-zero in health calculations /// and incorrectly value collateral at $0. The PriceOracle interface /// guarantees `result! > 0.0`, so setting price to 0.0 must cause the oracle to revert. @@ -171,9 +171,9 @@ fun test_oracle_near_zero_price_extreme_health() { // STEP 2: Crash FLOW to near-zero ($0.00000001) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.00000001) - // Collateral: + // Collateral: // FLOW: 1000 * $0.00000001 * 0.8 = $0.000008 - // Debt: + // Debt: // MOET: 500 * $1.00 / 1.0 = $500 // // Health = $0.000008 / $500 = 0.000000016 (unhealty, essentially zero) @@ -215,9 +215,9 @@ fun test_oracle_very_large_price_no_overflow() { // STEP 2: Set WETH to extreme price (UFix64.max) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: UFix64.max) - // Collateral: + // Collateral: // WETH: 0.001 * $100,000,000 * 0.75 = $75,000 - // Debt: 0$ + // Debt: 0$ // // Health = infinite (UFix128.max) let health = getPositionHealth(pid: pid, beFailed: false) @@ -288,6 +288,7 @@ fun test_governance_tightens_dex_deviation_threshold() { let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + provideSource(signer: user, positionId: pid, source: nil) borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false) @@ -355,13 +356,14 @@ fun test_flash_crash_triggers_liquidation() { let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + provideSource(signer: user, positionId: pid, source: nil) borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 600.0, beFailed: false) - // Collateral: + // Collateral: // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 - // Debt: + // Debt: // MOET: 600 * $1.00 / 1.0 = $600 // Total debt: $600 // @@ -381,10 +383,10 @@ fun test_flash_crash_triggers_liquidation() { ) // New position state: - // Collateral: + // Collateral: // FLOW: 1000 * $0.5 * 0.8 = $400 // Total collateral: $400 - // Debt: + // Debt: // MOET: 600 * $1.00 / 1.0 = $600 // Total debt: $600 // @@ -431,7 +433,7 @@ fun test_flash_crash_triggers_liquidation() { // Tests that a position immediately reflects the new higher health. // Typical collateral factors are not sufficient to protect against sudden and dramatic price moves. // FlowALP relies on Oracle implementations to smooth out underlying price information or return no price -// at all when price information sources disagree. +// at all when price information sources disagree. // ----------------------------------------------------------------------------- access(all) fun test_flash_pump_increase_doubles_health() { @@ -455,11 +457,11 @@ fun test_flash_pump_increase_doubles_health() { borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 500.0, beFailed: false) - // Collateral: + // Collateral: // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt: + // Debt: // MOET: 500 * $1.00 / 1.0 = $500 // Total debt: $500 // @@ -473,11 +475,11 @@ fun test_flash_pump_increase_doubles_health() { setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 2.0) // New position state: - // Collateral: + // Collateral: // FLOW: 1000 * $2.00 * 0.8 = $1600 // Total collateral: $1600 // - // Debt: + // Debt: // MOET: 500 * $1.00 / 1.0 = $500 // Total debt: $500 // @@ -499,11 +501,11 @@ fun test_flash_pump_increase_doubles_health() { setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) // Position after correction: - // Collateral: + // Collateral: // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt: + // Debt: // MOET: (500+900) * $1.00 / 1.0 = $1400 // Total debt: $1400 // diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index da88b3d8..016d04b7 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -61,6 +61,7 @@ fun testManualLiquidation_healthyPosition() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // Log initial health let health = getPositionHealth(pid: pid, beFailed: false) @@ -77,12 +78,12 @@ fun testManualLiquidation_healthyPosition() { let repayAmount = 2.0 let seizeAmount = 1.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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") @@ -103,6 +104,8 @@ fun testManualLiquidation_liquidationExceedsTargetHealth() { // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) + // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -131,12 +134,12 @@ fun testManualLiquidation_liquidationExceedsTargetHealth() { let repayAmount = 500.0 let seizeAmount = 500.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -162,6 +165,7 @@ fun testManualLiquidation_repayExceedsDebt() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -194,12 +198,12 @@ fun testManualLiquidation_repayExceedsDebt() { let repayAmount = debtBalance + 0.001 let seizeAmount = (repayAmount / newPrice) * 0.99 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -225,6 +229,7 @@ fun testManualLiquidation_seizeExceedsCollateral() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -255,12 +260,12 @@ fun testManualLiquidation_seizeExceedsCollateral() { let seizeAmount = collateralBalance + 0.001 let repayAmount = seizeAmount * newPrice * 1.01 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -286,6 +291,7 @@ fun testManualLiquidation_reduceHealth() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -317,12 +323,12 @@ fun testManualLiquidation_reduceHealth() { let seizeAmount = collateralBalancePreLiq - 0.01 let repayAmount = seizeAmount * newPrice * 1.01 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -355,6 +361,7 @@ fun testManualLiquidation_increaseHealthBelowTarget() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // cause severe undercollateralization let newPrice = 0.5 // $/FLOW @@ -383,12 +390,12 @@ fun testManualLiquidation_increaseHealthBelowTarget() { let repayAmount = 100.0 let seizeAmount = 150.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should succeed Test.expect(liqRes, Test.beSucceeded()) @@ -417,6 +424,7 @@ fun testManualLiquidation_liquidateToTarget() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // cause undercollateralization let newPrice = 0.7 // $/FLOW @@ -451,12 +459,12 @@ fun testManualLiquidation_liquidateToTarget() { let repayAmount = 100.0 let seizeAmount = 33.66 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + signer:liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) // Should succeed Test.expect(liqRes, Test.beSucceeded()) @@ -482,6 +490,7 @@ fun testManualLiquidation_repaymentVaultCollateralType() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -535,6 +544,7 @@ fun testManualLiquidation_repaymentVaultTypeMismatch() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -590,6 +600,7 @@ fun testManualLiquidation_unsupportedDebtType() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -645,6 +656,7 @@ fun testManualLiquidation_unsupportedCollateralType() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // health before price drop let hBefore = getPositionHealth(pid: pid, beFailed: false) @@ -677,12 +689,12 @@ fun testManualLiquidation_unsupportedCollateralType() { let seizeAmount = collateralBalancePreLiq - 0.01 let repayAmount = seizeAmount * newPrice * 1.01 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -725,6 +737,7 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { // debt is MOET, collateral is FLOW let pid1: UInt64 = 0 createPosition(admin: PROTOCOL_ACCOUNT, signer: user1, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user1, positionId: pid1, source: nil) // user2 setup - deposits MockYieldToken let user2 = Test.createAccount() @@ -735,6 +748,7 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { // user2 opens wrapped position with MockYieldToken collateral let pid2: UInt64 = 1 createPosition(admin: PROTOCOL_ACCOUNT, signer: user2, amount: 1000.0, vaultStoragePath: MockYieldToken.VaultStoragePath, pushToDrawDownSink: true) + provideSource(signer: user2, positionId: pid2, source: nil) // health before price drop for user1 let hBefore = getPositionHealth(pid: pid1, beFailed: false) @@ -760,16 +774,16 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { let liqBalance = getBalance(address: liquidator.address, vaultPublicPath: MOET.VaultPublicPath) ?? 0.0 // Try to liquidate user1's position but repay MockYieldToken instead of MOET - // user1 has no MockYieldToken debt balance + // user1 has no MockYieldToken debt balance let seizeAmount = 0.01 let repayAmount = 100.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid1, - debtVaultIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -811,6 +825,7 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { // user1 opens wrapped position with FLOW collateral, MOET debt let pid1: UInt64 = 0 createPosition(admin: PROTOCOL_ACCOUNT, signer: user1, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user1, positionId: pid1, source: nil) // user2 setup - deposits MockYieldToken, borrows MOET let user2 = Test.createAccount() @@ -821,6 +836,7 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { // user2 opens wrapped position with MockYieldToken collateral let pid2: UInt64 = 1 createPosition(admin: PROTOCOL_ACCOUNT, signer: user2, amount: 1000.0, vaultStoragePath: MockYieldToken.VaultStoragePath, pushToDrawDownSink: true) + provideSource(signer: user2, positionId: pid2, source: nil) // health before price drop for user1 let hBefore = getPositionHealth(pid: pid1, beFailed: false) @@ -851,12 +867,12 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { let seizeAmount = 0.01 let repayAmount = 100.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid1, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -885,6 +901,7 @@ fun testManualLiquidation_dexOraclePriceDivergence_withinThreshold() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // cause undercollateralization let oraclePrice = 0.7 // $/FLOW @@ -913,12 +930,12 @@ fun testManualLiquidation_dexOraclePriceDivergence_withinThreshold() { let repayAmount = 50.0 let seizeAmount = 72.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -934,6 +951,7 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexBelowOracle() { setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // cause undercollateralization setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.7) @@ -951,12 +969,12 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexBelowOracle() { setupMoetVault(liquidator, beFailed: false) mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: 70.0, - repayAmount: 50.0, + 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()) @@ -973,6 +991,7 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexAboveOracle() { setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // cause undercollateralization setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: 0.7) @@ -990,12 +1009,12 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexAboveOracle() { setupMoetVault(liquidator, beFailed: false) mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: 66.0, - repayAmount: 50.0, + 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()) @@ -1016,6 +1035,7 @@ fun testManualLiquidation_liquidatorOfferWorseThanDex() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // cause undercollateralization let newPrice = 0.7 // $/FLOW @@ -1044,12 +1064,12 @@ fun testManualLiquidation_liquidatorOfferWorseThanDex() { let repayAmount = 50.0 let seizeAmount = 75.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) @@ -1070,6 +1090,7 @@ fun testManualLiquidation_combinedEdgeCase() { // open wrapped position and deposit via existing helper txs // debt is MOET, collateral is FLOW createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + provideSource(signer: user, positionId: pid, source: nil) // cause undercollateralization let oraclePrice = 0.7 // $/FLOW @@ -1100,15 +1121,71 @@ fun testManualLiquidation_combinedEdgeCase() { let repayAmount = 50.0 let seizeAmount = 75.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: Type<@MOET.Vault>().identifier, - seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + 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()) Test.assertError(liqRes, errorMessage: "DEX/oracle price deviation too large") } + +/// FLO-23: manualLiquidation should attempt rebalance via topUpSource before liquidating. +/// If the topUpSource has sufficient funds to restore health, the liquidation should be reverted. +access(all) +fun testManualLiquidation_topUpSourcePreventsLiquidation() { + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open position with pushToDrawDownSink=true, which wires the user's MOET vault as both + // drawDownSink and topUpSource + createPosition(admin: PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // The user now holds some MOET from the drawDownSink. Mint additional MOET so the topUpSource + // has enough funds to fully restore health after the price drop. + mintMoet(signer: Test.getAccount(0x0000000000000007), to: user.address, amount: 1000.0, beFailed: false) + + let userMoetBefore = getBalance(address: user.address, vaultPublicPath: MOET.VaultPublicPath)! + + // cause mild undercollateralization (price drop of 20%) + let newPrice = 0.7 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: newPrice) + setMockDexPriceForPair( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + outVaultIdentifier: MOET_TOKEN_IDENTIFIER, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) + + let healthAfterPriceDrop = getPositionHealth(pid: pid, beFailed: false) + Test.assert(healthAfterPriceDrop < 1.0, message: "position should be unhealthy after price drop") + + // attempt liquidation - should fail because rebalance restores health via topUpSource + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + let repayAmount = 50.0 + let seizeAmount = 50.0 + let liqRes = manualLiquidation( + signer: liquidator, + pid: pid, + debtVaultIdentifier: Type<@MOET.Vault>().identifier, + seizeVaultIdentifier: FLOW_TOKEN_IDENTIFIER, + seizeAmount: seizeAmount, + repayAmount: repayAmount, + ) + // Should fail because rebalance via topUpSource restored health above 1.0 + Test.expect(liqRes, Test.beFailed()) + Test.assertError(liqRes, errorMessage: "Cannot liquidate healthy position") +} diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 19d43bc0..a37409e2 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -2,6 +2,7 @@ import Test import "FlowALPv0" import "FlowALPModels" import "MOET" +import "DeFiActions" /* --- Global test constants --- */ @@ -736,7 +737,7 @@ fun collectStability( [ tokenTypeIdentifier ], signer ) - + return res } @@ -753,7 +754,7 @@ fun withdrawStabilityFund( [tokenTypeIdentifier, amount, recipient, recipientPath], signer ) - + return res } @@ -769,11 +770,11 @@ fun rebalancePosition(signer: Test.TestAccount, pid: UInt64, force: Bool, beFail access(all) fun manualLiquidation( - signer: Test.TestAccount, - pid: UInt64, - debtVaultIdentifier: String, - seizeVaultIdentifier: String, - seizeAmount: UFix64, + signer: Test.TestAccount, + pid: UInt64, + debtVaultIdentifier: String, + seizeVaultIdentifier: String, + seizeAmount: UFix64, repayAmount: UFix64, ): Test.TransactionResult { return _executeTransaction( @@ -879,6 +880,16 @@ fun withdrawReserve( Test.expect(txRes, beFailed ? Test.beFailed() : Test.beSucceeded()) } +access(all) +fun provideSource(signer: Test.TestAccount, positionId: UInt64, source: {DeFiActions.Source}?) { + let provideSourceRes = _executeTransaction( + "../transactions/flow-alp/position/provide_source.cdc", + [positionId, source], + signer + ) + Test.expect(provideSourceRes, Test.beSucceeded()) +} + /* --- Assertion Helpers --- */ access(all) fun equalWithinVariance(_ expected: AnyStruct, _ actual: AnyStruct): Bool { diff --git a/cadence/transactions/flow-alp/position/provide_source.cdc b/cadence/transactions/flow-alp/position/provide_source.cdc new file mode 100644 index 00000000..e23307fe --- /dev/null +++ b/cadence/transactions/flow-alp/position/provide_source.cdc @@ -0,0 +1,25 @@ +import "FungibleToken" +import "FlowALPv0" +import "FlowALPModels" +import "DeFiActions" + +/// Sets the top-up source on a position. +transaction( + positionId: UInt64, + source: {DeFiActions.Source}? +) { + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + + prepare(signer: auth(BorrowValue) &Account) { + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) + ?? panic("Could not find PositionManager in signer's storage") + + self.position = manager.borrowAuthorizedPosition(pid: positionId) + } + + execute { + self.position.provideSource(source: source) + } +}