From 62ca2d65f5073489f8c8cd91a8ad80dc81f29898 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 09:46:41 -0800 Subject: [PATCH 01/11] update FlowActions --- FlowActions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowActions b/FlowActions index 1254f6e..d014ce4 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 1254f6e94fe23e27490d9df042de186b29e5e4cc +Subproject commit d014ce49a4ee3cf663c03c6a9b4498a7a1bbff58 From d35d9c1d1fd3ebf94970b9c8d551879aceff632c Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 10:00:03 -0800 Subject: [PATCH 02/11] add dex field, and do deps update (no errors in IDE, but tests failing (can't find FCM in this scope) on this commit) --- cadence/contracts/FlowCreditMarket.cdc | 9 +++++++-- flow.json | 12 +++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 1f5d779..e275346 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1185,20 +1185,24 @@ access(all) contract FlowCreditMarket { // - also to make allowlist pattern work with automated liquidation, initiator of this automation will need actual handle on a dex in order to pass it to FCM /// Allowlist of permitted DeFiActions Swapper types for DEX liquidations + /// TODO: unused! To remove, must re-deploy existing contracts access(self) var allowedSwapperTypes: {Type: Bool} + access(self) let dex: {DeFiActions.SwapperProvider} + /// Max allowed deviation in basis points between DEX-implied price and oracle price access(self) var dexOracleDeviationBps: UInt16 /// Max slippage allowed in basis points for DEX liquidations /// TODO(jord): revisit this. Is this ever necessary if we are also checking dexOracleDeviationBps? Do we want both a spot price check and a slippage from spot price check? + /// TODO: unused! To remove, must re-deploy existing contracts access(self) var dexMaxSlippageBps: UInt64 /// Max route hops allowed for DEX liquidations - // TODO(jord): unused + /// TODO: unused! To remove, must re-deploy existing contracts access(self) var dexMaxRouteHops: UInt64 - init(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}) { + init(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}, dex: {DeFiActions.SwapperProvider}) { pre { priceOracle.unitOfAccount() == defaultToken: "Price oracle must return prices in terms of the default token" @@ -1230,6 +1234,7 @@ access(all) contract FlowCreditMarket { self.lastUnpausedAt = nil self.protocolLiquidationFeeBps = 0 self.allowedSwapperTypes = {} + self.dex = dex self.dexOracleDeviationBps = UInt16(300) // 3% default self.dexMaxSlippageBps = 100 self.dexMaxRouteHops = 3 diff --git a/flow.json b/flow.json index a72b42d..99d878a 100644 --- a/flow.json +++ b/flow.json @@ -75,6 +75,7 @@ "Burner": { "source": "mainnet://f233dcee88fe0abe.Burner", "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", + "block_height": 139085361, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", @@ -84,6 +85,7 @@ "FlowFees": { "source": "mainnet://f919ee77447b7497.FlowFees", "hash": "341cc0f3cc847d6b787c390133f6a5e6c867c111784f09c5c0083c47f2f1df64", + "block_height": 139085361, "aliases": { "emulator": "e5a8b7f23e8b548f", "mainnet": "f919ee77447b7497", @@ -93,6 +95,7 @@ "FlowStorageFees": { "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", "hash": "a92c26fb2ea59725441fa703aa4cd811e0fc56ac73d649a8e12c1e72b67a8473", + "block_height": 139085361, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -102,6 +105,7 @@ "FlowToken": { "source": "mainnet://1654653399040a61.FlowToken", "hash": "f82389e2412624ffa439836b00b42e6605b0c00802a4e485bc95b8930a7eac38", + "block_height": 139085361, "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", @@ -110,7 +114,8 @@ }, "FlowTransactionScheduler": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", - "hash": "c701f26f6a8e993b2573ec8700142f61c9ca936b199af8cc75dee7d9b19c9e95", + "hash": "23157cf7d70534e45b0ab729133232d0ffb3cdae52661df1744747cb1f8c0495", + "block_height": 139085361, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", @@ -120,6 +125,7 @@ "FungibleToken": { "source": "mainnet://f233dcee88fe0abe.FungibleToken", "hash": "4b74edfe7d7ddfa70b703c14aa731a0b2e7ce016ce54d998bfd861ada4d240f6", + "block_height": 139085361, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -129,6 +135,7 @@ "FungibleTokenMetadataViews": { "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", "hash": "70477f80fd7678466c224507e9689f68f72a9e697128d5ea54d19961ec856b3c", + "block_height": 139085361, "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", @@ -138,6 +145,7 @@ "MetadataViews": { "source": "mainnet://1d7e57aa55817448.MetadataViews", "hash": "b290b7906d901882b4b62e596225fb2f10defb5eaaab4a09368f3aee0e9c18b1", + "block_height": 139085361, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -147,6 +155,7 @@ "NonFungibleToken": { "source": "mainnet://1d7e57aa55817448.NonFungibleToken", "hash": "a258de1abddcdb50afc929e74aca87161d0083588f6abf2b369672e64cf4a403", + "block_height": 139085361, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", @@ -156,6 +165,7 @@ "ViewResolver": { "source": "mainnet://1d7e57aa55817448.ViewResolver", "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", + "block_height": 139085361, "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", From de434877e65ca978537bbb1a2d9cb3a362fff9e5 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 13:06:28 -0800 Subject: [PATCH 03/11] add mock swapper provider --- cadence/contracts/mocks/MockDexSwapper.cdc | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cadence/contracts/mocks/MockDexSwapper.cdc b/cadence/contracts/mocks/MockDexSwapper.cdc index 2ba8a17..98876f0 100644 --- a/cadence/contracts/mocks/MockDexSwapper.cdc +++ b/cadence/contracts/mocks/MockDexSwapper.cdc @@ -24,6 +24,7 @@ access(all) contract MockDexSwapper { access(all) struct Swapper : DeFiActions.Swapper { access(self) let inVault: Type access(self) let outVault: Type + /// source for output tokens only (reverse swaps unsupported) access(self) let vaultSource: Capability access(self) let priceRatio: UFix64 // out per unit in access(contract) var uniqueID: DeFiActions.UniqueIdentifier? @@ -86,4 +87,32 @@ access(all) contract MockDexSwapper { access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { return self.uniqueID } access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } } + + access(all) struct SwapperProvider : DeFiActions.SwapperProvider { + // inType -> outType -> Swapper + access(self) let swappers: {Type: {Type: Swapper}} + + init() { + self.swappers = {} + } + + // Implements SwapperProvider: code being tested calls this function. + access(all) fun getSwapper(inType: Type, outType: Type): Swapper? { + if let swappersForInType = self.swappers[inType] { + return swappersForInType[outType] + } + return nil + } + + // Used by testing code to configure the DEX. + access(all) fun _addSwapper(swapper: Swapper) { + if self.swappers[swapper.inType()] == nil { + self.swappers[swapper.inType()] = { swapper.outType(): swapper } + } else { + let swappersForInType = self.swappers[swapper.inType()]! + swappersForInType[swapper.outType()] = swapper + self.swappers[swapper.inType()] = swappersForInType + } + } + } } From 821ea4e37c8a49134c4d364a8f5abbe28ba4a70f Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 13:17:52 -0800 Subject: [PATCH 04/11] rework mock dex to use same pattern as oracle state is stored in smart contract, struct with reference to contract acts as mock object from FCM's perspective --- cadence/contracts/mocks/MockDexSwapper.cdc | 52 ++++++++++++---------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/cadence/contracts/mocks/MockDexSwapper.cdc b/cadence/contracts/mocks/MockDexSwapper.cdc index 98876f0..a140951 100644 --- a/cadence/contracts/mocks/MockDexSwapper.cdc +++ b/cadence/contracts/mocks/MockDexSwapper.cdc @@ -8,6 +8,33 @@ import "DeFiActionsUtils" /// Do NOT use in production. access(all) contract MockDexSwapper { + /// Holds the set of available swappers which will be provided to users of SwapperProvider. + /// inType -> outType -> Swapper + access(self) let swappers: {Type: {Type: Swapper}} + + init() { + self.swappers = {} + } + + access(all) fun getSwapper(inType: Type, outType: Type): Swapper? { + if let swappersForInType = self.swappers[inType] { + return swappersForInType[outType] + } + return nil + } + + /// Used by testing code to configure the DEX with swappers. + /// Overwrites existing swapper with same types, if any. + access(all) fun _addSwapper(swapper: Swapper) { + if self.swappers[swapper.inType()] == nil { + self.swappers[swapper.inType()] = { swapper.outType(): swapper } + } else { + let swappersForInType = self.swappers[swapper.inType()]! + swappersForInType[swapper.outType()] = swapper + self.swappers[swapper.inType()] = swappersForInType + } + } + access(all) struct BasicQuote : DeFiActions.Quote { access(all) let inType: Type access(all) let outType: Type @@ -21,6 +48,7 @@ access(all) contract MockDexSwapper { } } + /// NOTE: reverse swaps are unsupported. access(all) struct Swapper : DeFiActions.Swapper { access(self) let inVault: Type access(self) let outVault: Type @@ -89,30 +117,8 @@ access(all) contract MockDexSwapper { } access(all) struct SwapperProvider : DeFiActions.SwapperProvider { - // inType -> outType -> Swapper - access(self) let swappers: {Type: {Type: Swapper}} - - init() { - self.swappers = {} - } - - // Implements SwapperProvider: code being tested calls this function. access(all) fun getSwapper(inType: Type, outType: Type): Swapper? { - if let swappersForInType = self.swappers[inType] { - return swappersForInType[outType] - } - return nil - } - - // Used by testing code to configure the DEX. - access(all) fun _addSwapper(swapper: Swapper) { - if self.swappers[swapper.inType()] == nil { - self.swappers[swapper.inType()] = { swapper.outType(): swapper } - } else { - let swappersForInType = self.swappers[swapper.inType()]! - swappersForInType[swapper.outType()] = swapper - self.swappers[swapper.inType()] = swappersForInType - } + return MockDexSwapper.getSwapper(inType: inType, outType: outType) } } } From f27cc28fba555d97425146b9db533acca2776855 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 13:27:09 -0800 Subject: [PATCH 05/11] include dex in pool creation in tests --- cadence/contracts/FlowCreditMarket.cdc | 7 ++++--- .../pool-factory/create_and_store_pool.cdc | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index e275346..8eb4a1a 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1188,6 +1188,7 @@ access(all) contract FlowCreditMarket { /// TODO: unused! To remove, must re-deploy existing contracts access(self) var allowedSwapperTypes: {Type: Bool} + /// A trusted DEX (or set of DEXes) used by FCM as a pricing oracle and trading counterparty for liquidations. access(self) let dex: {DeFiActions.SwapperProvider} /// Max allowed deviation in basis points between DEX-implied price and oracle price @@ -2105,7 +2106,7 @@ access(all) contract FlowCreditMarket { ) } - // Returns health value of this position if the given amount of the specified token were withdrawn without + // Returns health value of this position if the given amount of the specified token were withdrawn without // using the top up source. // NOTE: This method can return health values below 1.0, which aren't actually allowed. This indicates // that the proposed withdrawal would fail (unless a top up source is available and used). @@ -3087,12 +3088,12 @@ access(all) contract FlowCreditMarket { /// access(all) resource PoolFactory { /// Creates the contract-managed Pool and saves it to the canonical path, reverting if one is already stored - access(all) fun createPool(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}) { + access(all) fun createPool(defaultToken: Type, priceOracle: {DeFiActions.PriceOracle}, dex: {DeFiActions.SwapperProvider}) { pre { FlowCreditMarket.account.storage.type(at: FlowCreditMarket.PoolStoragePath) == nil: "Storage collision - Pool has already been created & saved to \(FlowCreditMarket.PoolStoragePath)" } - let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle) + let pool <- create Pool(defaultToken: defaultToken, priceOracle: priceOracle, dex: dex) FlowCreditMarket.account.storage.save(<-pool, to: FlowCreditMarket.PoolStoragePath) let cap = FlowCreditMarket.account.capabilities.storage.issue<&Pool>(FlowCreditMarket.PoolStoragePath) FlowCreditMarket.account.capabilities.unpublish(FlowCreditMarket.PoolPublicPath) diff --git a/cadence/transactions/flow-credit-market/pool-factory/create_and_store_pool.cdc b/cadence/transactions/flow-credit-market/pool-factory/create_and_store_pool.cdc index dd7e926..b988b89 100644 --- a/cadence/transactions/flow-credit-market/pool-factory/create_and_store_pool.cdc +++ b/cadence/transactions/flow-credit-market/pool-factory/create_and_store_pool.cdc @@ -3,6 +3,7 @@ import "FungibleToken" import "DeFiActions" import "FlowCreditMarket" import "MockOracle" +import "MockDexSwapper" /// THIS TRANSACTION IS NOT INTENDED FOR PRODUCTION /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -16,15 +17,17 @@ transaction(defaultTokenIdentifier: String) { let factory: &FlowCreditMarket.PoolFactory let defaultToken: Type let oracle: {DeFiActions.PriceOracle} + let dex: {DeFiActions.SwapperProvider} prepare(signer: auth(BorrowValue) &Account) { self.factory = signer.storage.borrow<&FlowCreditMarket.PoolFactory>(from: FlowCreditMarket.PoolFactoryPath) ?? panic("Could not find PoolFactory in signer's account") self.defaultToken = CompositeType(defaultTokenIdentifier) ?? panic("Invalid defaultTokenIdentifier \(defaultTokenIdentifier)") self.oracle = MockOracle.PriceOracle() + self.dex = MockDexSwapper.SwapperProvider() } execute { - self.factory.createPool(defaultToken: self.defaultToken, priceOracle: self.oracle) + self.factory.createPool(defaultToken: self.defaultToken, priceOracle: self.oracle, dex: self.dex) } } From 6bf2d86ea4583cfada3b89bc9358def5aeeaa405 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Fri, 16 Jan 2026 15:13:42 -0800 Subject: [PATCH 06/11] Enable DEX pricing comparison in manual liquidation Un-comment the DEX price validation logic to ensure liquidators provide competitive prices and prevent oracle manipulation. Add test infrastructure with MockDexSwapper transaction and helpers to configure DEX prices that stay synchronized with oracle prices during liquidation tests. Co-Authored-By: Claude Sonnet 4.5 --- cadence/contracts/FlowCreditMarket.cdc | 5 +- cadence/tests/liquidation_phase1_test.cdc | 109 ++++++++++++++++++ cadence/tests/test_helpers.cdc | 16 +++ .../mock-dex-swapper/add_swapper.cdc | 44 +++++++ 4 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 cadence/tests/transactions/mock-dex-swapper/add_swapper.cdc diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 8eb4a1a..80276fe 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1537,14 +1537,12 @@ access(all) contract FlowCreditMarket { let postHealth = FlowCreditMarket.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) assert(postHealth <= self.liquidationTargetHF, message: "Liquidation must not exceed target health: \(postHealth)>\(self.liquidationTargetHF)") - // TODO(jord): uncomment following when implementing dex logic https://github.com/onflow/FlowCreditMarket/issues/94 -/* // Compare the liquidation offer to liquidation via DEX. If the DEX would provide a better price, reject the offer. let swapper = self.dex!.getSwapper(inType: seizeType, outType: debtType)! // TODO: will revert if pair unsupported // Get a quote: "how much collateral do I need to give you to get `repayAmount` debt tokens" let quote = swapper.quoteIn(forDesired: repayAmount, reverse: false) assert(seizeAmount < quote.inAmount, message: "Liquidation offer must be better than that offered by DEX") - + // Compare the DEX price to the oracle price and revert if they diverge beyond configured threshold. let Pcd_dex = quote.outAmount / quote.inAmount // price of collateral, denominated in debt token, implied by dex quote (D/C) // Compute the absolute value of the difference between the oracle price and dex price @@ -1554,7 +1552,6 @@ access(all) contract FlowCreditMarket { let Pcd_dex_oracle_diffBps = UInt16(Pcd_dex_oracle_diffPct * 10_000.0) // cannot overflow because Pcd_dex_oracle_diffPct<=1 assert(Pcd_dex_oracle_diffBps <= self.dexOracleDeviationBps, message: "Too large difference between dex/oracle prices diff=\(Pcd_dex_oracle_diffBps)bps") -*/ // Execute the liquidation return <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index 8577360..bfee2a3 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -27,6 +27,7 @@ fun setup() { let protocolAccount = Test.getAccount(0x0000000000000007) setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) + setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: moetIdentifier, price: 1.0) createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) grantPoolCapToConsumer() addSupportedTokenZeroRateCurve( @@ -38,6 +39,16 @@ fun setup() { depositCapacityCap: 1_000_000.0 ) + // Add DEX swapper for FLOW -> MOET pair (for liquidations) + // priceRatio = 1.0 means 1 MOET per 1 FLOW (matches oracle prices) + addMockDexSwapper( + signer: protocolAccount, + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: 1.0 + ) + snapshot = getCurrentBlockHeight() } @@ -100,6 +111,14 @@ fun testManualLiquidation_liquidationExceedsTargetHealth() { // cause undercollateralization let newPrice = 0.7 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) // execute liquidation @@ -149,6 +168,14 @@ fun testManualLiquidation_repayExceedsDebt() { // cause undercollateralization let newPrice = 0.7 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: moetIdentifier) @@ -201,6 +228,14 @@ fun testManualLiquidation_seizeExceedsCollateral() { // cause undercollateralization AND insolvency let newPrice = 0.5 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let collateralBalance = getPositionBalance(pid: pid, vaultID: flowTokenIdentifier).balance @@ -251,6 +286,14 @@ fun testManualLiquidation_reduceHealth() { // cause undercollateralization AND insolvency let newPrice = 0.5 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let collateralBalancePreLiq = getPositionBalance(pid: pid, vaultID: flowTokenIdentifier).balance @@ -309,6 +352,14 @@ fun testManualLiquidation_repaymentVaultCollateralType() { // cause undercollateralization let newPrice = 0.7 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: moetIdentifier) @@ -354,6 +405,14 @@ fun testManualLiquidation_repaymentVaultTypeMismatch() { // cause undercollateralization let newPrice = 0.7 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: moetIdentifier) @@ -401,6 +460,14 @@ fun testManualLiquidation_unsupportedDebtType() { // cause undercollateralization let newPrice = 0.7 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let debtPositionBalance = getPositionBalance(pid: pid, vaultID: moetIdentifier) @@ -448,6 +515,14 @@ fun testManualLiquidation_unsupportedCollateralType() { // cause undercollateralization let newPrice = 0.7 // $/FLOW setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid, beFailed: false) let collateralBalancePreLiq = getPositionBalance(pid: pid, vaultID: flowTokenIdentifier).balance @@ -492,6 +567,15 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { depositCapacityCap: 1_000_000.0 ) + // Add DEX swapper for MockYieldToken -> MOET pair + addMockDexSwapper( + signer: protocolAccount, + inVaultIdentifier: mockYieldTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: 1.0 + ) + // user1 setup - deposits FLOW let user1 = Test.createAccount() setupMoetVault(user1, beFailed: false) @@ -518,6 +602,14 @@ fun testManualLiquidation_supportedDebtTypeNotInPosition() { // cause undercollateralization for user1 by dropping FLOW price let newPrice = 0.7 // $/FLOW setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: protocolAccount, + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid1, beFailed: false) // execute liquidation @@ -559,6 +651,15 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { depositCapacityCap: 1_000_000.0 ) + // Add DEX swapper for MockYieldToken -> MOET pair + addMockDexSwapper( + signer: protocolAccount, + inVaultIdentifier: mockYieldTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: 1.0 + ) + // user1 setup - deposits FLOW, borrows MOET let user1 = Test.createAccount() setupMoetVault(user1, beFailed: false) @@ -584,6 +685,14 @@ fun testManualLiquidation_supportedCollateralTypeNotInPosition() { // cause undercollateralization for user1 by dropping FLOW price let newPrice = 0.5 // $/FLOW setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: newPrice) + // Update DEX price to match oracle + addMockDexSwapper( + signer: protocolAccount, + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) let hAfterPrice = getPositionHealth(pid: pid1, beFailed: false) // execute liquidation diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index d1ea14f..b575ba3 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -269,6 +269,22 @@ fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, pri Test.expect(setRes, Test.beSucceeded()) } +access(all) +fun addMockDexSwapper( + signer: Test.TestAccount, + inVaultIdentifier: String, + outVaultIdentifier: String, + vaultSourceStoragePath: StoragePath, + priceRatio: UFix64 +) { + let addRes = _executeTransaction( + "./transactions/mock-dex-swapper/add_swapper.cdc", + [inVaultIdentifier, outVaultIdentifier, vaultSourceStoragePath, priceRatio], + signer + ) + Test.expect(addRes, Test.beSucceeded()) +} + access(all) fun addSupportedTokenZeroRateCurve( signer: Test.TestAccount, diff --git a/cadence/tests/transactions/mock-dex-swapper/add_swapper.cdc b/cadence/tests/transactions/mock-dex-swapper/add_swapper.cdc new file mode 100644 index 0000000..5ed5c86 --- /dev/null +++ b/cadence/tests/transactions/mock-dex-swapper/add_swapper.cdc @@ -0,0 +1,44 @@ +import "FungibleToken" +import "MockDexSwapper" + +/// TEST-ONLY: Adds a swapper to MockDexSwapper for a specific token pair +/// +/// @param inVaultIdentifier: The input Vault's Type identifier (e.g., Type<@FlowToken.Vault>().identifier) +/// @param outVaultIdentifier: The output Vault's Type identifier (e.g., Type<@MOET.Vault>().identifier) +/// @param vaultSourceStoragePath: The storage path of the vault to use as the output source (from signer's storage) +/// @param priceRatio: The price ratio for the swap (out per unit in) +transaction( + inVaultIdentifier: String, + outVaultIdentifier: String, + vaultSourceStoragePath: StoragePath, + priceRatio: UFix64 +) { + let inType: Type + let outType: Type + let vaultSourceCap: Capability + + prepare(signer: auth(IssueStorageCapabilityController) &Account) { + self.inType = CompositeType(inVaultIdentifier) ?? panic("Invalid inVaultIdentifier \(inVaultIdentifier)") + self.outType = CompositeType(outVaultIdentifier) ?? panic("Invalid outVaultIdentifier \(outVaultIdentifier)") + + // Get a capability to the vault source from the signer's storage + self.vaultSourceCap = signer.capabilities.storage + .issue(vaultSourceStoragePath) + + assert(self.vaultSourceCap.check(), message: "Invalid vault source capability") + } + + execute { + // Create the swapper + let swapper = MockDexSwapper.Swapper( + inVault: self.inType, + outVault: self.outType, + vaultSource: self.vaultSourceCap, + priceRatio: priceRatio, + uniqueID: nil + ) + + // Add the swapper to MockDexSwapper + MockDexSwapper._addSwapper(swapper: swapper) + } +} From e366a8d540fad75807ed601e009fe819fb16dfd6 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 20 Jan 2026 12:46:02 -0800 Subject: [PATCH 07/11] Add comprehensive tests for DEX pricing comparison in manual liquidation Implement 5 test functions covering DEX pricing validation logic: - testManualLiquidation_dexOraclePriceDivergence: Tests price divergence thresholds with 3 sub-cases (within/exceeds/above oracle) - testManualLiquidation_increaseHealthBelowTarget: Tests partial liquidation improving health without reaching target - testManualLiquidation_liquidateToTarget: Tests liquidation bringing health to exactly 1.05 target - testManualLiquidation_liquidatorOfferWorseThanDex: Tests rejection when liquidator offers worse price than DEX - testManualLiquidation_combinedEdgeCase: Tests divergence check precedence over competitive offers All 18 tests in liquidation_phase1_test.cdc now pass. Co-Authored-By: Claude Sonnet 4.5 --- cadence/tests/liquidation_phase1_test.cdc | 334 +++++++++++++++++++++- 1 file changed, 331 insertions(+), 3 deletions(-) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index bfee2a3..c1389e4 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -727,13 +727,341 @@ fun testManualLiquidation_liquidationWarmup() {} /// Liquidations should fail if DEX price and oracle price diverge by too much. access(all) -fun testManualLiquidation_dexOraclePriceDivergence() {} +fun testManualLiquidation_dexOraclePriceDivergence() { + // Test Case 1a: Within threshold (2.94%, should succeed) + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open wrapped position and deposit via existing helper txs + // debt is MOET, collateral is FLOW + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause undercollateralization + let oraclePrice = 0.7 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: oraclePrice) + + // Set DEX price to 0.68 (2.94% divergence: (0.7-0.68)/0.68 = 0.0294 = 2.94%) + let dexPriceRatio = 0.68 + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: dexPriceRatio + ) + + let health = getPositionHealth(pid: pid, beFailed: false) + + // execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + // Repay MOET to seize FLOW + // DEX quote would require: 50/0.68 = 73.53 FLOW + // Liquidator offers 72 FLOW < 73.53 FLOW (better price) + let repayAmount = 50.0 + let seizeAmount = 72.0 + let liqRes1a = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], + liquidator + ) + // Should succeed because divergence is within threshold + Test.expect(liqRes1a, Test.beSucceeded()) + + // Test Case 1b: Exceeds threshold (6.06%, should fail) + safeReset() + let user1b = Test.createAccount() + setupMoetVault(user1b, beFailed: false) + transferFlowTokens(to: user1b, amount: 1000.0) + createWrappedPosition(signer: user1b, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause undercollateralization + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: 0.7) + + // Set DEX price to 0.66 (6.06% divergence: (0.7-0.66)/0.66 = 0.0606 = 6.06%) + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: 0.66 + ) + + let liquidator1b = Test.createAccount() + setupMoetVault(liquidator1b, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator1b.address, amount: 1000.0, beFailed: false) + + let liqRes1b = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, 70.0, 50.0], + liquidator1b + ) + // Should fail because divergence exceeds threshold + Test.expect(liqRes1b, Test.beFailed()) + Test.assertError(liqRes1b, errorMessage: "Too large difference between dex/oracle prices") + + // Test Case 1c: DEX higher than oracle, exceeds threshold (should fail) + safeReset() + let user1c = Test.createAccount() + setupMoetVault(user1c, beFailed: false) + transferFlowTokens(to: user1c, amount: 1000.0) + createWrappedPosition(signer: user1c, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause undercollateralization + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: 0.7) + + // Set DEX price to 0.74 (5.71% divergence above oracle: (0.74-0.7)/0.7 = 0.0571 = 5.71%) + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: 0.74 + ) + + let liquidator1c = Test.createAccount() + setupMoetVault(liquidator1c, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator1c.address, amount: 1000.0, beFailed: false) + + let liqRes1c = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, 66.0, 50.0], + liquidator1c + ) + // Should fail because divergence exceeds threshold + Test.expect(liqRes1c, Test.beFailed()) + Test.assertError(liqRes1c, errorMessage: "Too large difference between dex/oracle prices") +} /// Should be able to liquidate to below target health while increasing health factor. access(all) -fun testManualLiquidation_increaseHealthBelowTarget() {} +fun testManualLiquidation_increaseHealthBelowTarget() { + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open wrapped position and deposit via existing helper txs + // debt is MOET, collateral is FLOW + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause severe undercollateralization + let newPrice = 0.5 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) + + let healthBefore = getPositionHealth(pid: pid, beFailed: false) + Test.assert(healthBefore < 1.05, message: "position should be unhealthy before liquidation") + + // execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + // Repay MOET to seize FLOW + // DEX quote would require: 100/0.5 = 200 FLOW + // Liquidator offers 150 FLOW < 200 FLOW (better price) + let repayAmount = 100.0 + let seizeAmount = 150.0 + let liqRes = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], + liquidator + ) + // Should succeed + Test.expect(liqRes, Test.beSucceeded()) + + // Check post-liquidation health + let healthAfter = getPositionHealth(pid: pid, beFailed: false) + + // Health should have improved + Test.assert(healthAfter > healthBefore, message: "health should improve after liquidation") + + // Health should still be below target + Test.assert(healthAfter < 1.05, message: "health should still be below target (1.05)") +} /// Should be able to liquidate to exactly target health access(all) -fun testManualLiquidation_liquidateToTarget() {} +fun testManualLiquidation_liquidateToTarget() { + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open wrapped position and deposit via existing helper txs + // debt is MOET, collateral is FLOW + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause undercollateralization + let newPrice = 0.7 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) + + let healthBefore = getPositionHealth(pid: pid, beFailed: false) + Test.assert(healthBefore < 1.05, message: "position should be unhealthy before liquidation") + + // execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + // Repay MOET to seize FLOW - calculated to bring health to exactly 1.05 + // Initial: 1000 FLOW at $0.7 with effective collateral factor 0.8 + // Debt: ~615.38 MOET (from auto-borrow at creation) + // Pre-health: (1000 * 0.7 * 0.8) / 615.38 = 0.91 + // Target post-health: 1.05 + // Formula: (1000 - seizeAmount) * 0.7 * 0.8 / (615.38 - repayAmount) = 1.05 + // Using repayAmount = 100: seizeAmount = 33.66 + // DEX quote would require: 100/0.7 = 142.86 FLOW + // Liquidator offers 33.66 FLOW < 142.86 FLOW (better price) + let repayAmount = 100.0 + let seizeAmount = 33.66 + let liqRes = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], + liquidator + ) + // Should succeed + Test.expect(liqRes, Test.beSucceeded()) + + // Check post-liquidation health + let healthAfter = getPositionHealth(pid: pid, beFailed: false) + + // Health should be very close to target (1.05), allowing for small variance + Test.assert(healthAfter >= 1.04 && healthAfter <= 1.06, message: "health should be close to target (1.05), actual: ".concat(healthAfter.toString())) +} + +/// Liquidation should fail if liquidator offer is worse than DEX price. +access(all) +fun testManualLiquidation_liquidatorOfferWorseThanDex() { + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open wrapped position and deposit via existing helper txs + // debt is MOET, collateral is FLOW + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause undercollateralization + let newPrice = 0.7 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) + + let health = getPositionHealth(pid: pid, beFailed: false) + Test.assert(health < 1.0, message: "position should be unhealthy") + + // execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + // Liquidator offers worse price than DEX + // DEX quote: 50/0.7 = 71.43 FLOW + // Liquidator offers 75 FLOW > 71.43 FLOW (worse price) + let repayAmount = 50.0 + let seizeAmount = 75.0 + let liqRes = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], + liquidator + ) + // Should fail because liquidator offer is worse than DEX + Test.expect(liqRes, Test.beFailed()) + Test.assertError(liqRes, errorMessage: "Liquidation offer must be better than that offered by DEX") +} + +/// Liquidation should fail when DEX/oracle divergence is too high, even when liquidator offer is competitive. +access(all) +fun testManualLiquidation_combinedEdgeCase() { + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open wrapped position and deposit via existing helper txs + // debt is MOET, collateral is FLOW + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause undercollateralization + let oraclePrice = 0.7 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: oraclePrice) + + // Set DEX price to 0.64 (9.375% divergence: (0.7-0.64)/0.64 = 0.09375 = 9.375%) + let dexPriceRatio = 0.64 + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: dexPriceRatio + ) + + let health = getPositionHealth(pid: pid, beFailed: false) + Test.assert(health < 1.0, message: "position should be unhealthy") + + // execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + // Liquidator provides better price than DEX but divergence is too high + // DEX quote: 50/0.64 = 78.125 FLOW + // Liquidator offers 75 FLOW < 78.125 FLOW (better than DEX) + // But divergence is 9.375% which exceeds 3% threshold + let repayAmount = 50.0 + let seizeAmount = 75.0 + let liqRes = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], + liquidator + ) + // Should fail because DEX/oracle divergence is too high, even though liquidator offer is competitive + Test.expect(liqRes, Test.beFailed()) + Test.assertError(liqRes, errorMessage: "Too large difference between dex/oracle prices") +} From d9857309b5dbee7f2bc15e1f0843b1053003917c Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 20 Jan 2026 12:58:43 -0800 Subject: [PATCH 08/11] Split DEX price divergence test into separate success and failure cases Refactor testManualLiquidation_dexOraclePriceDivergence into: - testManualLiquidation_dexOraclePriceDivergence_withinThreshold: Tests successful liquidation when divergence is within 3% threshold - testManualLiquidation_dexOraclePriceDivergence_exceedsThreshold: Tests two failure cases when divergence exceeds threshold (DEX below and above oracle) This improves test clarity by separating success and failure scenarios into distinct test functions. All 19 tests pass. Co-Authored-By: Claude Sonnet 4.5 --- cadence/tests/liquidation_phase1_test.cdc | 63 ++++++++++++----------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index c1389e4..ea81adc 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -725,10 +725,9 @@ fun testManualLiquidation_liquidationPaused() {} access(all) fun testManualLiquidation_liquidationWarmup() {} -/// Liquidations should fail if DEX price and oracle price diverge by too much. +/// Liquidations should succeed when DEX/oracle price divergence is within threshold. access(all) -fun testManualLiquidation_dexOraclePriceDivergence() { - // Test Case 1a: Within threshold (2.94%, should succeed) +fun testManualLiquidation_dexOraclePriceDivergence_withinThreshold() { safeReset() let pid: UInt64 = 0 @@ -767,20 +766,26 @@ fun testManualLiquidation_dexOraclePriceDivergence() { // Liquidator offers 72 FLOW < 73.53 FLOW (better price) let repayAmount = 50.0 let seizeAmount = 72.0 - let liqRes1a = _executeTransaction( + let liqRes = _executeTransaction( "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], liquidator ) // Should succeed because divergence is within threshold - Test.expect(liqRes1a, Test.beSucceeded()) + Test.expect(liqRes, Test.beSucceeded()) +} - // Test Case 1b: Exceeds threshold (6.06%, should fail) +/// Liquidations should fail when DEX/oracle price divergence exceeds threshold. +access(all) +fun testManualLiquidation_dexOraclePriceDivergence_exceedsThreshold() { + // Test Case 1: DEX price below oracle, exceeds threshold (6.06%) safeReset() - let user1b = Test.createAccount() - setupMoetVault(user1b, beFailed: false) - transferFlowTokens(to: user1b, amount: 1000.0) - createWrappedPosition(signer: user1b, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + let pid: UInt64 = 0 + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: 0.7) @@ -794,25 +799,25 @@ fun testManualLiquidation_dexOraclePriceDivergence() { priceRatio: 0.66 ) - let liquidator1b = Test.createAccount() - setupMoetVault(liquidator1b, beFailed: false) - mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator1b.address, amount: 1000.0, beFailed: false) + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) - let liqRes1b = _executeTransaction( + let liqRes = _executeTransaction( "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, 70.0, 50.0], - liquidator1b + liquidator ) // Should fail because divergence exceeds threshold - Test.expect(liqRes1b, Test.beFailed()) - Test.assertError(liqRes1b, errorMessage: "Too large difference between dex/oracle prices") + Test.expect(liqRes, Test.beFailed()) + Test.assertError(liqRes, errorMessage: "Too large difference between dex/oracle prices") - // Test Case 1c: DEX higher than oracle, exceeds threshold (should fail) + // Test Case 2: DEX price above oracle, exceeds threshold (5.71%) safeReset() - let user1c = Test.createAccount() - setupMoetVault(user1c, beFailed: false) - transferFlowTokens(to: user1c, amount: 1000.0) - createWrappedPosition(signer: user1c, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + let user2 = Test.createAccount() + setupMoetVault(user2, beFailed: false) + transferFlowTokens(to: user2, amount: 1000.0) + createWrappedPosition(signer: user2, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: 0.7) @@ -826,18 +831,18 @@ fun testManualLiquidation_dexOraclePriceDivergence() { priceRatio: 0.74 ) - let liquidator1c = Test.createAccount() - setupMoetVault(liquidator1c, beFailed: false) - mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator1c.address, amount: 1000.0, beFailed: false) + let liquidator2 = Test.createAccount() + setupMoetVault(liquidator2, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator2.address, amount: 1000.0, beFailed: false) - let liqRes1c = _executeTransaction( + let liqRes2 = _executeTransaction( "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, 66.0, 50.0], - liquidator1c + liquidator2 ) // Should fail because divergence exceeds threshold - Test.expect(liqRes1c, Test.beFailed()) - Test.assertError(liqRes1c, errorMessage: "Too large difference between dex/oracle prices") + Test.expect(liqRes2, Test.beFailed()) + Test.assertError(liqRes2, errorMessage: "Too large difference between dex/oracle prices") } /// Should be able to liquidate to below target health while increasing health factor. From a529b76c6ff095b4a872380cf1edf55450752e83 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 20 Jan 2026 13:08:20 -0800 Subject: [PATCH 09/11] Split DEX price divergence failure test into two distinct tests Refactor testManualLiquidation_dexOraclePriceDivergence_exceedsThreshold into: - testManualLiquidation_dexOraclePriceDivergence_dexBelowOracle: Tests failure when DEX price is below oracle (6.06% divergence) - testManualLiquidation_dexOraclePriceDivergence_dexAboveOracle: Tests failure when DEX price is above oracle (5.71% divergence) This provides better test isolation with one assertion per test function. All 20 tests pass. Co-Authored-By: Claude Sonnet 4.5 --- cadence/tests/liquidation_phase1_test.cdc | 36 +++++++++++++---------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index ea81adc..18d9e59 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -27,7 +27,7 @@ fun setup() { let protocolAccount = Test.getAccount(0x0000000000000007) setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: 1.0) - setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: moetIdentifier, price: 1.0) + // setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: moetIdentifier, price: 1.0) createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: defaultTokenIdentifier, beFailed: false) grantPoolCapToConsumer() addSupportedTokenZeroRateCurve( @@ -775,10 +775,9 @@ fun testManualLiquidation_dexOraclePriceDivergence_withinThreshold() { Test.expect(liqRes, Test.beSucceeded()) } -/// Liquidations should fail when DEX/oracle price divergence exceeds threshold. +/// Liquidations should fail when DEX price is below oracle and divergence exceeds threshold. access(all) -fun testManualLiquidation_dexOraclePriceDivergence_exceedsThreshold() { - // Test Case 1: DEX price below oracle, exceeds threshold (6.06%) +fun testManualLiquidation_dexOraclePriceDivergence_dexBelowOracle() { safeReset() let pid: UInt64 = 0 @@ -811,13 +810,18 @@ fun testManualLiquidation_dexOraclePriceDivergence_exceedsThreshold() { // Should fail because divergence exceeds threshold Test.expect(liqRes, Test.beFailed()) Test.assertError(liqRes, errorMessage: "Too large difference between dex/oracle prices") +} - // Test Case 2: DEX price above oracle, exceeds threshold (5.71%) +/// Liquidations should fail when DEX price is above oracle and divergence exceeds threshold. +access(all) +fun testManualLiquidation_dexOraclePriceDivergence_dexAboveOracle() { safeReset() - let user2 = Test.createAccount() - setupMoetVault(user2, beFailed: false) - transferFlowTokens(to: user2, amount: 1000.0) - createWrappedPosition(signer: user2, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + let pid: UInt64 = 0 + + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) // cause undercollateralization setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: 0.7) @@ -831,18 +835,18 @@ fun testManualLiquidation_dexOraclePriceDivergence_exceedsThreshold() { priceRatio: 0.74 ) - let liquidator2 = Test.createAccount() - setupMoetVault(liquidator2, beFailed: false) - mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator2.address, amount: 1000.0, beFailed: false) + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) - let liqRes2 = _executeTransaction( + let liqRes = _executeTransaction( "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, 66.0, 50.0], - liquidator2 + liquidator ) // Should fail because divergence exceeds threshold - Test.expect(liqRes2, Test.beFailed()) - Test.assertError(liqRes2, errorMessage: "Too large difference between dex/oracle prices") + Test.expect(liqRes, Test.beFailed()) + Test.assertError(liqRes, errorMessage: "Too large difference between dex/oracle prices") } /// Should be able to liquidate to below target health while increasing health factor. From 3b934d5b66244180c146ea306997199d2fa008a8 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 20 Jan 2026 13:09:35 -0800 Subject: [PATCH 10/11] move related tests together --- cadence/tests/liquidation_phase1_test.cdc | 242 +++++++++++----------- 1 file changed, 121 insertions(+), 121 deletions(-) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index 18d9e59..06b0d32 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -331,6 +331,127 @@ fun testManualLiquidation_reduceHealth() { Test.assert(hAfterLiq < hAfterPrice, message: "test expects health to decrease after liquidation") } +/// Should be able to liquidate to below target health while increasing health factor. +access(all) +fun testManualLiquidation_increaseHealthBelowTarget() { + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open wrapped position and deposit via existing helper txs + // debt is MOET, collateral is FLOW + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause severe undercollateralization + let newPrice = 0.5 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) + + let healthBefore = getPositionHealth(pid: pid, beFailed: false) + Test.assert(healthBefore < 1.05, message: "position should be unhealthy before liquidation") + + // execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + // Repay MOET to seize FLOW + // DEX quote would require: 100/0.5 = 200 FLOW + // Liquidator offers 150 FLOW < 200 FLOW (better price) + let repayAmount = 100.0 + let seizeAmount = 150.0 + let liqRes = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], + liquidator + ) + // Should succeed + Test.expect(liqRes, Test.beSucceeded()) + + // Check post-liquidation health + let healthAfter = getPositionHealth(pid: pid, beFailed: false) + + // Health should have improved + Test.assert(healthAfter > healthBefore, message: "health should improve after liquidation") + + // Health should still be below target + Test.assert(healthAfter < 1.05, message: "health should still be below target (1.05)") +} + +/// Should be able to liquidate to exactly target health +access(all) +fun testManualLiquidation_liquidateToTarget() { + safeReset() + let pid: UInt64 = 0 + + // user setup + let user = Test.createAccount() + setupMoetVault(user, beFailed: false) + transferFlowTokens(to: user, amount: 1000.0) + + // open wrapped position and deposit via existing helper txs + // debt is MOET, collateral is FLOW + createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) + + // cause undercollateralization + let newPrice = 0.7 // $/FLOW + setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) + + // Update DEX price to match oracle + addMockDexSwapper( + signer: Test.getAccount(0x0000000000000007), + inVaultIdentifier: flowTokenIdentifier, + outVaultIdentifier: moetIdentifier, + vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, + priceRatio: newPrice + ) + + let healthBefore = getPositionHealth(pid: pid, beFailed: false) + Test.assert(healthBefore < 1.05, message: "position should be unhealthy before liquidation") + + // execute liquidation + let liquidator = Test.createAccount() + setupMoetVault(liquidator, beFailed: false) + mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) + + // Repay MOET to seize FLOW - calculated to bring health to exactly 1.05 + // Initial: 1000 FLOW at $0.7 with effective collateral factor 0.8 + // Debt: ~615.38 MOET (from auto-borrow at creation) + // Pre-health: (1000 * 0.7 * 0.8) / 615.38 = 0.91 + // Target post-health: 1.05 + // Formula: (1000 - seizeAmount) * 0.7 * 0.8 / (615.38 - repayAmount) = 1.05 + // Using repayAmount = 100: seizeAmount = 33.66 + // DEX quote would require: 100/0.7 = 142.86 FLOW + // Liquidator offers 33.66 FLOW < 142.86 FLOW (better price) + let repayAmount = 100.0 + let seizeAmount = 33.66 + let liqRes = _executeTransaction( + "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", + [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], + liquidator + ) + // Should succeed + Test.expect(liqRes, Test.beSucceeded()) + + // Check post-liquidation health + let healthAfter = getPositionHealth(pid: pid, beFailed: false) + + // Health should be very close to target (1.05), allowing for small variance + Test.assert(healthAfter >= 1.04 && healthAfter <= 1.06, message: "health should be close to target (1.05), actual: ".concat(healthAfter.toString())) +} + /// Test the case where the liquidator provides a repayment vault of the collateral type instead of debt type. access(all) fun testManualLiquidation_repaymentVaultCollateralType() { @@ -849,127 +970,6 @@ fun testManualLiquidation_dexOraclePriceDivergence_dexAboveOracle() { Test.assertError(liqRes, errorMessage: "Too large difference between dex/oracle prices") } -/// Should be able to liquidate to below target health while increasing health factor. -access(all) -fun testManualLiquidation_increaseHealthBelowTarget() { - safeReset() - let pid: UInt64 = 0 - - // user setup - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 1000.0) - - // open wrapped position and deposit via existing helper txs - // debt is MOET, collateral is FLOW - createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) - - // cause severe undercollateralization - let newPrice = 0.5 // $/FLOW - setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) - - // Update DEX price to match oracle - addMockDexSwapper( - signer: Test.getAccount(0x0000000000000007), - inVaultIdentifier: flowTokenIdentifier, - outVaultIdentifier: moetIdentifier, - vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, - priceRatio: newPrice - ) - - let healthBefore = getPositionHealth(pid: pid, beFailed: false) - Test.assert(healthBefore < 1.05, message: "position should be unhealthy before liquidation") - - // execute liquidation - let liquidator = Test.createAccount() - setupMoetVault(liquidator, beFailed: false) - mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) - - // Repay MOET to seize FLOW - // DEX quote would require: 100/0.5 = 200 FLOW - // Liquidator offers 150 FLOW < 200 FLOW (better price) - let repayAmount = 100.0 - let seizeAmount = 150.0 - let liqRes = _executeTransaction( - "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], - liquidator - ) - // Should succeed - Test.expect(liqRes, Test.beSucceeded()) - - // Check post-liquidation health - let healthAfter = getPositionHealth(pid: pid, beFailed: false) - - // Health should have improved - Test.assert(healthAfter > healthBefore, message: "health should improve after liquidation") - - // Health should still be below target - Test.assert(healthAfter < 1.05, message: "health should still be below target (1.05)") -} - -/// Should be able to liquidate to exactly target health -access(all) -fun testManualLiquidation_liquidateToTarget() { - safeReset() - let pid: UInt64 = 0 - - // user setup - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - transferFlowTokens(to: user, amount: 1000.0) - - // open wrapped position and deposit via existing helper txs - // debt is MOET, collateral is FLOW - createWrappedPosition(signer: user, amount: 1000.0, vaultStoragePath: /storage/flowTokenVault, pushToDrawDownSink: true) - - // cause undercollateralization - let newPrice = 0.7 // $/FLOW - setMockOraclePrice(signer: Test.getAccount(0x0000000000000007), forTokenIdentifier: flowTokenIdentifier, price: newPrice) - - // Update DEX price to match oracle - addMockDexSwapper( - signer: Test.getAccount(0x0000000000000007), - inVaultIdentifier: flowTokenIdentifier, - outVaultIdentifier: moetIdentifier, - vaultSourceStoragePath: /storage/moetTokenVault_0x0000000000000007, - priceRatio: newPrice - ) - - let healthBefore = getPositionHealth(pid: pid, beFailed: false) - Test.assert(healthBefore < 1.05, message: "position should be unhealthy before liquidation") - - // execute liquidation - let liquidator = Test.createAccount() - setupMoetVault(liquidator, beFailed: false) - mintMoet(signer: Test.getAccount(0x0000000000000007), to: liquidator.address, amount: 1000.0, beFailed: false) - - // Repay MOET to seize FLOW - calculated to bring health to exactly 1.05 - // Initial: 1000 FLOW at $0.7 with effective collateral factor 0.8 - // Debt: ~615.38 MOET (from auto-borrow at creation) - // Pre-health: (1000 * 0.7 * 0.8) / 615.38 = 0.91 - // Target post-health: 1.05 - // Formula: (1000 - seizeAmount) * 0.7 * 0.8 / (615.38 - repayAmount) = 1.05 - // Using repayAmount = 100: seizeAmount = 33.66 - // DEX quote would require: 100/0.7 = 142.86 FLOW - // Liquidator offers 33.66 FLOW < 142.86 FLOW (better price) - let repayAmount = 100.0 - let seizeAmount = 33.66 - let liqRes = _executeTransaction( - "../transactions/flow-credit-market/pool-management/manual_liquidation.cdc", - [pid, Type<@MOET.Vault>().identifier, flowTokenIdentifier, seizeAmount, repayAmount], - liquidator - ) - // Should succeed - Test.expect(liqRes, Test.beSucceeded()) - - // Check post-liquidation health - let healthAfter = getPositionHealth(pid: pid, beFailed: false) - - // Health should be very close to target (1.05), allowing for small variance - Test.assert(healthAfter >= 1.04 && healthAfter <= 1.06, message: "health should be close to target (1.05), actual: ".concat(healthAfter.toString())) -} - /// Liquidation should fail if liquidator offer is worse than DEX price. access(all) fun testManualLiquidation_liquidatorOfferWorseThanDex() { From dd5b8af32dee0c8356ad298213c88a654e656661 Mon Sep 17 00:00:00 2001 From: Jordan Schalm Date: Tue, 20 Jan 2026 13:09:53 -0800 Subject: [PATCH 11/11] extend documentation for Pool.dex --- cadence/contracts/FlowCreditMarket.cdc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowCreditMarket.cdc b/cadence/contracts/FlowCreditMarket.cdc index 80276fe..5aadae5 100644 --- a/cadence/contracts/FlowCreditMarket.cdc +++ b/cadence/contracts/FlowCreditMarket.cdc @@ -1189,6 +1189,12 @@ access(all) contract FlowCreditMarket { access(self) var allowedSwapperTypes: {Type: Bool} /// A trusted DEX (or set of DEXes) used by FCM as a pricing oracle and trading counterparty for liquidations. + /// The SwapperProvider implementation MUST return a Swapper for all possible (ordered) pairs of supported tokens. + /// If [X1, X2, ..., Xn] is the set of supported tokens, then the SwapperProvider must return a Swapper for all pairs: + /// (Xi, Xj) where i∈[1,n], j∈[1,n], i≠j + /// + /// FCM does not attempt to construct multi-part paths (using multiple Swappers) or compare prices across Swappers. + /// It relies directly on the Swapper's returned by the configured SwapperProvider. access(self) let dex: {DeFiActions.SwapperProvider} /// Max allowed deviation in basis points between DEX-implied price and oracle price @@ -1538,7 +1544,7 @@ access(all) contract FlowCreditMarket { assert(postHealth <= self.liquidationTargetHF, message: "Liquidation must not exceed target health: \(postHealth)>\(self.liquidationTargetHF)") // Compare the liquidation offer to liquidation via DEX. If the DEX would provide a better price, reject the offer. - let swapper = self.dex!.getSwapper(inType: seizeType, outType: debtType)! // TODO: will revert if pair unsupported + let swapper = self.dex.getSwapper(inType: seizeType, outType: debtType)! // TODO: will revert if pair unsupported // Get a quote: "how much collateral do I need to give you to get `repayAmount` debt tokens" let quote = swapper.quoteIn(forDesired: repayAmount, reverse: false) assert(seizeAmount < quote.inAmount, message: "Liquidation offer must be better than that offered by DEX")