From b53cc77bbf8daa9f543e4e74016b2d6ab2ee4edf Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Thu, 19 Feb 2026 15:29:46 +0200 Subject: [PATCH 1/9] Update mock oracle setPrice to accept optional value, updated transaction --- cadence/contracts/mocks/MockOracle.cdc | 2 +- cadence/tests/transactions/mock-oracle/set_price.cdc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/contracts/mocks/MockOracle.cdc b/cadence/contracts/mocks/MockOracle.cdc index 44deeab7..6ffa57aa 100644 --- a/cadence/contracts/mocks/MockOracle.cdc +++ b/cadence/contracts/mocks/MockOracle.cdc @@ -65,7 +65,7 @@ access(all) contract MockOracle { } } - access(all) fun setPrice(forToken: Type, price: UFix64) { + access(all) fun setPrice(forToken: Type, price: UFix64?) { self.mockedPrices[forToken] = price } diff --git a/cadence/tests/transactions/mock-oracle/set_price.cdc b/cadence/tests/transactions/mock-oracle/set_price.cdc index 285e9939..58de60bf 100644 --- a/cadence/tests/transactions/mock-oracle/set_price.cdc +++ b/cadence/tests/transactions/mock-oracle/set_price.cdc @@ -5,7 +5,7 @@ import "MockOracle" /// @param vaultIdentifier: The Vault's Type identifier /// e.g. vault.getType().identifier == 'A.0ae53cb6e3f42a79.FlowToken.Vault' /// @param price: The price to set the token to in the MockOracle denominated in USD -transaction(forTokenIdentifier: String, price: UFix64) { +transaction(forTokenIdentifier: String, price: UFix64?) { let tokenType: Type prepare(signer: &Account) { From 20f27318c263c9d9b6b21099b1ed012f111c2a61 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Thu, 19 Feb 2026 15:31:27 +0200 Subject: [PATCH 2/9] Add is_liquidatable script for flow-alp --- cadence/scripts/flow-alp/get_is_liquidatable.cdc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 cadence/scripts/flow-alp/get_is_liquidatable.cdc diff --git a/cadence/scripts/flow-alp/get_is_liquidatable.cdc b/cadence/scripts/flow-alp/get_is_liquidatable.cdc new file mode 100644 index 00000000..26a0f3df --- /dev/null +++ b/cadence/scripts/flow-alp/get_is_liquidatable.cdc @@ -0,0 +1,16 @@ +import "FlowALPv1" + +/// Returns whether a position is eligible for liquidation +/// +/// A position is liquidatable when its health factor is below 1.0, +/// indicating it has crossed the global liquidation threshold. +/// +/// @param pid: The unique identifier of the position. +/// @return `true` if the position can be liquidated, otherwise `false`. +access(all) fun main(pid: UInt64): Bool { + let protocolAddress = Type<@FlowALPv1.Pool>().address! + let pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv1.Pool>(FlowALPv1.PoolPublicPath) + ?? panic("Could not find Pool at path \(FlowALPv1.PoolPublicPath)") + + return pool.isLiquidatable(pid: pid) +} From 5c19615a857da83aed1508a82bb91d5e857cab35 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Thu, 19 Feb 2026 15:32:25 +0200 Subject: [PATCH 3/9] Update and add test helpers --- cadence/tests/test_helpers.cdc | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 61245519..f42b6cfa 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -341,6 +341,13 @@ fun getLastStabilityCollectionTime(tokenTypeIdentifier: String): UFix64? { return res.returnValue as? UFix64 } +access(all) +fun getIsLiquidatable(pid: UInt64): Bool { + let res = _executeScript("../scripts/flow-alp/get_is_liquidatable.cdc", [pid]) + Test.expect(res, Test.beSucceeded()) + return res.returnValue as! Bool +} + /* --- Transaction Helpers --- */ access(all) @@ -362,7 +369,7 @@ fun createAndStorePool(signer: Test.TestAccount, defaultTokenIdentifier: String, } access(all) -fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, price: UFix64) { +fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, price: UFix64?) { let setRes = _executeTransaction( "./transactions/mock-oracle/set_price.cdc", [forTokenIdentifier, price], @@ -391,6 +398,19 @@ fun setMockDexPriceForPair( Test.expect(addRes, Test.beSucceeded()) } +access(all) +fun setDexLiquidationConfig( + signer: Test.TestAccount, + dexOracleDeviationBps: UInt16, +) { + let addRes = _executeTransaction( + "./../transactions/flow-alp/pool-governance/set_dex_liquidation_config.cdc", + [dexOracleDeviationBps], + signer + ) + Test.expect(addRes, Test.beSucceeded()) +} + access(all) fun addSupportedTokenZeroRateCurve( signer: Test.TestAccount, From eafc4bfc0b7d56266ac3c487b381864cd46fed25 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Fri, 20 Feb 2026 14:57:10 +0200 Subject: [PATCH 4/9] Update script according to renaming changes --- cadence/scripts/flow-alp/get_is_liquidatable.cdc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadence/scripts/flow-alp/get_is_liquidatable.cdc b/cadence/scripts/flow-alp/get_is_liquidatable.cdc index 26a0f3df..2d1b7f03 100644 --- a/cadence/scripts/flow-alp/get_is_liquidatable.cdc +++ b/cadence/scripts/flow-alp/get_is_liquidatable.cdc @@ -1,4 +1,4 @@ -import "FlowALPv1" +import "FlowALPv0" /// Returns whether a position is eligible for liquidation /// @@ -8,9 +8,9 @@ import "FlowALPv1" /// @param pid: The unique identifier of the position. /// @return `true` if the position can be liquidated, otherwise `false`. access(all) fun main(pid: UInt64): Bool { - let protocolAddress = Type<@FlowALPv1.Pool>().address! - let pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv1.Pool>(FlowALPv1.PoolPublicPath) - ?? panic("Could not find Pool at path \(FlowALPv1.PoolPublicPath)") + let protocolAddress = Type<@FlowALPv0.Pool>().address! + let pool = getAccount(protocolAddress).capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not find Pool at path \(FlowALPv0.PoolPublicPath)") return pool.isLiquidatable(pid: pid) } From b65ed93fea325a75c5bfb2e3fec93e5da8fee4c1 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Fri, 20 Feb 2026 15:04:04 +0200 Subject: [PATCH 5/9] Add tests for oracle and price manipulation --- cadence/tests/fork_oracle_failure_test.cdc | 697 +++++++++++++++++++++ 1 file changed, 697 insertions(+) create mode 100644 cadence/tests/fork_oracle_failure_test.cdc diff --git a/cadence/tests/fork_oracle_failure_test.cdc b/cadence/tests/fork_oracle_failure_test.cdc new file mode 100644 index 00000000..a412dbf5 --- /dev/null +++ b/cadence/tests/fork_oracle_failure_test.cdc @@ -0,0 +1,697 @@ +#test_fork(network: "mainnet", height: 142528994) + +import Test +import BlockchainHelpers + +import "FlowToken" +import "FungibleToken" +import "MOET" +import "FlowALPv0" +import "DeFiActions" +import "MockOracle" + +import "test_helpers.cdc" + +access(all) let protocolAccount = Test.getAccount(0x6b00ff876c299c61) +access(all) let MAINNET_USDF_HOLDER = Test.getAccount(0xf18b50870aed46ad) +access(all) let MAINNET_WETH_HOLDER = Test.getAccount(0xf62e3381a164f993) + +access(all) var snapshot: UInt64 = 0 + +// Storage paths +access(all) let USDF_STORAGE_PATH = /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault +access(all) let WETH_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Vault +access(all) let MOET_STORAGE_PATH = /storage/moetTokenVault_0x6b00ff876c299c61 + +access(all) +fun safeReset() { + let cur = getCurrentBlockHeight() + if cur > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + var err = Test.deployContract( + name: "DeFiActionsUtils", + path: "../../FlowActions/cadence/contracts/utils/DeFiActionsUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPMath", + path: "../lib/FlowALPMath.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "DeFiActions", + path: "../../FlowActions/cadence/contracts/interfaces/DeFiActions.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "MockOracle", + path: "../contracts/mocks/MockOracle.cdc", + arguments: [MAINNET_MOET_TOKEN_ID] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FungibleTokenConnectors", + path: "../../FlowActions/cadence/contracts/connectors/FungibleTokenConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "MockDexSwapper", + path: "../contracts/mocks/MockDexSwapper.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "FlowALPv0", + path: "../contracts/FlowALPv0.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false) + + // Set initial oracle prices (baseline) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 2000.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_MOET_TOKEN_ID, price: 1.0) + + // Add FLOW as supported token (80% CF, 90% BF) + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, + collateralFactor: 0.8, + borrowFactor: 0.9, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // Add USDF as supported token (90% CF, 95% BF) + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, + collateralFactor: 0.9, + borrowFactor: 0.95, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + // Add WETH as supported token (75% CF, 85% BF) + addSupportedTokenZeroRateCurve( + signer: protocolAccount, + tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, + collateralFactor: 0.75, + borrowFactor: 0.85, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + snapshot = getCurrentBlockHeight() +} + +// ============================================================================= +// Price feed failure +// ============================================================================= + +// test_oracle_nil_price tests scenario when the oracle has no price for a token, any operation that requires +// pricing (health check, borrow, withdraw) must revert. +// The PriceOracle interface allows returning nil, but the Pool force-unwraps +// with `self.priceOracle.price(ofToken: type)!` — so nil triggers a panic. +access(all) +fun test_oracle_nil_price() { + safeReset() + + // STEP 1: Setup user with FLOW position + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // STEP 2: Remove FLOW price from oracle + let res = setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: nil) + + // STEP 3: Attempting to read position health should revert + // The pool's positionHealth() calls `self.priceOracle.price(ofToken: type)!` which panics on nil. + // because the oracle returns nil for FLOW and the pool force-unwraps it + let health = getPositionHealth(pid: pid, beFailed: true) +} + +// ============================================================================= +// Invalid price data +// ============================================================================= + +// ----------------------------------------------------------------------------- +/// 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. +// ----------------------------------------------------------------------------- +access(all) +fun test_oracle_zero_price() { + safeReset() + + // STEP 1: Attempt to set FLOW price to 0.0 + // The PriceOracle interface postcondition requires price > 0.0. + // The MockOracle's price() function should revert on zero. + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.0) + + // STEP 2: Setup user with FLOW position + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // STEP 3: Attempting to create position should fail + grantBetaPoolParticipantAccess(protocolAccount, user) + + let openRes = _executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [1000.0, FLOW_VAULT_STORAGE_PATH, false], + user + ) + Test.expect(openRes, Test.beFailed()) + Test.assertError(openRes, errorMessage: "PriceOracle must return a price greater than 0.0 if available") +} + +// ----------------------------------------------------------------------------- +// Oracle Returns Extremely Small Price (Near-Zero) +// A price like 0.00000001 is technically > 0 and passes +// the interface check, but may cause overflow or extreme health values. +// Tests that the protocol handles micro-prices correctly. +// ----------------------------------------------------------------------------- +access(all) +fun test_oracle_near_zero_price_extreme_health() { + safeReset() + + // STEP 1: Setup MOET LP + user with FLOW collateral + MOET debt + let moetLp = Test.createAccount() + setupMoetVault(moetLp, beFailed: false) + mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) + createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // Borrow 500 MOET + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 500.0, beFailed: false) + + // STEP 2: Crash FLOW to near-zero ($0.00000001) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.00000001) + + // Collateral: + // FLOW: 1000 * $0.00000001 * 0.8 = $0.000008 + // Debt: + // MOET: 500 * $1.00 / 1.0 = $500 + // + // Health = $0.000008 / $500 = 0.000000016 (unhealty, essentially zero) + let expectedHealth: UFix128 = 0.000000016 + let health = getPositionHealth(pid: pid, beFailed: false) + Test.assertEqual(expectedHealth, health) + + // Position should be liquidatable + let isLiquidatable = getIsLiquidatable(pid: pid) + Test.assertEqual(true, isLiquidatable) +} + +// ----------------------------------------------------------------------------- +// Oracle Returns Extremely Large Price (UFix64 Max) +// Tests that extremely large prices don't overflow internal UFix128 math. +// A WETH price of UFix64.max should be within safe bounds. +// ----------------------------------------------------------------------------- +access(all) +fun test_oracle_very_large_price_no_overflow() { + safeReset() + + // STEP 1: Setup user with WETH position + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + var res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) + Test.expect(res, Test.beSucceeded()) + + let wethAmount: UFix64 = 0.001 + transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: wethAmount) + + let tinyDeposit = 0.00000001 + setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) + + createPosition(admin: protocolAccount, signer: user, amount: wethAmount, vaultStoragePath: WETH_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + // STEP 2: Set WETH to extreme price (UFix64.max) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: UFix64.max) + + // Collateral: + // WETH: 0.001 * $100,000,000 * 0.75 = $75,000 + // Debt: 0$ + // + // Health = infinite (UFix128.max) + let health = getPositionHealth(pid: pid, beFailed: false) + Test.assertEqual(CEILING_HEALTH, health) + + // Verify available balance doesn't overflow + let available = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_WETH_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) + Test.assertEqual(wethAmount, available) +} + +// ============================================================================= +// DEX-Oracle price deviation utility function +// ============================================================================= + +// ----------------------------------------------------------------------------- +// dexOraclePriceDeviationInRange — Boundary Cases +// Tests the pure helper function that computes deviation in basis points. +// ----------------------------------------------------------------------------- +access(all) +fun test_dex_oracle_deviation_boundary_exact_threshold() { + safeReset() + + // Exactly at 300 bps (3%) — should pass + // Oracle: $1.00, DEX: $1.03 → deviation = |1.03-1.00|/1.00 = 3.0% = 300 bps + var res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 1.03, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(true, res) + + // One basis point over — should fail + // Oracle: $1.00, DEX: $1.0301 → deviation = 3.01% = 301 bps + res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 1.0301, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(false, res) + + // DEX below oracle — same threshold applies + // Oracle: $1.00, DEX: $0.97 → deviation = |0.97-1.00|/0.97 = 3.09% = 309 bps + res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 0.97, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(false, res) + + // DEX: $0.971 → deviation = |0.971-1.00|/0.971 = 2.98% = 298 bps + res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 0.971, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(true, res) + + // Equal prices — zero deviation — always passes + res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 1.0, oraclePrice: 1.0, maxDeviationBps: 0) + Test.assertEqual(true, res) +} + +// ============================================================================= +// DEX/Oracle price deviation and circuit breaker +// ============================================================================= + +// ----------------------------------------------------------------------------- +// DEX Price Deviates Beyond Threshold — Liquidation Blocked +// The protocol has a `dexOracleDeviationBps` parameter (default 300 = 3%). +// If the DEX-implied price deviates from the oracle by more than this +// threshold, liquidation must be rejected to prevent price manipulation. +// ----------------------------------------------------------------------------- +access(all) +fun test_dex_oracle_deviation_blocks_liquidation() { + safeReset() + + // STEP 1: Setup MOET LP + user with unhealthy position + let moetLp = Test.createAccount() + setupMoetVault(moetLp, beFailed: false) + mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) + createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 700.0, beFailed: false) + + // Drop FLOW oracle price to make position unhealthy + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) + + // Collateral: + // FLOW: 1000 * $0.70 * 0.8 = $560 + // Debt: + // MOET: 700 * $1.00 / 1.0 = $700 + // + // Health = $560 / $700 = 0.8 (unhealthy) + let expectedHealth: UFix128 = 0.8 + let health = getPositionHealth(pid: pid, beFailed: false) + Test.assertEqual(expectedHealth, health) + + // STEP 2: Set DEX price to deviate MORE than 3% from oracle + // Oracle says FLOW = $0.70, DEX says FLOW = $0.60 (14.3% deviation) + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MOET.VaultStoragePath, + priceRatio: 0.60 + ) + + // STEP 3: Attempt liquidation — should fail due to DEX/oracle deviation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: protocolAccount, to: liquidator.address, amount: 500.0, beFailed: false) + + // DEX quote: 100 / 0.60 = 166.67 FLOW + // Liquidator offers 160 FLOW (better than DEX) + // But the DEX/oracle deviation check: |0.60 - 0.70| / 0.60 = 16.7% >> 3% + let liqRes = manualLiquidation( + signer: liquidator, + pid: pid, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, + seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + seizeAmount: 160.0, + repayAmount: 100.0, + ) + Test.expect(liqRes, Test.beFailed()) + Test.assertError(liqRes, errorMessage: "DEX/oracle price deviation too large") +} + +// ----------------------------------------------------------------------------- +// DEX Price Within Threshold — Liquidation Succeeds +// Verify that when the DEX price is within the configured deviation +// threshold, liquidation proceeds normally. +// ----------------------------------------------------------------------------- +access(all) +fun test_dex_oracle_within_threshold_liquidation_succeeds() { + safeReset() + + // STEP 1: Setup MOET LP + user with unhealthy position + let moetLp = Test.createAccount() + setupMoetVault(moetLp, beFailed: false) + mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) + createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 700.0, beFailed: false) + + // Drop FLOW oracle price to make position unhealthy + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) + + // Set DEX price within 3% of oracle: $0.70 * 0.98 = $0.686 + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MOET.VaultStoragePath, + priceRatio: 0.686 + ) + + // STEP 2: Liquidation should succeed + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: protocolAccount, to: liquidator.address, amount: 500.0, beFailed: false) + + // DEX quote: 100 / 0.686 = ~145.77 FLOW + // Liquidator offers 140 FLOW (better than DEX) + let liqRes = manualLiquidation( + signer: liquidator, + pid: pid, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, + seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + seizeAmount: 140.0, + repayAmount: 100.0, + ) + Test.expect(liqRes, Test.beSucceeded()) +} + +// ----------------------------------------------------------------------------- +// Governance Adjusts DEX Deviation Threshold +// Tests that governance can tighten or loosen the circuit breaker. +// A tighter threshold (e.g., 100 bps = 1%) should block liquidations +// that would be allowed under the default 300 bps. +// ----------------------------------------------------------------------------- +access(all) +fun test_governance_tightens_dex_deviation_threshold() { + safeReset() + + // STEP 1: Setup MOET LP + unhealthy position + let moetLp = Test.createAccount() + setupMoetVault(moetLp, beFailed: false) + mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) + createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 700.0, beFailed: false) + + // Make position unhealthy + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) + + // DEX price within default 3% threshold but outside 1% + // Oracle: $0.70, DEX: $0.685 → deviation = |0.685-0.70|/0.685 = 2.19% + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MOET.VaultStoragePath, + priceRatio: 0.685 + ) + + // STEP 2: Tighten threshold to 100 bps (1%) + setDexLiquidationConfig(signer: protocolAccount, dexOracleDeviationBps: 100) + + // STEP 3: Liquidation should now fail (2.19% > 1% threshold) + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: protocolAccount, to: liquidator.address, amount: 500.0, beFailed: false) + + let liqRes = manualLiquidation( + signer: liquidator, + pid: pid, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, + seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + seizeAmount: 140.0, + repayAmount: 100.0, + ) + Test.expect(liqRes, Test.beFailed()) + Test.assertError(liqRes, errorMessage: "DEX/oracle price deviation too large") +} + + +// ============================================================================= +// Extreme price scenarious +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Flash Crash: 50% Price Drop in a Single Block +// FLOW drops from $1.00 to $0.50 between two operations. +// Tests that a previously healthy multi-collateral position becomes +// immediately liquidatable, and that liquidation works correctly at the +// post-crash price. +// ----------------------------------------------------------------------------- +access(all) +fun test_flash_crash_triggers_liquidation() { + safeReset() + + // STEP 1: Setup MOET liquidity provider + let moetLp = Test.createAccount() + setupMoetVault(moetLp, beFailed: false) + mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) + createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + + // STEP 2: Setup user with FLOW collateral + moderate MOET debt + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 600.0, beFailed: false) + + // Collateral: + // FLOW: 1000 * $1.00 * 0.8 = $800 + // Total collateral: $800 + // Debt: + // MOET: 600 * $1.00 / 1.0 = $600 + // Total debt: $600 + // + // Health = $800 / $600 = 1.333... (healthy) + + let healthBefore = getPositionHealth(pid: pid, beFailed: false) + Test.assert(healthBefore > 1.0, message: "Position should be healthy before crash") + + // STEP 3: Flash crash — FLOW drops 50% ($1.00 → $0.50) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.50) + setMockDexPriceForPair( + signer: protocolAccount, + inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + outVaultIdentifier: MAINNET_MOET_TOKEN_ID, + vaultSourceStoragePath: MOET.VaultStoragePath, + priceRatio: 0.50 + ) + + // New position state: + // Collateral: + // FLOW: 1000 * $0.5 * 0.8 = $400 + // Total collateral: $400 + // Debt: + // MOET: 600 * $1.00 / 1.0 = $600 + // Total debt: $600 + // + // Health = $400 / $600 = 0.666... (unhealthy) + let healthAfterCrash = getPositionHealth(pid: pid, beFailed: false) + let expectedHealthAfterCrash: UFix128 = 0.666666666666666666666666 + Test.assertEqual(expectedHealthAfterCrash, healthAfterCrash) + Test.assert(healthAfterCrash < 1.0, message: "Position should be unhealthy after 50% crash") + + // STEP 4: Verify the position is liquidatable + let isLiquidatable = getIsLiquidatable(pid: pid) + Test.assertEqual(true, isLiquidatable) + + // STEP 5: Execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: protocolAccount, to: liquidator.address, amount: 1000.0, beFailed: false) + + // Repay 100 MOET, seize FLOW + // DEX quote: 100 / 0.50 = 200 FLOW + // Liquidator offers: 195 FLOW (better than DEX) + let liqRes = manualLiquidation( + signer: liquidator, + pid: pid, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, + seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + seizeAmount: 195.0, + repayAmount: 100.0, + ) + Test.expect(liqRes, Test.beSucceeded()) + + // Verify post-liquidation state + let details = getPositionDetails(pid: pid, beFailed: false) + let flowCredit = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) + Test.assertEqual(805.0, flowCredit) // 1000 - 195 + + let moetDebit = getDebitBalanceForType(details: details, vaultType: Type<@MOET.Vault>()) + Test.assertEqual(500.0, moetDebit) // 600 - 100 +} + +// ----------------------------------------------------------------------------- +// Flash Pump: 100% Price Increase in a Single Block +// FLOW doubles from $1.00 to $2.00. +// Tests that a position immediately reflects the new higher health, +// and that the user cannot exploit the pump by borrowing excessively +// before the price corrects. +// ----------------------------------------------------------------------------- +access(all) +fun test_flash_pump_increase_doubles_health() { + safeReset() + + // STEP 1: Setup MOET liquidity provider + let moetLp = Test.createAccount() + setupMoetVault(moetLp, beFailed: false) + mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) + createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) + + // STEP 2: Setup user with FLOW collateral and MOET debt + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 500.0, beFailed: false) + + // Collateral: + // FLOW: 1000 * $1.00 * 0.8 = $800 + // Total collateral: $800 + // + // Debt: + // MOET: 500 * $1.00 / 1.0 = $500 + // Total debt: $500 + // + // Health = $800 / $500 = 1.6 (healthy) + + let healthBefore = getPositionHealth(pid: pid, beFailed: false) + let expectedHealthBefore: UFix128 = 1.6 + Test.assertEqual(expectedHealthBefore, healthBefore) + + // STEP 3: Flash pump — FLOW doubles ($1.00 → $2.00) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 2.0) + + // New position state: + // Collateral: + // FLOW: 1000 * $2.00 * 0.8 = $1600 + // Total collateral: $1600 + // + // Debt: + // MOET: 500 * $1.00 / 1.0 = $500 + // Total debt: $500 + // + // Health = $1600 / $500 = 3.2 (healthy) + + let healthAfterPump = getPositionHealth(pid: pid, beFailed: false) + let expectedHealthAfterPump: UFix128 = 3.2 + Test.assertEqual(expectedHealthAfterPump, healthAfterPump) + + // STEP 4: User tries to borrow max at pumped price + // Max borrow = ($1600 / 1.1) * 1.0 / $1.0 - $500 = ~954.545 additional MOET + let availableMoet = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_MOET_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) + Test.assert(availableMoet > 900.0, message: "User should be able to borrow significantly more at pumped price") + + // STEP 5: User borrows at pumped price, then price corrects back + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 900.0, beFailed: false) + + // Price corrects back to $1.00 + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) + + // Position after correction: + // Collateral: + // FLOW: 1000 * $1.00 * 0.8 = $800 + // Total collateral: $800 + // + // Debt: + // MOET: (500+900) * $1.00 / 1.0 = $1400 + // Total debt: $1400 + // + // Health = $800 / $1400 = 0.571... (severely unhealthy) + + // This demonstrates the danger of flash pump exploitation: + // The user borrowed at inflated prices and is now underwater. + // The protocol's collateral factors provide some buffer, but cannot + // fully protect against 100% price swings. + let healthAfterCorrection = getPositionHealth(pid: pid, beFailed: false) + Test.assert(healthAfterCorrection < 1.0, message: "Position should be underwater after pump-and-dump scenario") +} \ No newline at end of file From fb27d178d1a3f0c80e936757dc1f9df30aea9379 Mon Sep 17 00:00:00 2001 From: Uliana Andrukhiv Date: Tue, 3 Mar 2026 17:21:41 +0200 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: Kan Zhang Co-authored-by: Jordan Schalm --- cadence/tests/fork_oracle_failure_test.cdc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cadence/tests/fork_oracle_failure_test.cdc b/cadence/tests/fork_oracle_failure_test.cdc index a412dbf5..c7970191 100644 --- a/cadence/tests/fork_oracle_failure_test.cdc +++ b/cadence/tests/fork_oracle_failure_test.cdc @@ -506,7 +506,7 @@ fun test_governance_tightens_dex_deviation_threshold() { // ============================================================================= -// Extreme price scenarious +// Extreme price scenarios // ============================================================================= // ----------------------------------------------------------------------------- @@ -608,9 +608,10 @@ fun test_flash_crash_triggers_liquidation() { // ----------------------------------------------------------------------------- // Flash Pump: 100% Price Increase in a Single Block // FLOW doubles from $1.00 to $2.00. -// Tests that a position immediately reflects the new higher health, -// and that the user cannot exploit the pump by borrowing excessively -// before the price corrects. +// 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. // ----------------------------------------------------------------------------- access(all) fun test_flash_pump_increase_doubles_health() { @@ -692,6 +693,8 @@ fun test_flash_pump_increase_doubles_health() { // The user borrowed at inflated prices and is now underwater. // The protocol's collateral factors provide some buffer, but cannot // fully protect against 100% price swings. + // The protocol also relies on Oracle implementations to smooth out price information or report no price + // when information sources disagree. let healthAfterCorrection = getPositionHealth(pid: pid, beFailed: false) Test.assert(healthAfterCorrection < 1.0, message: "Position should be underwater after pump-and-dump scenario") } \ No newline at end of file From 8ee68b4841907813b55193fe67cce1534df6f21c Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Tue, 3 Mar 2026 17:38:28 +0200 Subject: [PATCH 7/9] Merged with branch UlianaAndrukhiv/148-multiple-collateral-testing --- cadence/tests/fork_oracle_failure_test.cdc | 116 ++++++--------------- flow.json | 1 - 2 files changed, 33 insertions(+), 84 deletions(-) diff --git a/cadence/tests/fork_oracle_failure_test.cdc b/cadence/tests/fork_oracle_failure_test.cdc index c7970191..47c495b2 100644 --- a/cadence/tests/fork_oracle_failure_test.cdc +++ b/cadence/tests/fork_oracle_failure_test.cdc @@ -1,4 +1,4 @@ -#test_fork(network: "mainnet", height: 142528994) +#test_fork(network: "mainnet-fork", height: 142528994) import Test import BlockchainHelpers @@ -9,6 +9,8 @@ import "MOET" import "FlowALPv0" import "DeFiActions" import "MockOracle" +import "FlowALPEvents" +import "FlowALPMath" import "test_helpers.cdc" @@ -18,11 +20,6 @@ access(all) let MAINNET_WETH_HOLDER = Test.getAccount(0xf62e3381a164f993) access(all) var snapshot: UInt64 = 0 -// Storage paths -access(all) let USDF_STORAGE_PATH = /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault -access(all) let WETH_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Vault -access(all) let MOET_STORAGE_PATH = /storage/moetTokenVault_0x6b00ff876c299c61 - access(all) fun safeReset() { let cur = getCurrentBlockHeight() @@ -33,54 +30,7 @@ fun safeReset() { access(all) fun setup() { - var err = Test.deployContract( - name: "DeFiActionsUtils", - path: "../../FlowActions/cadence/contracts/utils/DeFiActionsUtils.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "FlowALPMath", - path: "../lib/FlowALPMath.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "DeFiActions", - path: "../../FlowActions/cadence/contracts/interfaces/DeFiActions.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "MockOracle", - path: "../contracts/mocks/MockOracle.cdc", - arguments: [MAINNET_MOET_TOKEN_ID] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "FungibleTokenConnectors", - path: "../../FlowActions/cadence/contracts/connectors/FungibleTokenConnectors.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "MockDexSwapper", - path: "../contracts/mocks/MockDexSwapper.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - err = Test.deployContract( - name: "FlowALPv0", - path: "../contracts/FlowALPv0.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) + deployContracts() createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false) @@ -142,8 +92,8 @@ fun test_oracle_nil_price() { createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid // STEP 2: Remove FLOW price from oracle let res = setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: nil) @@ -212,11 +162,11 @@ fun test_oracle_near_zero_price_extreme_health() { createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid // Borrow 500 MOET - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 500.0, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 500.0, beFailed: false) // STEP 2: Crash FLOW to near-zero ($0.00000001) setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.00000001) @@ -257,10 +207,10 @@ fun test_oracle_very_large_price_no_overflow() { let tinyDeposit = 0.00000001 setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) - createPosition(admin: protocolAccount, signer: user, amount: wethAmount, vaultStoragePath: WETH_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: protocolAccount, signer: user, amount: wethAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid // STEP 2: Set WETH to extreme price (UFix64.max) setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: UFix64.max) @@ -292,25 +242,25 @@ fun test_dex_oracle_deviation_boundary_exact_threshold() { // Exactly at 300 bps (3%) — should pass // Oracle: $1.00, DEX: $1.03 → deviation = |1.03-1.00|/1.00 = 3.0% = 300 bps - var res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 1.03, oraclePrice: 1.0, maxDeviationBps: 300) + var res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 1.03, oraclePrice: 1.0, maxDeviationBps: 300) Test.assertEqual(true, res) // One basis point over — should fail // Oracle: $1.00, DEX: $1.0301 → deviation = 3.01% = 301 bps - res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 1.0301, oraclePrice: 1.0, maxDeviationBps: 300) + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 1.0301, oraclePrice: 1.0, maxDeviationBps: 300) Test.assertEqual(false, res) // DEX below oracle — same threshold applies // Oracle: $1.00, DEX: $0.97 → deviation = |0.97-1.00|/0.97 = 3.09% = 309 bps - res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 0.97, oraclePrice: 1.0, maxDeviationBps: 300) + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 0.97, oraclePrice: 1.0, maxDeviationBps: 300) Test.assertEqual(false, res) // DEX: $0.971 → deviation = |0.971-1.00|/0.971 = 2.98% = 298 bps - res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 0.971, oraclePrice: 1.0, maxDeviationBps: 300) + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 0.971, oraclePrice: 1.0, maxDeviationBps: 300) Test.assertEqual(true, res) // Equal prices — zero deviation — always passes - res = FlowALPv0.dexOraclePriceDeviationInRange(dexPrice: 1.0, oraclePrice: 1.0, maxDeviationBps: 0) + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 1.0, oraclePrice: 1.0, maxDeviationBps: 0) Test.assertEqual(true, res) } @@ -340,10 +290,10 @@ fun test_dex_oracle_deviation_blocks_liquidation() { createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 700.0, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false) // Drop FLOW oracle price to make position unhealthy setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) @@ -409,10 +359,10 @@ fun test_dex_oracle_within_threshold_liquidation_succeeds() { createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 700.0, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false) // Drop FLOW oracle price to make position unhealthy setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) @@ -466,10 +416,10 @@ fun test_governance_tightens_dex_deviation_threshold() { createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 700.0, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false) // Make position unhealthy setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) @@ -533,10 +483,10 @@ fun test_flash_crash_triggers_liquidation() { createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 600.0, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 600.0, beFailed: false) // Collateral: // FLOW: 1000 * $1.00 * 0.8 = $800 @@ -630,10 +580,10 @@ fun test_flash_pump_increase_doubles_health() { createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPv0.Opened).pid + let openEvents = Test.eventsOfType(Type()) + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 500.0, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 500.0, beFailed: false) // Collateral: // FLOW: 1000 * $1.00 * 0.8 = $800 @@ -673,7 +623,7 @@ fun test_flash_pump_increase_doubles_health() { Test.assert(availableMoet > 900.0, message: "User should be able to borrow significantly more at pumped price") // STEP 5: User borrows at pumped price, then price corrects back - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET_STORAGE_PATH, amount: 900.0, beFailed: false) + borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 900.0, beFailed: false) // Price corrects back to $1.00 setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) diff --git a/flow.json b/flow.json index 5a0e6bed..26e6f86e 100644 --- a/flow.json +++ b/flow.json @@ -121,7 +121,6 @@ "aliases": { "testing": "0000000000000007", "mainnet": "6b00ff876c299c61" - } }, "MockDexSwapper": { From 42f43c80eed51ad8f31e6f410b83222665c0bbea Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Tue, 3 Mar 2026 17:41:27 +0200 Subject: [PATCH 8/9] Removed 2 tests to avoid duplicating --- cadence/tests/fork_oracle_failure_test.cdc | 130 --------------------- 1 file changed, 130 deletions(-) diff --git a/cadence/tests/fork_oracle_failure_test.cdc b/cadence/tests/fork_oracle_failure_test.cdc index 47c495b2..1b457903 100644 --- a/cadence/tests/fork_oracle_failure_test.cdc +++ b/cadence/tests/fork_oracle_failure_test.cdc @@ -264,136 +264,6 @@ fun test_dex_oracle_deviation_boundary_exact_threshold() { Test.assertEqual(true, res) } -// ============================================================================= -// DEX/Oracle price deviation and circuit breaker -// ============================================================================= - -// ----------------------------------------------------------------------------- -// DEX Price Deviates Beyond Threshold — Liquidation Blocked -// The protocol has a `dexOracleDeviationBps` parameter (default 300 = 3%). -// If the DEX-implied price deviates from the oracle by more than this -// threshold, liquidation must be rejected to prevent price manipulation. -// ----------------------------------------------------------------------------- -access(all) -fun test_dex_oracle_deviation_blocks_liquidation() { - safeReset() - - // STEP 1: Setup MOET LP + user with unhealthy position - let moetLp = Test.createAccount() - setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) - createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 1000.0) - - createPosition(admin: protocolAccount, 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 - - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false) - - // Drop FLOW oracle price to make position unhealthy - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) - - // Collateral: - // FLOW: 1000 * $0.70 * 0.8 = $560 - // Debt: - // MOET: 700 * $1.00 / 1.0 = $700 - // - // Health = $560 / $700 = 0.8 (unhealthy) - let expectedHealth: UFix128 = 0.8 - let health = getPositionHealth(pid: pid, beFailed: false) - Test.assertEqual(expectedHealth, health) - - // STEP 2: Set DEX price to deviate MORE than 3% from oracle - // Oracle says FLOW = $0.70, DEX says FLOW = $0.60 (14.3% deviation) - setMockDexPriceForPair( - signer: protocolAccount, - inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - outVaultIdentifier: MAINNET_MOET_TOKEN_ID, - vaultSourceStoragePath: MOET.VaultStoragePath, - priceRatio: 0.60 - ) - - // STEP 3: Attempt liquidation — should fail due to DEX/oracle deviation - let liquidator = Test.createAccount() - setupMoetVault(liquidator, beFailed: false) - mintMoet(signer: protocolAccount, to: liquidator.address, amount: 500.0, beFailed: false) - - // DEX quote: 100 / 0.60 = 166.67 FLOW - // Liquidator offers 160 FLOW (better than DEX) - // But the DEX/oracle deviation check: |0.60 - 0.70| / 0.60 = 16.7% >> 3% - let liqRes = manualLiquidation( - signer: liquidator, - pid: pid, - debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, - seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - seizeAmount: 160.0, - repayAmount: 100.0, - ) - Test.expect(liqRes, Test.beFailed()) - Test.assertError(liqRes, errorMessage: "DEX/oracle price deviation too large") -} - -// ----------------------------------------------------------------------------- -// DEX Price Within Threshold — Liquidation Succeeds -// Verify that when the DEX price is within the configured deviation -// threshold, liquidation proceeds normally. -// ----------------------------------------------------------------------------- -access(all) -fun test_dex_oracle_within_threshold_liquidation_succeeds() { - safeReset() - - // STEP 1: Setup MOET LP + user with unhealthy position - let moetLp = Test.createAccount() - setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) - createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 1000.0) - - createPosition(admin: protocolAccount, 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 - - borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false) - - // Drop FLOW oracle price to make position unhealthy - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) - - // Set DEX price within 3% of oracle: $0.70 * 0.98 = $0.686 - setMockDexPriceForPair( - signer: protocolAccount, - inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - outVaultIdentifier: MAINNET_MOET_TOKEN_ID, - vaultSourceStoragePath: MOET.VaultStoragePath, - priceRatio: 0.686 - ) - - // STEP 2: Liquidation should succeed - let liquidator = Test.createAccount() - setupMoetVault(liquidator, beFailed: false) - mintMoet(signer: protocolAccount, to: liquidator.address, amount: 500.0, beFailed: false) - - // DEX quote: 100 / 0.686 = ~145.77 FLOW - // Liquidator offers 140 FLOW (better than DEX) - let liqRes = manualLiquidation( - signer: liquidator, - pid: pid, - debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, - seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - seizeAmount: 140.0, - repayAmount: 100.0, - ) - Test.expect(liqRes, Test.beSucceeded()) -} - // ----------------------------------------------------------------------------- // Governance Adjusts DEX Deviation Threshold // Tests that governance can tighten or loosen the circuit breaker. From dd2de3e4107630bafe43b97dae45a48cf3cc6674 Mon Sep 17 00:00:00 2001 From: UlyanaAndrukhiv Date: Tue, 3 Mar 2026 17:53:22 +0200 Subject: [PATCH 9/9] Updated oracle forked test to use common addresses from test_helpers --- cadence/tests/fork_oracle_failure_test.cdc | 80 +++++++++++----------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/cadence/tests/fork_oracle_failure_test.cdc b/cadence/tests/fork_oracle_failure_test.cdc index 1b457903..300a5d0a 100644 --- a/cadence/tests/fork_oracle_failure_test.cdc +++ b/cadence/tests/fork_oracle_failure_test.cdc @@ -14,9 +14,9 @@ import "FlowALPMath" import "test_helpers.cdc" -access(all) let protocolAccount = Test.getAccount(0x6b00ff876c299c61) -access(all) let MAINNET_USDF_HOLDER = Test.getAccount(0xf18b50870aed46ad) -access(all) let MAINNET_WETH_HOLDER = Test.getAccount(0xf62e3381a164f993) +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) var snapshot: UInt64 = 0 @@ -32,17 +32,17 @@ access(all) fun setup() { deployContracts() - createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false) + createAndStorePool(signer: MAINNET_PROTOCOL_ACCOUNT, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false) // Set initial oracle prices (baseline) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 2000.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_MOET_TOKEN_ID, price: 1.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_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: 2000.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_MOET_TOKEN_ID, price: 1.0) // Add FLOW as supported token (80% CF, 90% BF) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, collateralFactor: 0.8, borrowFactor: 0.9, @@ -52,7 +52,7 @@ fun setup() { // Add USDF as supported token (90% CF, 95% BF) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, collateralFactor: 0.9, borrowFactor: 0.95, @@ -62,7 +62,7 @@ fun setup() { // Add WETH as supported token (75% CF, 85% BF) addSupportedTokenZeroRateCurve( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, collateralFactor: 0.75, borrowFactor: 0.85, @@ -90,13 +90,13 @@ fun test_oracle_nil_price() { setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) - createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + 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 2: Remove FLOW price from oracle - let res = setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: nil) + let res = setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: nil) // STEP 3: Attempting to read position health should revert // The pool's positionHealth() calls `self.priceOracle.price(ofToken: type)!` which panics on nil. @@ -121,7 +121,7 @@ fun test_oracle_zero_price() { // STEP 1: Attempt to set FLOW price to 0.0 // The PriceOracle interface postcondition requires price > 0.0. // The MockOracle's price() function should revert on zero. - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.0) // STEP 2: Setup user with FLOW position let user = Test.createAccount() @@ -129,7 +129,7 @@ fun test_oracle_zero_price() { transferFlowTokens(to: user, amount: 1000.0) // STEP 3: Attempting to create position should fail - grantBetaPoolParticipantAccess(protocolAccount, user) + grantBetaPoolParticipantAccess(MAINNET_PROTOCOL_ACCOUNT, user) let openRes = _executeTransaction( "../transactions/flow-alp/position/create_position.cdc", @@ -153,14 +153,14 @@ fun test_oracle_near_zero_price_extreme_health() { // STEP 1: Setup MOET LP + user with FLOW collateral + MOET debt let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) - createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: 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 user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) - createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + 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 @@ -169,7 +169,7 @@ fun test_oracle_near_zero_price_extreme_health() { borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 500.0, beFailed: false) // STEP 2: Crash FLOW to near-zero ($0.00000001) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.00000001) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.00000001) // Collateral: // FLOW: 1000 * $0.00000001 * 0.8 = $0.000008 @@ -205,15 +205,15 @@ fun test_oracle_very_large_price_no_overflow() { transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: wethAmount) let tinyDeposit = 0.00000001 - setMinimumTokenBalancePerPosition(signer: protocolAccount, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) + setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) - createPosition(admin: protocolAccount, signer: user, amount: wethAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) + createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: wethAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid // STEP 2: Set WETH to extreme price (UFix64.max) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: UFix64.max) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: UFix64.max) // Collateral: // WETH: 0.001 * $100,000,000 * 0.75 = $75,000 @@ -277,14 +277,14 @@ fun test_governance_tightens_dex_deviation_threshold() { // STEP 1: Setup MOET LP + unhealthy position let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) - createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: 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 user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) - createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + 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 @@ -292,12 +292,12 @@ fun test_governance_tightens_dex_deviation_threshold() { borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false) // Make position unhealthy - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.70) // DEX price within default 3% threshold but outside 1% // Oracle: $0.70, DEX: $0.685 → deviation = |0.685-0.70|/0.685 = 2.19% setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MOET.VaultStoragePath, @@ -305,12 +305,12 @@ fun test_governance_tightens_dex_deviation_threshold() { ) // STEP 2: Tighten threshold to 100 bps (1%) - setDexLiquidationConfig(signer: protocolAccount, dexOracleDeviationBps: 100) + setDexLiquidationConfig(signer: MAINNET_PROTOCOL_ACCOUNT, dexOracleDeviationBps: 100) // STEP 3: Liquidation should now fail (2.19% > 1% threshold) let liquidator = Test.createAccount() setupMoetVault(liquidator, beFailed: false) - mintMoet(signer: protocolAccount, to: liquidator.address, amount: 500.0, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: liquidator.address, amount: 500.0, beFailed: false) let liqRes = manualLiquidation( signer: liquidator, @@ -343,15 +343,15 @@ fun test_flash_crash_triggers_liquidation() { // STEP 1: Setup MOET liquidity provider let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) - createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: 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 user with FLOW collateral + moderate MOET debt let user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) - createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + 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 @@ -371,9 +371,9 @@ fun test_flash_crash_triggers_liquidation() { Test.assert(healthBefore > 1.0, message: "Position should be healthy before crash") // STEP 3: Flash crash — FLOW drops 50% ($1.00 → $0.50) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.50) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.50) setMockDexPriceForPair( - signer: protocolAccount, + signer: MAINNET_PROTOCOL_ACCOUNT, inVaultIdentifier: MAINNET_FLOW_TOKEN_ID, outVaultIdentifier: MAINNET_MOET_TOKEN_ID, vaultSourceStoragePath: MOET.VaultStoragePath, @@ -401,7 +401,7 @@ fun test_flash_crash_triggers_liquidation() { // STEP 5: Execute liquidation let liquidator = Test.createAccount() setupMoetVault(liquidator, beFailed: false) - mintMoet(signer: protocolAccount, to: liquidator.address, amount: 1000.0, beFailed: false) + mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: liquidator.address, amount: 1000.0, beFailed: false) // Repay 100 MOET, seize FLOW // DEX quote: 100 / 0.50 = 200 FLOW @@ -440,15 +440,15 @@ fun test_flash_pump_increase_doubles_health() { // STEP 1: Setup MOET liquidity provider let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: protocolAccount, to: moetLp.address, amount: 50000.0, beFailed: false) - createPosition(admin: protocolAccount, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: 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 user with FLOW collateral and MOET debt let user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) - createPosition(admin: protocolAccount, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) + 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 @@ -470,7 +470,7 @@ fun test_flash_pump_increase_doubles_health() { Test.assertEqual(expectedHealthBefore, healthBefore) // STEP 3: Flash pump — FLOW doubles ($1.00 → $2.00) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 2.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 2.0) // New position state: // Collateral: @@ -496,7 +496,7 @@ fun test_flash_pump_increase_doubles_health() { borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 900.0, beFailed: false) // Price corrects back to $1.00 - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) + setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0) // Position after correction: // Collateral: