Skip to content

Multi position per user scenarios testing#172

Open
mts1715 wants to merge 17 commits intomainfrom
taras/147-multi-position-per-user-scenarios-testing
Open

Multi position per user scenarios testing#172
mts1715 wants to merge 17 commits intomainfrom
taras/147-multi-position-per-user-scenarios-testing

Conversation

@mts1715
Copy link
Contributor

@mts1715 mts1715 commented Feb 19, 2026

Closes: #147

Multi-Position Per User Scenarios: Fork Tests & Supporting Infrastructure
cadence/tests/fork_multiple_positions_per_user.cdc
Runs against mainnet at block 142528994 using real token holders and real token identifiers (FLOW, USDF, USDC, WETH, WBTC). Contains 4 tests:

  • testMultiplePositionsPerUser — Creates 5 positions with different collateral types, borrows varying amounts from each, and asserts that operations on one position do not affect the health factors of others (isolation guarantee).

  • testPositionInteractionsSharedLiquidity — Verifies cross-position effects through the shared FLOW liquidity pool: Position A's heavy borrowing reduces available liquidity for Position B, a subsequent repayment restores it, and a crash of Position A's collateral price has zero effect on Position B's health.

  • testBatchLiquidations — Creates 5 positions (USDF, WETH, USDC, WBTC, FLOW) with differentiated borrow levels, crashes 4 collateral prices, then executes a single-transaction batch liquidation: 2 full liquidations (WETH, USDF) and 2 partial liquidations (USDC, WBTC). The FLOW-collateral position, whose price is unchanged, remains untouched.

  • testMassUnhealthyLiquidations — System-wide stress test: 100 positions across 3 collateral types (50 USDF × 10 USDF, 45 USDC × 2 USDC, 5 WBTC × 0.00009 WBTC), two risk tiers (high/moderate) per stablecoin group, simultaneous 40% price crash across all three collateral types, then batch DEX liquidation of all 100 positions in chunks of 10 to stay within computation limits. Verifies all positions recover health and protocol FLOW reserve stays positive.

cadence/transactions/flow-alp/pool-management/batch_manual_liquidation.cdc — Liquidates multiple positions in one transaction using the caller's own debt tokens as repayment.

cadence/transactions/flow-alp/pool-management/batch_liquidate_via_mock_dex.cdc — Liquidates multiple positions in one transaction sourcing repayment from a pre-configured MockDexSwapper.

…dation - fork_multiple_positions_per_user.cdc covering three scenarios:

- Multiple positions with distinct collateral types (FLOW, USDF, USDC,
  WETH, WBTC) and isolation guarantees between them
- Cross-position effects through shared liquidity pools
- Batch liquidation of 4 positions (2 full, 2 partial) in a single tx
…eates

100 positions across three collateral types (50 USDF, 45 USDC, 5 WBTC),
crashes all collateral prices 40% simultaneously, and batch-liquidates all
positions via MockDexSwapper in chunks of 10 to stay within computation limits.
…ion-per-user-scenarios-testing

# Conflicts:
#	flow.json
@mts1715 mts1715 requested a review from a team as a code owner February 19, 2026 13:48
@mts1715 mts1715 self-assigned this Feb 20, 2026
@vishalchangrani vishalchangrani requested a review from a team February 24, 2026 17:20
Copy link
Collaborator

@nialexsan nialexsan left a comment

Choose a reason for hiding this comment

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

a few nits

/// repayAmounts: Array of repay amounts for each position
transaction(
pids: [UInt64],
debtVaultIdentifier: String,
Copy link
Collaborator

Choose a reason for hiding this comment

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

change debt* to repayment* to reduce confusion as it represents a repayment, not actual the actual debt

Comment on lines +74 to +77
// Deposit seized collateral back to liquidator
// For simplicity, we'll just destroy it in this test transaction
// In production, you'd want to properly handle the seized collateral
destroy seizedVault
Copy link
Collaborator

Choose a reason for hiding this comment

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

depositing it back to liquidator would improve testing, as it'd be possible to check the vault balance after liquidation

/// repayAmounts: Array of debt amounts to repay for each position (sourced from the DEX)
transaction(
pids: [UInt64],
debtVaultIdentifier: String,
Copy link
Collaborator

Choose a reason for hiding this comment

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

change debt* to repayment*

)

totalRepaid = totalRepaid + repayAmount
destroy seizedVault
Copy link
Collaborator

Choose a reason for hiding this comment

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

deposit back to liquidator to improve test assertions

Copy link
Member

Choose a reason for hiding this comment

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

yeah, better not to destroy, just finish up by depositing somewhere ideally.


// Build an exact quote for the repayAmount we need from the swapper's vaultSource
let swapQuote = MockDexSwapper.BasicQuote(
inType: seizeType,
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: it's slightly awkward to swap seizeType to debtType in order to get back seizeType back from liquidator. It would be better to use something like FlowToken or another token to clearly demonstrate that the debt could be repaid with any other token, not just debtType

// pid 1: 0.06 WETH @ $3500 (CF=0.75), borrow 90 → health = 0.06*3500*0.75/90 = 1.750
// pid 2: 80 USDC @ $1.00 (CF=0.85), borrow 40 → health = 80*1.0*0.85/40 = 1.700
// pid 3: 0.0004 WBTC @ $50000 (CF=0.75), borrow 10 → health = 0.0004*50000*0.75/10 = 1.500
// pid 4: 200 FLOW @ $1.00 (CF=0.80), borrow 80 → health = 200*1.0*0.80/80 = 2.000
Copy link
Collaborator

Choose a reason for hiding this comment

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

technically this will be collateral withdrawal, not borrowing
put FLOW, get FLOW back

Copy link
Member

Choose a reason for hiding this comment

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

then should we just remove?

// 40% simultaneously, requiring a chunked batch DEX liquidation of every position.
//
// =============================================================================
access(all) fun testMassUnhealthyLiquidations() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

would it be possible to store the ranges in some variables instead of using magic numbers through the test, even with comments it's tricky to track all of them

@@ -0,0 +1,9 @@
import "MockOracle"
Copy link
Member

Choose a reason for hiding this comment

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

can we name this something else? or move into a scripts folder in the tests section, since this only grabs from the MockOracle.

debtVaultIdentifier: String,
seizeVaultIdentifiers: [String],
seizeAmounts: [UFix64],
repayAmounts: [UFix64]
Copy link
Member

Choose a reason for hiding this comment

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

not needed for this PR, but i do wonder if this should be a single array of a.composite data structure or even map, rather than 4 separate arrays, since this adds 3 asserts, and slightly more complex for loop notion. for idx in InclusiveRange(0, numPositions - 1) {

mostly a nit though, so up to you if it's easy to adjust in this PR, or to just keep in mind for the future

)

totalRepaid = totalRepaid + repayAmount
destroy seizedVault
Copy link
Member

Choose a reason for hiding this comment

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

yeah, better not to destroy, just finish up by depositing somewhere ideally.

depositCapacityCap: 1_000_000.0
)

addSupportedTokenZeroRateCurve(
Copy link
Member

Choose a reason for hiding this comment

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

Dete actually has mentioned that he's aiming to NOT allow other stables as collateral in the protocol. I think if we do want to keep one stable, it's probably okay, but we def don't need 2 i think. If it's easy to remove, let's just do 1 stable coin only.

// pid 1: 0.06 WETH @ $3500 (CF=0.75), borrow 90 → health = 0.06*3500*0.75/90 = 1.750
// pid 2: 80 USDC @ $1.00 (CF=0.85), borrow 40 → health = 80*1.0*0.85/40 = 1.700
// pid 3: 0.0004 WBTC @ $50000 (CF=0.75), borrow 10 → health = 0.0004*50000*0.75/10 = 1.500
// pid 4: 200 FLOW @ $1.00 (CF=0.80), borrow 80 → health = 200*1.0*0.80/80 = 2.000
Copy link
Member

Choose a reason for hiding this comment

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

then should we just remove?

inVaultIdentifier: MAINNET_USDF_TOKEN_ID,
outVaultIdentifier: MAINNET_FLOW_TOKEN_ID,
vaultSourceStoragePath: FLOW_VAULT_STORAGE_PATH,
priceRatio: 0.3 // $0.30 USDF / $1.00 FLOW
Copy link
Member

Choose a reason for hiding this comment

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

nit: $1.00 FLOW is a token count right? let's remove the $ if so

// and 1 unit of each collateral token to initialize vault storage paths.
//
// Repay amounts derived from: repay = debt - (collat - seize) * CF * P_crashed / H_target
// WETH=71: debt=90, (0.06-0.035)*0.75*1050 = 19.6875, H≈1.034 → 90 - 19.6875/1.034 ≈ 71
Copy link
Member

Choose a reason for hiding this comment

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

You mention it a bit below but might be good to mention where 1.034 comes from

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.

Multi-Position Scenarios Testing

4 participants