Skip to content

Conversation

@jordanschalm
Copy link
Member

@jordanschalm jordanschalm commented Jan 16, 2026

Closes: #94

Extends #77 (review that PR first)

Description

This PR implements integration with a generic DEX (SwapperProvider), as a source of pricing comparison, for the purposes of manual liquidation. The DEX price is used in two ways:

  • liquidation offers much be strictly better than what could be obtained by swapping through the DEX
  • if the DEX price diverges from the oracle price too much, no liquidation is allowed

Policy Decisions

  • A SwapperProvider (DEX) must always be provided at construction time of the Pool (field is non-optional)
  • The configured SwapperProvider must provide a Swapper for all possible supported token pairs. If it does not, liquidation will fail.

jordanschalm and others added 13 commits January 16, 2026 09:46
(no errors in IDE, but tests failing (can't find FCM in this scope) on
this commit)
state is stored in smart contract, struct with reference to contract
acts as mock object from FCM's perspective
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
@jordanschalm jordanschalm marked this pull request as ready for review January 21, 2026 20:44
@jordanschalm jordanschalm requested a review from a team as a code owner January 21, 2026 20:44
Comment on lines 26 to 36
/// 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
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want a better distinction between adding a new mock swapper and updating the price by overwriting an existing mock swapper. The tests currently call addMockDexSwapper to update the DEX price, after which the original "1:1" price mock swapper is no longer available - I think this should be made more clear.

To that end: Can we split this into two functions - one for exclusively adding a swapper and one for updating it? (Both with appropriate preconditions.)

If we want to keep the existing function, I think we should update the name and documentation to clarify that this overwrites existing swapper for the same input/output token pair, e.g. rename to "_addOrUpdateSwapper".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated naming and documentation to clarify this in 5fc56c6. The function used within tests is now called setMockDexPriceForPair.

Comment on lines 272 to 286
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())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern here, I want a better distinction between adding a new mock swapper and updating the price by overwriting an existing mock swapper, or at least documentation that either can happen

Copy link
Member

@joshuahannan joshuahannan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused about the upgrade plans here

///
/// 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}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't add this in an upgrade, right? Or are we redeploying?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right -- we will need to redeploy to add this field.

I've been operating under the assumption that we may not redeploy, but obviously this change will make redeployment necessary (we also separately decided this morning to proceed under the assumption that we will do a redeployment). I think we can leave this field as is, and I will be going through the fields marked deprecated in previous PRs to remove them prior to redeployment (since we can safely do that now).

Base automatically changed from jord/liquidation to main January 27, 2026 20:25
jordanschalm and others added 5 commits January 27, 2026 12:29
Renamed functions and updated documentation to make it clear that setting a mock DEX swapper overwrites any existing swapper for the same token pair. This is primarily used in tests to set or update prices.

Changes:

- Renamed MockDexSwapper._addSwapper() to setMockDEXSwapperForPair()

- Renamed test helper addMockDexSwapper() to setMockDexPriceForPair()

- Renamed transaction file to set_mock_dex_price_for_pair.cdc

- Updated all test usages in liquidation_phase1_test.cdc

- Added documentation explaining overwrite behavior and intended use case

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Resolved merge conflicts in liquidation_phase1_test.cdc by accepting DEX
integration changes and updating identifier variables to use global CAPS
constants (FLOW_TOKEN_IDENTIFIER, MOET_TOKEN_IDENTIFIER, PROTOCOL_ACCOUNT).

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Comment on lines 39 to 41
let swappersForInType = self.swappers[inType]!
swappersForInType[outType] = swapper
self.swappers[inType] = swappersForInType
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a mock contract so it doesn't matter, but in production code we should consider optimizing such code, i.e. avoiding copying and updating existing (potentially large) datasets by using in-place mutation (get a reference and insert)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified to use references in 514d126.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants