diff --git a/cadence/lib/FlowALPMath.cdc b/cadence/lib/FlowALPMath.cdc index 1a753d89..0185d714 100644 --- a/cadence/lib/FlowALPMath.cdc +++ b/cadence/lib/FlowALPMath.cdc @@ -90,11 +90,12 @@ access(all) contract FlowALPMath { return scaledInt % 2 == 1 ? self.roundUp(base) : base } - /// Checks that the DEX price does not deviate from the oracle price by more than the given threshold. - /// The deviation is computed as the absolute difference divided by the smaller price, expressed in basis points. + /// Checks if the DEX price deviates from the oracle price by more than the allowed threshold (in basis points). + /// The deviation is measured relative to the oracle price, which is treated as the reference price. access(all) view fun dexOraclePriceDeviationInRange(dexPrice: UFix64, oraclePrice: UFix64, maxDeviationBps: UInt16): Bool { let diff: UFix64 = dexPrice < oraclePrice ? oraclePrice - dexPrice : dexPrice - oraclePrice - let diffPct: UFix64 = dexPrice < oraclePrice ? diff / dexPrice : diff / oraclePrice + // Deviation expressed as a percentage of the oracle price (reference) + let diffPct: UFix64 = diff / oraclePrice let diffBps = UInt16(diffPct * 10_000.0) return diffBps <= maxDeviationBps } @@ -146,7 +147,7 @@ access(all) contract FlowALPMath { /// Effective Collateral is defined: /// Ce = (Nc)(Pc)(Fc) /// Where: - /// Ce = Effective Collateral + /// Ce = Effective Collateral /// Nc = Number of Collateral Tokens /// Pc = Collateral Token Price /// Fc = Collateral Factor @@ -162,7 +163,7 @@ access(all) contract FlowALPMath { /// Effective Debt is defined: /// De = (Nd)(Pd)(Fd) /// Where: - /// De = Effective Debt + /// De = Effective Debt /// Nd = Number of Debt Tokens /// Pd = Debt Token Price /// Fd = Borrow Factor diff --git a/cadence/tests/flowALPMath_dex_oracle_deviation_test.cdc b/cadence/tests/flowALPMath_dex_oracle_deviation_test.cdc new file mode 100644 index 00000000..11fcadfc --- /dev/null +++ b/cadence/tests/flowALPMath_dex_oracle_deviation_test.cdc @@ -0,0 +1,43 @@ +import Test +import "FlowALPMath" + +access(all) fun setup() { + let err = Test.deployContract( + name: "FlowALPMath", + path: "../lib/FlowALPMath.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +access(all) 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 = 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 = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 1.0301, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(false, res) + + // DEX below oracle — exactly at threshold + // Oracle: $1.00, DEX: $0.97 → deviation = |0.97-1.00|/1.00 = 3.0% = 300 bps + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 0.97, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(true, res) + + // One basis point over on the low side — should fail + // Oracle: $1.00, DEX: $0.9699 → deviation = |0.9699-1.00|/1.00 = 3.01% = 301 bps + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 0.9699, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(false, res) + + // DEX: $0.971 → deviation = |0.971-1.00|/1.00 = 2.9% = 290 bps + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 0.971, oraclePrice: 1.0, maxDeviationBps: 300) + Test.assertEqual(true, res) + + // Equal prices — zero deviation — always passes + res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 1.0, oraclePrice: 1.0, maxDeviationBps: 0) + Test.assertEqual(true, res) +} + + diff --git a/cadence/tests/fork_oracle_failure_test.cdc b/cadence/tests/fork_oracle_failure_test.cdc index 300a5d0a..f2a029db 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) @@ -228,42 +228,6 @@ fun test_oracle_very_large_price_no_overflow() { 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 = 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 = 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 = 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 = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 0.971, oraclePrice: 1.0, maxDeviationBps: 300) - Test.assertEqual(true, res) - - // Equal prices — zero deviation — always passes - res = FlowALPMath.dexOraclePriceDeviationInRange(dexPrice: 1.0, oraclePrice: 1.0, maxDeviationBps: 0) - Test.assertEqual(true, res) -} - // ----------------------------------------------------------------------------- // Governance Adjusts DEX Deviation Threshold // Tests that governance can tighten or loosen the circuit breaker. @@ -358,10 +322,10 @@ fun test_flash_crash_triggers_liquidation() { 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 +345,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 +395,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 +419,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 +437,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 +463,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 //