From ba8fc1951ab13e48f6375620d8a32105a550ac37 Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Mon, 9 Mar 2026 13:15:26 +0100 Subject: [PATCH 1/2] fix maxWithdraw caps credit position --- cadence/contracts/FlowALPv0.cdc | 42 +-- .../compute_available_withdrawal_test.cdc | 261 ++++++++++++++++++ 2 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 cadence/tests/compute_available_withdrawal_test.cdc diff --git a/cadence/contracts/FlowALPv0.cdc b/cadence/contracts/FlowALPv0.cdc index 62133be4..68b2fd16 100644 --- a/cadence/contracts/FlowALPv0.cdc +++ b/cadence/contracts/FlowALPv0.cdc @@ -101,19 +101,27 @@ access(all) contract FlowALPv0 { : 0.0 as UFix128 return (deltaDebt * borrowFactor) / withdrawSnap.getPrice() } else { - // withdrawing reduces collateral + // withdrawing reduces collateral (and may flip into debt beyond zero) let trueBalance = FlowALPMath.scaledBalanceToTrueBalance( withdrawBal!.scaledBalance, interestIndex: withdrawSnap.getCreditIndex() ) - let maxPossible = trueBalance let requiredCollateral = effectiveDebtTotal * targetHealth if effectiveCollateralTotal <= requiredCollateral { return 0.0 } let deltaCollateralEffective = effectiveCollateralTotal - requiredCollateral let deltaTokens = (deltaCollateralEffective / collateralFactor) / withdrawSnap.getPrice() - return deltaTokens > maxPossible ? maxPossible : deltaTokens + if deltaTokens <= trueBalance { + // Health target is hit before exhausting credit — collateral-only withdrawal + return deltaTokens + } + // Exhausting all credit still leaves health above target: add debt capacity + let collateralEffectiveValue = (withdrawSnap.getPrice() * trueBalance) * collateralFactor + let remainingCollateral = effectiveCollateralTotal - collateralEffectiveValue + let availableDebtIncrease = (remainingCollateral / targetHealth) - effectiveDebtTotal + let additionalTokens = (availableDebtIncrease * borrowFactor) / withdrawSnap.getPrice() + return trueBalance + additionalTokens } } @@ -312,7 +320,7 @@ access(all) contract FlowALPv0 { if let tokenState = self.state.getTokenState(tokenType) { return tokenState.getInsuranceRate() } - + return nil } @@ -501,7 +509,7 @@ access(all) contract FlowALPv0 { post { !self.state.isPositionLocked(pid): "Position is not unlocked" } - + self.lockPosition(pid) let positionView = self.buildPositionView(pid: pid) @@ -521,7 +529,7 @@ access(all) contract FlowALPv0 { let Pc_oracle = self.config.getPriceOracle().price(ofToken: seizeType)! // collateral price given by oracle ($/C) // Price of collateral, denominated in debt token, implied by oracle (D/C) // Oracle says: "1 unit of collateral is worth `Pcd_oracle` units of debt" - let Pcd_oracle = Pc_oracle / Pd_oracle + let Pcd_oracle = Pc_oracle / Pd_oracle // Compute the health factor which would result if we were to accept this liquidation let Ce_pre = balanceSheet.effectiveCollateral // effective collateral pre-liquidation @@ -532,7 +540,7 @@ access(all) contract FlowALPv0 { // Ce_seize = effective value of seized collateral ($) let Ce_seize = FlowALPMath.effectiveCollateral(credit: UFix128(seizeAmount), price: UFix128(Pc_oracle), collateralFactor: Fc) // De_seize = effective value of repaid debt ($) - let De_seize = FlowALPMath.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) + let De_seize = FlowALPMath.effectiveDebt(debit: UFix128(repayAmount), price: UFix128(Pd_oracle), borrowFactor: Fd) let Ce_post = Ce_pre - Ce_seize // position's total effective collateral after liquidation ($) let De_post = De_pre - De_seize // position's total effective debt after liquidation ($) let postHealth = FlowALPMath.healthComputation(effectiveCollateral: Ce_post, effectiveDebt: De_post) @@ -551,9 +559,9 @@ access(all) contract FlowALPv0 { message: "DEX/oracle price deviation too large. Dex price: \(Pcd_dex), Oracle price: \(Pcd_oracle)") // Execute the liquidation let seizedCollateral <- self._doLiquidation(pid: pid, repayment: <-repayment, debtType: debtType, seizeType: seizeType, seizeAmount: seizeAmount) - + self.unlockPosition(pid) - + return <- seizedCollateral } @@ -563,7 +571,7 @@ access(all) contract FlowALPv0 { access(self) fun _doLiquidation(pid: UInt64, repayment: @{FungibleToken.Vault}, debtType: Type, seizeType: Type, seizeAmount: UFix64): @{FungibleToken.Vault} { pre { !self.isPausedOrWarmup(): "Liquidations are paused by governance" - // position must have debt and collateral balance + // position must have debt and collateral balance } let repayAmount = repayment.balance @@ -1670,7 +1678,7 @@ access(all) contract FlowALPv0 { // Validate constraint: non-zero rate requires swapper if insuranceRate > 0.0 { assert( - tsRef.getInsuranceSwapper() != nil, + tsRef.getInsuranceSwapper() != nil, message:"Cannot set non-zero insurance rate without an insurance swapper configured for \(tokenType.identifier)", ) } @@ -1689,13 +1697,13 @@ access(all) contract FlowALPv0 { self.isTokenSupported(tokenType: tokenType): "Unsupported token type" } let tsRef = self.state.borrowTokenState(tokenType) - ?? panic("Invariant: token state missing") + ?? panic("Invariant: token state missing") if let swapper = swapper { // Validate swapper types match assert(swapper.inType() == tokenType, message: "Swapper input type must match token type") assert(swapper.outType() == Type<@MOET.Vault>(), message: "Swapper output type must be MOET") - + } else { // cannot remove swapper if insurance rate > 0 assert( @@ -1779,7 +1787,7 @@ access(all) contract FlowALPv0 { let tsRef = self.state.borrowTokenState(tokenType) ?? panic("Invariant: token state missing") tsRef.setStabilityFeeRate(stabilityFeeRate) - + FlowALPEvents.emitStabilityFeeRateUpdated( poolUUID: self.uuid, tokenType: tokenType.identifier, @@ -1800,7 +1808,7 @@ access(all) contract FlowALPv0 { fundRef.balance >= amount, message: "Insufficient stability fund balance. Available: \(fundRef.balance), requested: \(amount)" ) - + let withdrawn <- fundRef.withdraw(amount: amount) recipient.deposit(from: <-withdrawn) @@ -2271,7 +2279,7 @@ access(all) contract FlowALPv0 { access(self) fun updateInterestRatesAndCollectInsurance(tokenType: Type) { let tokenState = self._borrowUpdatedTokenState(type: tokenType) tokenState.updateInterestRates() - + // Collect insurance if swapper is configured // Ensure reserves exist for this token type if !self.state.hasReserve(tokenType) { @@ -2353,7 +2361,7 @@ access(all) contract FlowALPv0 { access(all) fun getDefaultToken(): Type { return self.state.getDefaultToken() } - + /// Returns the deposit capacity and deposit capacity cap for a given token type access(all) fun getDepositCapacityInfo(type: Type): {String: UFix64} { let tokenState = self._borrowUpdatedTokenState(type: type) diff --git a/cadence/tests/compute_available_withdrawal_test.cdc b/cadence/tests/compute_available_withdrawal_test.cdc new file mode 100644 index 00000000..efe3614e --- /dev/null +++ b/cadence/tests/compute_available_withdrawal_test.cdc @@ -0,0 +1,261 @@ +import Test +import BlockchainHelpers + +import "MOET" +import "FlowALPEvents" +import "FlowALPModels" +import "test_helpers.cdc" + +// Tests for the Pool's computeAvailableWithdrawal logic, exercised via +// fundsAvailableAboveTargetHealthAfterDepositing (depositAmount: 0.0 for the base cases). +// +// Token setup used throughout: +// FLOW: collateralFactor=0.8, borrowFactor=1.0, price=1.0 +// MOET: collateralFactor=1.0, borrowFactor=1.0, price=1.0 (default token) +// +// Health formula: +// health = effectiveCollateral / effectiveDebt +// effectiveCollateral(FLOW) = balance * price * CF = balance * 1.0 * 0.8 +// effectiveDebt(MOET) = balance * price / BF = balance * 1.0 / 1.0 +// +// TARGET_HEALTH = 1.3 + +access(all) let user = Test.createAccount() + +access(all) let flowCF = 0.8 +access(all) let flowBF = 1.0 +access(all) let flowPrice = 1.0 +access(all) let moetCF = 1.0 +access(all) let moetBF = 1.0 +access(all) let moetPrice = 1.0 + +access(all) var snapshot: UInt64 = 0 + +access(all) +fun beforeEach() { + if getCurrentBlockHeight() > snapshot { + Test.reset(to: snapshot) + } +} + +access(all) +fun setup() { + deployContracts() + + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: flowPrice) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: moetPrice) + + createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + addSupportedTokenZeroRateCurve( + signer: PROTOCOL_ACCOUNT, + tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, + collateralFactor: flowCF, + borrowFactor: flowBF, + depositRate: 1_000_000.0, + depositCapacityCap: 1_000_000.0 + ) + + setupMoetVault(user, beFailed: false) + mintFlow(to: user, amount: 10_000.0) + + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) + + snapshot = getCurrentBlockHeight() +} + +// --------------------------------------------------------------------------- +// Test 1: Position already at target health → 0 available for both token types +// +// Setup: 100 FLOW deposited, pushed to draw-down sink (borrowed to target health). +// effectiveCollateral = 100 * 0.8 = 80 +// maxMOETBorrow = 80 / 1.3 = 61.538... +// health ≈ target (1.3) +// +// Expected: nothing available to withdraw for either FLOW or MOET. +// --------------------------------------------------------------------------- +access(all) +fun test_atTargetHealth_nothingAvailable() { + let flowDeposit = 100.0 + let pid = openPosition(flowAmount: flowDeposit, push: true) + + let availableMOET = fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: MOET_TOKEN_IDENTIFIER, + targetHealth: INT_TARGET_HEALTH, + depositType: FLOW_TOKEN_IDENTIFIER, + depositAmount: 0.0, + beFailed: false + ) + Test.assert(equalWithinVariance(0.0, availableMOET), + message: "Expected 0 MOET available at target health, got \(availableMOET)") + + let availableFLOW = fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: FLOW_TOKEN_IDENTIFIER, + targetHealth: INT_TARGET_HEALTH, + depositType: MOET_TOKEN_IDENTIFIER, + depositAmount: 0.0, + beFailed: false + ) + Test.assert(equalWithinVariance(0.0, availableFLOW), + message: "Expected 0 FLOW available at target health, got \(availableFLOW)") +} + +// --------------------------------------------------------------------------- +// Test 2: No credit in withdraw token, zero existing debt +// → full borrow capacity available (pure debt increase path) +// +// Setup: 100 FLOW, no borrow (push=false). +// effectiveCollateral = 100 * 0.8 = 80 +// effectiveDebt = 0 +// availableDebtIncrease = 80 / 1.3 +// availableMOET = (80 / 1.3) * 1.0 / 1.0 = 80 / 1.3 ≈ 61.538 +// +// This exercises the "no credit in withdraw token" branch at the bottom of +// computeAvailableWithdrawal. +// --------------------------------------------------------------------------- +access(all) +fun test_noCreditInWithdrawToken_zerodebt_fullBorrowCapacity() { + let flowDeposit = 100.0 + let pid = openPosition(flowAmount: flowDeposit, push: false) + + let effectiveCollateral = flowDeposit * flowCF * flowPrice // 80 + let expectedAvailable = effectiveCollateral / TARGET_HEALTH // 80 / 1.3 + + let actualAvailable = fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: MOET_TOKEN_IDENTIFIER, + targetHealth: INT_TARGET_HEALTH, + depositType: FLOW_TOKEN_IDENTIFIER, + depositAmount: 0.0, + beFailed: false + ) + Test.assert(equalWithinVariance(expectedAvailable, actualAvailable), + message: "Expected \(expectedAvailable) MOET available (zero-debt full capacity), got \(actualAvailable)") +} + +// --------------------------------------------------------------------------- +// Test 3: Credit in withdraw token, partial collateral withdrawal only +// → potentialHealth ≤ targetHealth when all credit is removed +// +// Setup: +// 1. 100 FLOW, push=true → borrows 80/1.3 ≈ 61.538 MOET (health = target) +// 2. Increase FLOW price to 2.0 → effectiveCollateral = 100 * 2.0 * 0.8 = 160 +// effectiveDebt = 61.538 (unchanged, in MOET units) +// health = 160 / 61.538 ≈ 2.6 +// +// Withdraw FLOW (has credit): +// Removing all 100 FLOW credit → effectiveCollateral goes to 0 (< effectiveDebt) → partial +// availableEffective = 160 - 1.3 * 61.538 = 160 - 80 = 80 +// availableTokens = 80 / (flowCF * flowPrice_new) = 80 / (0.8 * 2.0) = 50.0 +// +// Expected: 50.0 FLOW +// --------------------------------------------------------------------------- +access(all) +fun test_creditInWithdrawToken_partialCollateralOnly() { + let flowDeposit = 100.0 + let pid = openPosition(flowAmount: flowDeposit, push: true) + + // Confirm the borrow happened and health is at target + let healthAtCreation = getPositionHealth(pid: pid, beFailed: false) + Test.assert(equalWithinVariance(UFix64(INT_TARGET_HEALTH), UFix64(healthAtCreation)), + message: "Expected health ≈ 1.3 after creation with push, got \(healthAtCreation)") + + // Increase FLOW price to 2.0 → more headroom above target + let newFlowPrice = 2.0 + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: newFlowPrice) + + // effectiveCollateral = 100 * 2.0 * 0.8 = 160 + // effectiveDebt = 80/1.3 (borrowed at original price 1.0, in MOET units) + // NOTE: 80.0/1.3 * 1.3 = 80 (the "required collateral at target health") + // availableEffective = 160 - 80 = 80 + // availableTokens = 80 / (0.8 * 2.0) = 50.0 + let expectedAvailable = 50.0 + + let actualAvailable = fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: FLOW_TOKEN_IDENTIFIER, + targetHealth: INT_TARGET_HEALTH, + depositType: MOET_TOKEN_IDENTIFIER, + depositAmount: 0.0, + beFailed: false + ) + Test.assert(equalWithinVariance(expectedAvailable, actualAvailable), + message: "Expected \(expectedAvailable) FLOW available (partial collateral withdrawal), got \(actualAvailable)") +} + +// --------------------------------------------------------------------------- +// Test 4: Credit in withdraw token flips into debt — FLO-22 scenario +// → potentialHealth > targetHealth even after all credit is removed +// +// This verifies the "flip into debt" branch of computeAvailableWithdrawal +// which enables withdrawals beyond the deposited credit balance. +// +// Setup: +// 1. Deposit 100 FLOW (push=false) → 100 FLOW credit, 0 debt +// 2. Deposit 200 MOET to same position (push=false) → 200 MOET credit added +// effectiveCollateral = 80 (FLOW) + 200 (MOET) = 280 +// effectiveDebt = 0 +// +// Withdraw FLOW: +// Removing all 100 FLOW credit: +// remaining effectiveCollateral = 280 - 80 = 200 +// potentialHealth = 200 / 0 = infinite > target → flip into debt branch +// collateralTokenCount = 100 +// availableDebtIncrease = 200 / 1.3 ≈ 153.846 +// additionalFLOW = 153.846 * 1.0 / 1.0 ≈ 153.846 +// total = 100 + 153.846 ≈ 253.846 +// +// Expected: 100.0 + 200.0 / 1.3 ≈ 253.846 FLOW +// --------------------------------------------------------------------------- +access(all) +fun test_creditFlipsIntoDebt_availabilityExceedsCreditBalance() { + let flowDeposit = 100.0 + let moetDeposit = 200.0 + let pid = openPosition(flowAmount: flowDeposit, push: false) + + // Give user MOET to deposit as second collateral + mintMoet(signer: PROTOCOL_ACCOUNT, to: user.address, amount: moetDeposit, beFailed: false) + + // Deposit MOET as additional collateral (no borrow — push=false) + depositToPosition( + signer: user, + positionID: pid, + amount: moetDeposit, + vaultStoragePath: MOET.VaultStoragePath, + pushToDrawDownSink: false + ) + + // effectiveCollateral = 100 * 0.8 + 200 * 1.0 = 280 + // effectiveDebt = 0 + // After removing all FLOW credit: remaining effectiveCollateral = 200 (MOET only) + // availableDebtIncrease = 200 / 1.3 + // additionalFLOW = 200 / 1.3 * 1.0 / 1.0 + // total = 100 + 200/1.3 + let expectedAvailable = flowDeposit + moetDeposit / TARGET_HEALTH + + let actualAvailable = fundsAvailableAboveTargetHealthAfterDepositing( + pid: pid, + withdrawType: FLOW_TOKEN_IDENTIFIER, + targetHealth: INT_TARGET_HEALTH, + depositType: MOET_TOKEN_IDENTIFIER, + depositAmount: 0.0, + beFailed: false + ) + Test.assert(equalWithinVariance(expectedAvailable, actualAvailable), + message: "Expected \(expectedAvailable) FLOW available (credit→debt flip), got \(actualAvailable)") +} + +access(self) +fun openPosition(flowAmount: UFix64, push: Bool): UInt64 { + let openRes = executeTransaction( + "../transactions/flow-alp/position/create_position.cdc", + [flowAmount, FLOW_VAULT_STORAGE_PATH, push], + user + ) + Test.expect(openRes, Test.beSucceeded()) + + let evts = Test.eventsOfType(Type()) + let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened + return openedEvt.pid +} \ No newline at end of file From 52604fae4dfae4996d78e2f3758475c70c2f85e2 Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Mon, 9 Mar 2026 18:50:34 +0100 Subject: [PATCH 2/2] clean up tests --- .../fork_multi_collateral_position_test.cdc | 379 +++++++----------- .../position/repay_and_close_position.cdc | 27 +- 2 files changed, 169 insertions(+), 237 deletions(-) diff --git a/cadence/tests/fork_multi_collateral_position_test.cdc b/cadence/tests/fork_multi_collateral_position_test.cdc index 3c848c2a..4932c2af 100644 --- a/cadence/tests/fork_multi_collateral_position_test.cdc +++ b/cadence/tests/fork_multi_collateral_position_test.cdc @@ -41,7 +41,7 @@ fun setup() { setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 2000.0) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_MOET_TOKEN_ID, price: 1.0) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WBTC_TOKEN_ID, price: 40000.0) - + // Add FLOW as supported token (80% CF, 90% BF) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, @@ -51,7 +51,7 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - + // Add USDF as supported token (90% CF, 95% BF) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, @@ -61,7 +61,7 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - + // Add WETH as supported token (75% CF, 85% BF) addSupportedTokenZeroRateCurve( signer: MAINNET_PROTOCOL_ACCOUNT, @@ -81,7 +81,7 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) - + snapshot = getCurrentBlockHeight() } @@ -93,13 +93,13 @@ fun setup() { access(all) fun test_multi_collateral_position() { safeReset() - + // STEP 1: Setup MOET liquidity provider for borrowing let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + let FLOWAmount = 1000.0 let USDFAmount = 500.0 let WETHAmount = 0.05 @@ -110,7 +110,7 @@ fun test_multi_collateral_position() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + transferFlowTokens(to: user, amount: FLOWAmount) transferFungibleTokens( tokenIdentifier: MAINNET_USDF_TOKEN_ID, @@ -124,10 +124,10 @@ fun test_multi_collateral_position() { to: user, amount: WETHAmount ) - + // STEP 3: Create position with FLOW collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid @@ -139,7 +139,7 @@ fun test_multi_collateral_position() { depositToPosition(signer: user, positionID: pid, amount: USDFAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) // STEP 5: Add WETH collateral depositToPosition(signer: user, positionID: pid, amount: WETHAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 @@ -147,7 +147,7 @@ fun test_multi_collateral_position() { // Total collateral: $1325 // // Debt: $0 - + // Verify all balances let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) @@ -156,18 +156,18 @@ fun test_multi_collateral_position() { Test.assertEqual(USDFAmount, usdfCredit) let wethCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_WETH_TOKEN_ID)!) Test.assertEqual(WETHAmount, wethCredit) - + // Health still infinite (no debt) health = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(CEILING_HEALTH, health) - + // STEP 6: Test weighted collateral factors - calculate max borrowing // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // MOET: maxBorrow = ($1325 / 1.1) * 1.0 / $1.00 = 1204.54545455 MOET let expectedMaxMoet: UFix64 = 1204.54545455 let availableMoet = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_MOET_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) Test.assertEqual(expectedMaxMoet, availableMoet) - + // STEP 7: Borrow 1204 MOET to create debt borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1204.0, beFailed: false) @@ -183,7 +183,7 @@ fun test_multi_collateral_position() { // Total debt: $1204 // // Health = $1325 / $1204 = 1.100498338870431893687707 - + health = getPositionHealth(pid: pid, beFailed: false) let expectedHealth: UFix128 = 1.100498338870431893687707 Test.assertEqual(expectedHealth, health) @@ -196,12 +196,12 @@ fun test_multi_collateral_position() { access(all) fun test_cross_asset_flow_to_usdf_borrowing() { safeReset() - + // STEP 1: Setup USDF liquidity provider let usdfLp = Test.createAccount() var res = setupGenericVault(usdfLp, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + transferFungibleTokens( tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, @@ -209,31 +209,31 @@ fun test_cross_asset_flow_to_usdf_borrowing() { amount: 10000.0 ) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // STEP 2: Setup test user with FLOW let user = Test.createAccount() setupMoetVault(user, beFailed: false) res = setupGenericVault(user, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 transferFlowTokens(to: user, amount: flowAmount) - + // STEP 3: Create position with FLOW collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * CF(0.8) = $800 // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max USDF: ($800 / 1.1) * 0.95 / $1.00 = ~ 690.909 USDF - + // STEP 4: Borrow USDF against FLOW collateral let usdfBorrowAmount: UFix64 = 600.0 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, amount: usdfBorrowAmount, beFailed: false) - + // Position state: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * CF(0.8) = $800 @@ -242,17 +242,17 @@ fun test_cross_asset_flow_to_usdf_borrowing() { // USDF: 600 * $1.00 / BF(0.95) = $631.58 // // Health = $800 / $631.58 = 1.266666666666666666666666 - + let health = getPositionHealth(pid: pid, beFailed: false) let expectedHealth: UFix128 = 1.266666666666666666666666 - + Test.assertEqual(expectedHealth, health) - + // Verify balances let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) Test.assertEqual(flowAmount, flowCredit) - + let usdfDebit = getDebitBalanceForType(details: details, vaultType: CompositeType(MAINNET_USDF_TOKEN_ID)!) Test.assertEqual(usdfBorrowAmount, usdfDebit) } @@ -265,19 +265,19 @@ fun test_cross_asset_flow_to_usdf_borrowing() { access(all) fun test_cross_asset_flow_usdf_weth_borrowing() { safeReset() - + // STEP 1: Setup liquidity providers for USDF and WETH let usdfLp = Test.createAccount() var res = setupGenericVault(usdfLp, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: usdfLp, amount: 10000.0) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + let wethLp = Test.createAccount() res = setupGenericVault(wethLp, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: wethLp, amount: 0.05) - + let tinyDeposit = 0.00000001 setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: wethLp, amount: 0.05, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) @@ -289,17 +289,17 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 transferFlowTokens(to: user, amount: flowAmount) - + // STEP 3: Create position with FLOW createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // @@ -309,18 +309,18 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { // // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max USDF: ($800 / 1.1) * 0.95 = 690.90909091 USDF - + var health: UFix128 = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(CEILING_HEALTH, health) - + let usdfBorrowAmount: UFix64 = 500.0 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, amount: usdfBorrowAmount, beFailed: false) - // Collateral (effectiveCollateral = balance * price * collateralFactor): + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt (effectiveDebt = balance * price / borrowFactor): + // Debt (effectiveDebt = balance * price / borrowFactor): // USDF: 500 * $1.00 / 0.95 = $526.315789474 // // Health = $800 / $526.315789474 = 1.52 @@ -330,34 +330,34 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { // STEP 4: Deposit borrowed USDF as collateral depositToPosition(signer: user, positionID: pid, amount: usdfBorrowAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // New collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 // Total collateral: $1250 // - // Debt (effectiveDebt = balance * price / borrowFactor): + // Debt (effectiveDebt = balance * price / borrowFactor): // USDF: 500 * $1.00 / 0.95 = $526.315789474 // // After netting USDF (credit 500 - debt 500 = 0): // - // Collateral: + // Collateral: // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // // Debt $0 // // Health = $800 / $0 = ∞ (UFix128.max) - + health = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(CEILING_HEALTH, health) - + // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max WETH: ($1250 / 1.1) * 0.85 / $2000 = ~0.48295454545 WETH // But we only have 0.05 WETH on pool available, so borrow 0.04 let wethBorrowAmount: UFix64 = 0.04 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, amount: wethBorrowAmount, beFailed: false) - + // Final position: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 @@ -369,13 +369,13 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { // // After netting USDF (credit 500 - debt 500 = 0): // - // Collateral: - // FLOW: 1000 * $1.00 * 0.8 = $800 + // Collateral: + // FLOW: 1000 * $1.00 * 0.8 = $800 // Debt: // WETH: 0.04 * $2000 / 0.85 = $94.117647059 // // Health = $800 / $94.117647059 = 8.5 - + health = getPositionHealth(pid: pid, beFailed: false) expectedHealth = 8.5 Test.assertEqual(expectedHealth, health) @@ -388,7 +388,7 @@ fun test_cross_asset_flow_usdf_weth_borrowing() { access(all) fun test_cross_asset_chain() { safeReset() - + // STEP 1: Setup all liquidity providers // USDF LP let usdfLp = Test.createAccount() @@ -396,7 +396,7 @@ fun test_cross_asset_chain() { Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: usdfLp, amount: 1000.0) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 1000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // WETH LP (0.05 WETH available) let wethLp = Test.createAccount() res = setupGenericVault(wethLp, vaultIdentifier: MAINNET_WETH_TOKEN_ID) @@ -406,7 +406,7 @@ fun test_cross_asset_chain() { let tinyDeposit = 0.0000001 setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, minimum: tinyDeposit) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: wethLp, amount: 0.05, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // WBTC LP (0.0004 WBTC available) let wbtcLp = Test.createAccount() res = setupGenericVault(wbtcLp, vaultIdentifier: MAINNET_WBTC_TOKEN_ID) @@ -415,7 +415,7 @@ fun test_cross_asset_chain() { setMinimumTokenBalancePerPosition(signer: MAINNET_PROTOCOL_ACCOUNT, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_ID, minimum: tinyDeposit) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: wbtcLp, amount: 0.0004, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, pushToDrawDownSink: false) - + // STEP 2: Setup user with FLOW position let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -425,50 +425,50 @@ fun test_cross_asset_chain() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WBTC_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 transferFlowTokens(to: user, amount: flowAmount) - + // STEP 3: Create position and execute complete chain createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Max borrow ((effectiveCollateral / minHealth) * borrowFactor / price): // Max USDF = ($800 / 1.1) * 0.95 / $1.0 = ~690.9090 USDF - + // Step 4: Borrow USDF let usdfBorrow: UFix64 = 600.0 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_USDF_TOKEN_ID, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, amount: usdfBorrow, beFailed: false) - + // Step 5: Deposit USDF, borrow WETH (limited by available liquidity) depositToPosition(signer: user, positionID: pid, amount: usdfBorrow, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt: + // Debt: // USDF (0 netted) // // Max borrow = (effectiveCollateral / minHealth) * borrowFactor / price // Max WETH = ($800 / 1.1) * 0.85 / $2000 = ~0.30909090 WETH // limited by available liquidity: 0.0005 max - let wethBorrow: UFix64 = 0.0005 + let wethBorrow: UFix64 = 0.0005 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_WETH_TOKEN_ID, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, amount: wethBorrow, beFailed: false) - + // Step 6: Deposit WETH, borrow WBTC depositToPosition(signer: user, positionID: pid, amount: wethBorrow, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 // - // Debt: + // Debt: // USDF (0 netted) // WETH (0 netted) // @@ -478,7 +478,7 @@ fun test_cross_asset_chain() { // Limited by available liquidity (0.0004 total) let wbtcBorrow: UFix64 = 0.0004 borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_WBTC_TOKEN_ID, vaultStoragePath: MAINNET_WBTC_STORAGE_PATH, amount: wbtcBorrow, beFailed: false) - + // Final position: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 @@ -488,23 +488,23 @@ fun test_cross_asset_chain() { // WBTC: 0.0004 * $40000 / 0.8 = $20 // // Health = $800 / $20 = 40 - + let finalHealth = getPositionHealth(pid: pid, beFailed: false) let expectedHealth: UFix128 = 40.0 Test.assertEqual(expectedHealth, finalHealth) - + // Verify all balances let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_FLOW_TOKEN_ID)!) Test.assertEqual(1000.0, flowCredit) - + let usdfCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_USDF_TOKEN_ID)!) Test.assertEqual(0.0, usdfCredit) - + let wethCredit = getCreditBalanceForType(details: details, vaultType: CompositeType(MAINNET_WETH_TOKEN_ID)!) Test.assertEqual(0.0, wethCredit) - + let wbtcDebit = getDebitBalanceForType(details: details, vaultType: CompositeType(MAINNET_WBTC_TOKEN_ID)!) Test.assertEqual(0.0004, wbtcDebit) } @@ -517,13 +517,13 @@ fun test_cross_asset_chain() { access(all) fun test_multi_asset_uncorrelated_price_movements() { safeReset() - + // STEP 1: Setup liquidity providers for MOET let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // STEP 2: Setup test user with FLOW, USDF, and WETH let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -531,25 +531,25 @@ fun test_multi_asset_uncorrelated_price_movements() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 let usdfAmount: UFix64 = 500.0 let wethAmount: UFix64 = 0.05 - + transferFlowTokens(to: user, amount: flowAmount) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: user, amount: usdfAmount) transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: wethAmount) - + // STEP 3: Create position with FLOW collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + // STEP 4: Add USDF and WETH collateral depositToPosition(signer: user, positionID: pid, amount: usdfAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) depositToPosition(signer: user, positionID: pid, amount: wethAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * CF(0.8) = $800 // USDF: 500 * $1.00 * CF(0.9) = $450 @@ -558,20 +558,20 @@ fun test_multi_asset_uncorrelated_price_movements() { // STEP 5: Borrow 1000 MOET to create debt borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 1000.0, beFailed: false) - + // Position state after borrow at initial prices: // Collateral (effectiveCollateral = balance * price * collateralFactor): // Total collateral: $1325 (unchanged) // // Debt (effectiveDebt = balance * price / borrowFactor): // MOET: 1000 * $1.00 / BF(1.0) = $1000 - // + // // Health = $1325 / $1000 = 1.325 - + let initialHealth = getPositionHealth(pid: pid, beFailed: false) let expectedInitialHealth: UFix128 = 1.325 Test.assertEqual(expectedInitialHealth, initialHealth) - + // STEP 6: Test uncorrelated price movements // FLOW: $1.00 → $1.10 (+10%) @@ -582,7 +582,7 @@ fun test_multi_asset_uncorrelated_price_movements() { setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 0.95) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 2400.0) // MOET remains at $1.00 - + // New position state with changed prices: // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.10 * CF(0.8) = $880 (was $800, +$80) @@ -594,10 +594,10 @@ fun test_multi_asset_uncorrelated_price_movements() { // MOET: 1000 * $1.00 / BF(1.0) = $1000 // // Health = $1397.50 / $1000 = 1.3975 - + let healthAfterChange = getPositionHealth(pid: pid, beFailed: false) let expectedHealthAfterChange: UFix128 = 1.3975 - + Test.assertEqual(expectedHealthAfterChange, healthAfterChange) } @@ -607,39 +607,39 @@ fun test_multi_asset_uncorrelated_price_movements() { access(all) fun test_multi_asset_partial_withdrawal() { safeReset() - + // STEP 1: Setup MOET liquidity provider // We need someone else to deposit MOET so there's liquidity for borrowing let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) - + // MOET LP deposits MOET (creates MOET credit balance = provides liquidity) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // STEP 2: Setup test user let user = Test.createAccount() setupMoetVault(user, beFailed: false) transferFlowTokens(to: user, amount: 1000.0) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: user.address, amount: 500.0, beFailed: false) - + // STEP 3: Create position with FLOW createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + // STEP 4: Add MOET collateral depositToPosition(signer: user, positionID: pid, amount: 500.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // Initial collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // MOET: 500 * $1.00 * 1.0 = $500 // Total collateral: $1300 - + // STEP 5: Borrow 400 MOET borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 400.0, beFailed: false) - + // Position state after borrow: // MOET borrow (netting): // 1) Had 500 MOET credit @@ -656,12 +656,12 @@ fun test_multi_asset_partial_withdrawal() { // MOET: $0 // // Health = $900 / $0 = ∞ (UFix128.max) - + let initialHealth = getPositionHealth(pid: pid, beFailed: false) - + // STEP 6: Withdraw 300 FLOW (partial withdrawal) borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_FLOW_TOKEN_ID, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, amount: 300.0, beFailed: false) - + // Position state after FLOW withdrawal: // FLOW withdrawal mechanics: // 1) Had 1000 FLOW credit @@ -678,89 +678,22 @@ fun test_multi_asset_partial_withdrawal() { // MOET: $0 // // Health = $660 / $0 = (no debt) - + let newHealth = getPositionHealth(pid: pid, beFailed: false) - + // Both healths are infinite (no debt), so they're equal // We can't test health decrease when there's no debt // Instead verify the collateral decreased let details = getPositionDetails(pid: pid, beFailed: false) let remainingFlow = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) Test.assertEqual(700.0, remainingFlow) - + let remainingMoet = getCreditBalanceForType(details: details, vaultType: Type<@MOET.Vault>()) Test.assertEqual(100.0, remainingMoet) Test.assertEqual(newHealth, initialHealth) } -// ----------------------------------------------------------------------------- -// Cross-Collateral Borrowing Capacity -// Tests borrowing capacity when using multiple collateral types -// Key insight: Withdrawing a token which user deposited as collateral (MOET and FLOW) is -// limited by user credit balance. Borrowing a token not held as collateral -// (USDF) creates new debt and is limited by health factor. -// ----------------------------------------------------------------------------- -access(all) -fun test_cross_collateral_borrowing_capacity() { - safeReset() - - // STEP 1: Setup MOET and USDF liquidity providers - let moetLp = Test.createAccount() - setupMoetVault(moetLp, beFailed: false) - mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 10000.0, beFailed: false) - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 10000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: MAINNET_USDF_HOLDER, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - - // STEP 2: Setup test user with FLOW + MOET collateral - let user = Test.createAccount() - setupMoetVault(user, beFailed: false) - var res = setupGenericVault(user, vaultIdentifier: MAINNET_USDF_TOKEN_ID) - Test.expect(res, Test.beSucceeded()) - transferFlowTokens(to: user, amount: 1000.0) - mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: user.address, amount: 900.0, beFailed: false) - - // STEP 3: Create position with FLOW + MOET collateral - createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - - let openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - - depositToPosition(signer: user, positionID: pid, amount: 900.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - - // Collateral (effectiveCollateral = balance * price * collateralFactor): - // FLOW: 1000 * $1.00 * CF(0.8) = $800 - // MOET: 900 * $1.00 * CF(1.0) = $900 - // Total collateral: $1700 - // - // Debt: $0 - // - // Health: ∞ (no debt) - - // STEP 4: Calculate position's balance available for withdrawal for each token - // maxBorrow = (effectiveCollateral / minHealth) * borrowFactor / price - // Using default minHealth = 1.1 - // - // MOET (credit token) -> 900 MOET (limited by credit balance: withdrawing deposited collateral, not new debt) - // USDF (no balance, different from collateral, limited by health factor): maxBorrow = ($1700 / 1.1) * 0.95 / $1.00 = ~1468.18181818 USDF - // FLOW (credit token): -> 1000 FLOW (limited by credit balance: withdrawing deposited collateral, not new debt) - - // Test MOET borrowing (limited by credit amount) - let expectedMaxMoet: UFix64 = 900.0 - let availableMoet = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_MOET_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) - Test.assertEqual(expectedMaxMoet, availableMoet) - - // Test USDF borrowing (true cross-collateral calculation) - let expectedMaxUsdf: UFix64 = 1468.18181818 - let availableUsdf = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_USDF_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) - Test.assertEqual(expectedMaxUsdf, availableUsdf) - - // Test FLOW borrowing (limited by credit amount) - let expectedMaxFlow: UFix64 = 1000.0 - let availableFlow = getAvailableBalance(pid: pid, vaultIdentifier: MAINNET_FLOW_TOKEN_ID, pullFromTopUpSource: false, beFailed: false) - Test.assertEqual(expectedMaxFlow, availableFlow) -} - // ----------------------------------------------------------------------------- // Multi-Asset Position Liquidation - Liquidator Chooses Collateral // Position with 3 collateral types (FLOW, USDF, WETH) and 2 debt types (MOET, USDF) @@ -769,19 +702,19 @@ fun test_cross_collateral_borrowing_capacity() { access(all) fun test_multi_asset_liquidation_collateral_selection() { safeReset() - + // STEP 1: Setup liquidity providers let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + let usdfLp = Test.createAccount() var res = setupGenericVault(usdfLp, vaultIdentifier: MAINNET_USDF_TOKEN_ID) Test.expect(res, Test.beSucceeded()) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: usdfLp, amount: 10000.0) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: usdfLp, amount: 10000.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - + // STEP 2: Setup user with 3 collateral types let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -789,30 +722,30 @@ fun test_multi_asset_liquidation_collateral_selection() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + let flowAmount: UFix64 = 1000.0 let usdfAmount: UFix64 = 500.0 let wethAmount: UFix64 = 0.05 - + transferFlowTokens(to: user, amount: flowAmount) transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: user, amount: usdfAmount) transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: wethAmount) - + // STEP 3: Create position with all collateral createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: flowAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - + depositToPosition(signer: user, positionID: pid, amount: usdfAmount, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) depositToPosition(signer: user, positionID: pid, amount: wethAmount, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 // WETH: 0.05 * $2000 * 0.75 = $75 // Total collateral: $1325 - + // STEP 4: Create 2 debt types by borrowing // First borrow MOET // maxBorrow = (effectiveCollateral / minHealth) * borrowFactor / price @@ -849,7 +782,7 @@ fun test_multi_asset_liquidation_collateral_selection() { // Position now has: // Collateral (effectiveCollateral = balance * price * collateralFactor): - // FLOW: 1000 * $1.00 * 0.8 = $800 + // FLOW: 1000 * $1.00 * 0.8 = $800 // WETH: 0.05 * $2000 * 0.75 = $75 // Total collateral: $875 // @@ -859,7 +792,7 @@ fun test_multi_asset_liquidation_collateral_selection() { // Total debt: $752.63 // // Health = $875 / $752.63 = 1.163 (still healthy, will become unhealthy after price drop) - + let healthBefore = getPositionHealth(pid: pid, beFailed: false) Test.assert(healthBefore > 1.0, message: "Position should be healthy before price drop") @@ -873,14 +806,14 @@ fun test_multi_asset_liquidation_collateral_selection() { vaultSourceStoragePath: MOET.VaultStoragePath, priceRatio: 0.70 ) - + // New collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.7 * 0.8 = $560 // WETH: 0.05 * $2000 * 0.75 = $75 // Total collateral: $635 // // Debt (effectiveDebt = balance * price / borrowFactor): - // MOET: 700 * $1.00 / 1.0 = $700, + // MOET: 700 * $1.00 / 1.0 = $700, // USDF: 50 * $1.00 / 0.95 = $52.63 // Total debt: $752.63 // @@ -889,12 +822,12 @@ fun test_multi_asset_liquidation_collateral_selection() { let healthAfterDrop = getPositionHealth(pid: pid, beFailed: false) let expectedHealthAfterDrop: UFix128 = 0.843706293706293706293706 Test.assertEqual(expectedHealthAfterDrop, healthAfterDrop) - + // STEP 6: Liquidator chooses to seize FLOW collateral by repaying MOET debt let liquidator = Test.createAccount() setupMoetVault(liquidator, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: liquidator.address, amount: 1000.0, beFailed: false) - + // Repay 100 MOET, seize FLOW // DEX quote: 100 / 0.70 = 142.86 FLOW // Liquidator offers: 140 FLOW (better price) @@ -902,22 +835,22 @@ fun test_multi_asset_liquidation_collateral_selection() { let seizeAmount: UFix64 = 140.0 let liqRes = manualLiquidation( - signer:liquidator, - pid: pid, - debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, - seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, - seizeAmount: seizeAmount, - repayAmount: repayAmount, + signer:liquidator, + pid: pid, + debtVaultIdentifier: MAINNET_MOET_TOKEN_ID, + seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID, + seizeAmount: seizeAmount, + repayAmount: repayAmount, ) Test.expect(liqRes, Test.beSucceeded()) - + // Verify balances after liquidation let details = getPositionDetails(pid: pid, beFailed: false) let flowCredit = getCreditBalanceForType(details: details, vaultType: Type<@FlowToken.Vault>()) let expectedFlowCredit: UFix64 = 860.0 // 1000 - 140 - Test.assertEqual(expectedFlowCredit, flowCredit) - + Test.assertEqual(expectedFlowCredit, flowCredit) + let moetDebit = getDebitBalanceForType(details: details, vaultType: Type<@MOET.Vault>()) let expectedMoetDebit: UFix64 = 600.0 // 700 - 100 Test.assertEqual(expectedMoetDebit, moetDebit) @@ -935,13 +868,13 @@ fun test_multi_asset_liquidation_collateral_selection() { access(all) fun test_multi_asset_complex_workflow() { safeReset() - + // STEP 1: Setup liquidity providers let moetLp = Test.createAccount() setupMoetVault(moetLp, beFailed: false) mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: 50000.0, beFailed: false) createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: 50000.0, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) - + // STEP 2: User deposits FLOW collateral let user = Test.createAccount() setupMoetVault(user, beFailed: false) @@ -949,40 +882,40 @@ fun test_multi_asset_complex_workflow() { Test.expect(res, Test.beSucceeded()) res = setupGenericVault(user, vaultIdentifier: MAINNET_WETH_TOKEN_ID) Test.expect(res, Test.beSucceeded()) - + transferFlowTokens(to: user, amount: 1000.0) - + // STEP 3: Create position with FLOW createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: 1000.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) - + let openEvents = Test.eventsOfType(Type()) let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - - // Collateral(effectiveCollateral = balance * price * collateralFactor): + + // Collateral(effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // Total collateral: $800 - + // STEP 4: User deposits USDF collateral transferFungibleTokens(tokenIdentifier: MAINNET_USDF_TOKEN_ID, from: MAINNET_USDF_HOLDER, to: user, amount: 500.0) depositToPosition(signer: user, positionID: pid, amount: 500.0, vaultStoragePath: MAINNET_USDF_STORAGE_PATH, pushToDrawDownSink: false) - - // Collateral (effectiveCollateral = balance * price * collateralFactor): + + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $1.00 * 0.8 = $800 // USDF: 500 * $1.00 * 0.9 = $450 // Total collateral: $800 + $450 = $1250 - // + // // Debt: 0 - // + // // Health: ∞ (no debt) - + let healthAfterDeposits = getPositionHealth(pid: pid, beFailed: false) - + // STEP 5: User borrows MOET borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET.VaultStoragePath, amount: 300.0, beFailed: false) - + let healthAfterBorrow = getPositionHealth(pid: pid, beFailed: false) Test.assert(healthAfterBorrow > 1.0, message: "Position should be healthy after borrowing") - + // STEP 6: FLOW price drops 20% ($1.00 → $0.80) setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 0.80) setMockDexPriceForPair( @@ -992,7 +925,7 @@ fun test_multi_asset_complex_workflow() { vaultSourceStoragePath: MOET.VaultStoragePath, priceRatio: 0.80 ) - + // New collateral calculation (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.80 * 0.8 = $640 (was $800) // USDF: 500 * $1.00 * 0.9 = $450 (unchanged) @@ -1003,14 +936,14 @@ fun test_multi_asset_complex_workflow() { // Total debt: $300 // // Health: $1090 / $300 = 3.633 (still healthy but reduced) - + let healthAfterDrop = getPositionHealth(pid: pid, beFailed: false) - + // STEP 7: User borrows more to approach undercollateralization // Max borrow ((effectiveCollateral / minHealth) * borrowFactor / price): // Max MOET borrow = ($1090 / 1.1) * 1.0 / $1.0 = ~990.9090 MOET borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MOET.VaultStoragePath, amount: 600.0, beFailed: false) - + // Collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.80 * 0.8 = $640 (was $800) // USDF: 500 * $1.00 * 0.9 = $450 (unchanged) @@ -1020,18 +953,18 @@ fun test_multi_asset_complex_workflow() { // Total debt: $900 // // Health: $1090 / $900 = 1.211111111111111111111111 (close to minimum) - + let healthAfterSecondBorrow = getPositionHealth(pid: pid, beFailed: false) let expectedHealthAfterSecondBorrow:UFix128 = 1.211111111111111111111111 Test.assertEqual(expectedHealthAfterSecondBorrow, healthAfterSecondBorrow) - - // STEP 8: User deposits WETH as additional collateral + + // STEP 8: User deposits WETH as additional collateral // User deposits their WETH (0.05) directly to the position transferFungibleTokens(tokenIdentifier: MAINNET_WETH_TOKEN_ID, from: MAINNET_WETH_HOLDER, to: user, amount: 0.05) depositToPosition(signer: user, positionID: pid, amount: 0.05, vaultStoragePath: MAINNET_WETH_STORAGE_PATH, pushToDrawDownSink: true) // depositToPosition with pushToDrawDownSink=true: - // + // // 1. WETH is deposited → collateral increases from $1090 to $1165 // 2. Health calculation BEFORE rebalance: $1165 / $900 = 1.294444... // 3. System checks: Is health > targetHealth (1.3)? NO (1.294 < 1.3) @@ -1047,11 +980,11 @@ fun test_multi_asset_complex_workflow() { // Check if rebalance event was emitted let rebalanceEvents = Test.eventsOfType(Type()) - Test.assertEqual(1, rebalanceEvents.length) + Test.assertEqual(1, rebalanceEvents.length) let lastRebalance = rebalanceEvents[rebalanceEvents.length - 1] as! FlowALPEvents.Rebalanced Test.assertEqual(pid, lastRebalance.pid) Test.assertEqual(expectedPushedAmount, lastRebalance.amount) - + // After rebalance, position is at targetHealth (1.3) // Updated collateral (effectiveCollateral = balance * price * collateralFactor): // FLOW: 1000 * $0.80 * 0.8 = $640 @@ -1066,7 +999,7 @@ fun test_multi_asset_complex_workflow() { // Health: $1165 / $896.15384615 = 1.300000000005579399141654 let expectedHealthAfterWethDeposit: UFix128 = 1.300000000005579399141654 let expectedDebtAfterRebalance: UFix64 = 896.15384615 - + let healthAfterWethDeposit = getPositionHealth(pid: pid, beFailed: false) Test.assertEqual(expectedHealthAfterWethDeposit, healthAfterWethDeposit) diff --git a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc index 0bfd1c65..223fe531 100644 --- a/cadence/transactions/flow-alp/position/repay_and_close_position.cdc +++ b/cadence/transactions/flow-alp/position/repay_and_close_position.cdc @@ -48,18 +48,17 @@ transaction(positionId: UInt64) { sink.depositCapacity(from: self.moetWithdrawRef) } - // Now withdraw all available Flow collateral without top-up assistance - let withdrawAmount = self.position.availableBalance( - type: Type<@FlowToken.Vault>(), - pullFromTopUpSource: false - ) - let withdrawnVault <- self.position.withdrawAndPull( - type: Type<@FlowToken.Vault>(), - amount: withdrawAmount, - pullFromTopUpSource: false - ) - - // Deposit withdrawn collateral to user's vault - self.receiverRef.deposit(from: <-withdrawnVault) + let balances = self.position.getBalances() + for balance in balances { + if balance.vaultType == Type<@FlowToken.Vault>() { + let withdrawAmount = balance.balance + let withdrawnVault <- self.position.withdrawAndPull( + type: Type<@FlowToken.Vault>(), + amount: withdrawAmount, + pullFromTopUpSource: false + ) + self.receiverRef.deposit(from: <-withdrawnVault) + } + } } -} +}