This guide covers advanced techniques for liquidating positions in Flux Protocol, including capital-free liquidations, leveraged liquidations, and complex multi-step strategies.
Overview
During liquidation, the liquidator receives a callback where they have access to all locked_* operations, just like a manager. This enables powerful strategies:
Capital-Free Liquidation - Use seized collateral to pay for liquidation
Leveraged Liquidation - Borrow from the vault to liquidate
Flash Liquidation - Borrow, liquidate, repay in single transaction
Cross-Protocol Arbitrage - Liquidate via external DEXs for profit
Recursive Liquidation - Open position against vault to liquidate another manager
Key Insight: Liquidators are temporary managers during the callback. They can borrow, deposit, withdraw, and interact with wrappers just like regular managers.
Available Operations During Liquidation
During the liquidation callback, liquidators have access to:
interface ILiquidatorOperations {
// Capital operations
function locked_borrow(uint256 amount) external;
function locked_repay(uint256 amount) external;
// Bond operations
function locked_depositBond(uint256 amount) external;
function locked_withdrawBond(uint256 amount) external;
// Wrapper operations
function locked_registerWrapper(address wrapper) external;
function locked_deregisterWrapper(address wrapper) external;
function locked_depositToWrapper(IAsset wrapper, bytes32 positionId, uint256 amount) external;
function locked_withdrawFromWrapper(IAsset wrapper, bytes32 positionId, uint256 amount) external;
function locked_claimFromWrapper(IAsset wrapper, bytes32 positionId, uint256 amountOrTokenId) external;
function locked_interactWithWrapper(address wrapper, bytes calldata data) external;
// Working capital management
function locked_moveFunds(bytes32 fromPositionId, bytes32 toPositionId, uint256 amount) external;
}
What You Receive:
All of manager's positions (bond + wrappers + working capital)
Lock on vault (atomic execution)
Ability to use vault operations
What You Must Return:
Minimum payment in your working capital (base asset wrapper)
Vault verifies this after callback completes
Strategy 1: Capital-Free Liquidation (Basic)
Use the seized collateral itself to pay for the liquidation.
Concept
Implementation
Example
Advantages
Zero capital required
No external funding needed
Simple implementation
Gas efficient
Limitations
Only works if collateral > min payment
Collateral already in base asset (or must swap)
Lower profit than leveraged strategies
Strategy 2: Leveraged Liquidation
Borrow from the vault itself to increase your liquidation capacity and profit.
Concept
During liquidation callback, become a manager by borrowing from the vault. Use that borrowed capital plus seized collateral to maximize profit.
Implementation
Example
Advantages
Can liquidate positions even with insufficient collateral
Maximize profit by using vault capital
Handle complex multi-asset positions
Optimize swaps for best prices
Limitations
More complex implementation
Higher gas costs
Requires bond posting
Must repay borrowed capital
Strategy 3: Recursive/Nested Liquidation
Open a position against the vault to liquidate another manager.
This is the most advanced strategy: You become a manager yourself, borrow capital, use it to liquidate another manager, and close your position in the same transaction.
Concept
Implementation
Execution Flow
Example
Advantages
True capital-free: Use vault's own capital
Can liquidate very large positions
Atomic execution
No need for external funding
Scales with vault size
Limitations
Very complex implementation
High gas costs (nested callbacks)
Requires tracking callback depth
Must manage bond requirements
Vault must have idle liquidity
Strategy 4: Flash Loan Liquidation
Use external flash loans to liquidate, then repay the flash loan from seized collateral.
Concept
Implementation
Example
Advantages
No capital required
Can liquidate unlimited size positions
Pay minimal flash loan fees
No vault borrowing (don't become manager)
Limitations
Requires flash loan provider integration
Flash loan fees eat into profit
Must repay same transaction
Complex error handling
Strategy 5: Cross-Protocol Arbitrage
Liquidate positions and sell on external DEXs for better prices.
Concept
Instead of just unwinding positions to base asset, route through multiple DEXs to maximize profit:
Implementation
Advantages
Maximize profit through best execution
Reduce slippage by routing optimally
Can capture arbitrage opportunities
Better for large liquidations
Limitations
More gas intensive (multiple quote checks)
Requires maintaining multiple DEX integrations
Quotes may be stale by execution time
Increased complexity
Comparison of Strategies
Strategy
Capital Required
Complexity
Gas Cost
Max Profit
Best For
Capital-Free
None
Low
Low
Medium
Simple positions, base asset collateral
Leveraged
None (vault capital)
Medium
Medium
High
Complex multi-asset positions
Recursive
None (vault capital)
Very High
High
Very High
Large positions, low vault utilization
Flash Loan
None (flash loan)
High
Medium
High
Very large positions
Arbitrage
Varies
High
High
Highest
Multi-asset positions, volatile markets
Best Practices
For All Strategies
Do:
Calculate gas costs before executing
Check vault has sufficient idle liquidity (for borrowing strategies)
Verify position is still liquidatable before executing
Use proper slippage protection on DEX swaps
Test on testnet/fork first
Handle all error cases gracefully
Don't:
Assume seized collateral is in base asset
Ignore flash loan fees in profit calculation
Use unlimited approvals carelessly
Forget to repay borrowed capital
Skip callback depth tracking for recursive strategies
Gas Optimization
Profit Calculation
Always account for all costs:
Error Handling
Security Considerations
Reentrancy Protection
Flux vault has nonReentrant on liquidate(), but be careful with external calls:
Flash Loan Attacks
Verify liquidations are genuine, not manipulated:
Sandwich Attack Protection
Protect your swaps from MEV:
Summary
Flux's liquidation callback system enables sophisticated capital-efficient strategies:
Capital-Free: Use seized collateral to pay for liquidation
Leveraged: Borrow from vault to amplify liquidation capacity
Recursive: Become a manager to liquidate another manager
Flash Loans: Use external flash loans for unlimited capital
Arbitrage: Route through multiple DEXs for maximum profit
Key Insight: Liquidators are temporary managers with full access to locked_* operations. This enables truly capital-free liquidations at any scale.
1. Vault transfers manager's positions to liquidator
2. Liquidator withdraws working capital
3. Liquidator withdraws bond
4. Liquidator deposits minimum payment back to working capital
5. Vault takes payment, liquidator keeps excess as profit
contract CapitalFreeLiquidator is IFluxUnlockCallback {
IFluxVault public immutable vault;
IAsset public immutable baseWrapper;
address public immutable owner;
function fluxUnlockCallback(bytes calldata data) external override {
require(msg.sender == address(vault), "Only vault");
// Decode liquidation params
(uint256 minPayment, uint256 trueDebt, ) =
abi.decode(data, (uint256, uint256, bytes));
// Step 1: Withdraw seized working capital
uint256 workingCapital = vault.getWorkingCapital(owner);
if (workingCapital > 0) {
vault.locked_withdrawFromWrapper(
baseWrapper,
WORKING_CAPITAL,
workingCapital
);
}
// Step 2: Withdraw seized bond
uint256 bondBalance = vault.getBondBalance(owner);
if (bondBalance > 0) {
vault.locked_withdrawBond(bondBalance);
}
// Now we have: workingCapital + bondBalance in base asset
// Step 3: Deposit minimum payment back to working capital
vault.locked_depositToWrapper(
baseWrapper,
WORKING_CAPITAL,
minPayment
);
// Vault will take minPayment from our working capital
// We keep (workingCapital + bondBalance - minPayment) as profit
}
}
Manager Position:
- Working Capital: $50K
- Bond: $20K
- Total Collateral: $70K
- Debt: $60K
Liquidation:
- Min Payment (underwater): $60K
- Liquidator receives: $70K total
- Liquidator pays: $60K
- Liquidator profit: $10K (seized collateral - debt)
No capital required!
1. Receive manager's positions
2. Borrow additional capital from vault (locked_borrow)
3. Use seized collateral + borrowed capital for liquidation
4. Repay borrowed capital (locked_repay)
5. Keep profit
contract LeveragedLiquidator is IFluxUnlockCallback {
IFluxVault public immutable vault;
IAsset public immutable baseWrapper;
IERC20 public immutable baseAsset;
address public immutable owner;
ISwapRouter public immutable swapRouter;
function fluxUnlockCallback(bytes calldata data) external override {
require(msg.sender == address(vault), "Only vault");
(uint256 minPayment, uint256 trueDebt, ) =
abi.decode(data, (uint256, uint256, bytes));
// Step 1: Withdraw all seized positions
uint256 seizedWorkingCapital = vault.getWorkingCapital(owner);
if (seizedWorkingCapital > 0) {
vault.locked_withdrawFromWrapper(
baseWrapper,
WORKING_CAPITAL,
seizedWorkingCapital
);
}
uint256 seizedBond = vault.getBondBalance(owner);
if (seizedBond > 0) {
vault.locked_withdrawBond(seizedBond);
}
// Step 2: Withdraw collateral from other wrappers
address[] memory wrappers = vault.registeredWrappers(owner);
uint256 totalUnwound = 0;
for (uint256 i = 0; i < wrappers.length; i++) {
if (wrappers[i] == address(baseWrapper)) continue;
// Get position value
uint256 value = IAsset(wrappers[i]).getValue(
address(vault),
owner,
WORKING_CAPITAL // or enumerate all positions
);
if (value > 0) {
// Withdraw from wrapper
vault.locked_withdrawFromWrapper(
IAsset(wrappers[i]),
WORKING_CAPITAL,
type(uint256).max
);
// Get underlying token
address token = IAsset(wrappers[i]).underlyingAsset();
uint256 balance = IERC20(token).balanceOf(address(this));
// Swap to base asset for better price
uint256 baseReceived = _swapToBase(token, balance);
totalUnwound += baseReceived;
}
}
// Current total: seizedWorkingCapital + seizedBond + totalUnwound
uint256 totalSeized = seizedWorkingCapital + seizedBond + totalUnwound;
// Step 3: Calculate if we need more capital
if (totalSeized < minPayment) {
// Borrow the deficit from vault!
uint256 needToBorrow = minPayment - totalSeized;
// Need to post bond first (10% of borrow)
uint256 bondRequired = needToBorrow / 10;
vault.locked_depositBond(bondRequired);
vault.locked_borrow(needToBorrow);
// Now we have enough to pay minPayment
vault.locked_depositToWrapper(
baseWrapper,
WORKING_CAPITAL,
minPayment
);
// Repay the borrow
vault.locked_repay(needToBorrow);
vault.locked_withdrawBond(bondRequired);
} else {
// We have enough, just deposit minPayment
vault.locked_depositToWrapper(
baseWrapper,
WORKING_CAPITAL,
minPayment
);
}
// Profit = totalSeized - minPayment (minus any swap slippage)
}
function _swapToBase(address token, uint256 amount) internal returns (uint256) {
IERC20(token).approve(address(swapRouter), amount);
return swapRouter.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: token,
tokenOut: address(baseAsset),
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: amount,
amountOutMinimum: 0, // Use proper slippage in production!
sqrtPriceLimitX96: 0
})
);
}
}
Manager Position:
- Working Capital: $30K WETH
- Bond: $10K USDC
- Wrapper: $25K WBTC
- Total Collateral: $65K
- Debt: $60K
Liquidation:
1. Seize positions ($65K total)
2. Swap WETH → USDC: $30K
3. Swap WBTC → USDC: $25K
4. Have $65K USDC total
5. Need to pay: $60K
6. Profit: $5K USDC
With Leverage (if collateral < debt):
1. Seize positions: $40K
2. Borrow from vault: $20K (need $4K bond)
3. Total available: $60K
4. Pay debt: $60K
5. Repay borrow: $20K
6. Withdraw bond: $4K
7. Net profit: $40K - $20K - $4K = $16K
Profitable even when collateral < debt!
1. Register as manager with vault
2. During YOUR unlock callback:
- Borrow capital from vault
- Call vault.liquidate(otherManager)
- During liquidation callback:
- Receive other manager's positions
- Liquidate them to base asset
- Deposit payment
- Repay your borrow
- Close your position
3. Keep profit
contract RecursiveLiquidator is IFluxUnlockCallback {
IFluxVault public immutable vault;
IERC20 public immutable baseAsset;
IAsset public immutable baseWrapper;
address public immutable owner;
ISwapRouter public immutable swapRouter;
// Track nested callback depth
uint256 private callbackDepth;
address private targetManager;
uint256 private borrowedAmount;
/**
* @notice Execute recursive liquidation
* @param manager The manager to liquidate
*/
function executeLiquidation(address manager) external {
require(msg.sender == owner, "Only owner");
// Store target for callback
targetManager = manager;
// Calculate how much to borrow
uint256 trueDebt = vault.getManagerTrueDebt(manager);
borrowedAmount = trueDebt * 110 / 100; // Borrow 110% of debt
// Trigger our own unlock to get vault capital
vault.unlock("");
}
function fluxUnlockCallback(bytes calldata data) external override {
require(msg.sender == address(vault), "Only vault");
callbackDepth++;
if (callbackDepth == 1) {
// FIRST CALLBACK: We're borrowing to liquidate
_firstCallback_BorrowAndLiquidate();
} else {
// SECOND CALLBACK: We're inside liquidation of target manager
_secondCallback_LiquidateTarget(data);
}
callbackDepth--;
}
function _firstCallback_BorrowAndLiquidate() internal {
// Step 1: Post bond
uint256 bondAmount = borrowedAmount / 5; // 20% bond
vault.locked_depositBond(bondAmount);
// Step 2: Borrow capital
vault.locked_borrow(borrowedAmount);
// Step 3: Withdraw borrowed capital
vault.locked_withdrawFromWrapper(
baseWrapper,
WORKING_CAPITAL,
borrowedAmount
);
// Step 4: Approve vault for liquidation payment
baseAsset.approve(address(vault), type(uint256).max);
// Step 5: Liquidate target manager
// This will trigger ANOTHER callback (depth 2)
vault.liquidate(targetManager, new IAsset[](0), "");
// Step 6: After liquidation completes, we have profit
// Deposit borrowed amount back to working capital
vault.locked_depositToWrapper(
baseWrapper,
WORKING_CAPITAL,
borrowedAmount
);
// Step 7: Repay borrow
vault.locked_repay(borrowedAmount);
// Step 8: Withdraw bond
vault.locked_withdrawBond(bondAmount);
// Step 9: Withdraw profit
uint256 profit = vault.getWorkingCapital(owner);
if (profit > 0) {
vault.locked_withdrawFromWrapper(
baseWrapper,
WORKING_CAPITAL,
profit
);
}
}
function _secondCallback_LiquidateTarget(bytes calldata data) internal {
// We're inside liquidation callback for target manager
(uint256 minPayment, uint256 trueDebt, ) =
abi.decode(data, (uint256, uint256, bytes));
// Step 1: Withdraw all seized collateral
uint256 seizedWorkingCapital = vault.getWorkingCapital(owner);
if (seizedWorkingCapital > 0) {
vault.locked_withdrawFromWrapper(
baseWrapper,
WORKING_CAPITAL,
seizedWorkingCapital
);
}
uint256 seizedBond = vault.getBondBalance(owner);
if (seizedBond > 0) {
vault.locked_withdrawBond(seizedBond);
}
// Step 2: Unwind any wrapper positions
uint256 totalUnwound = _unwindWrappers();
// Step 3: Calculate payment
uint256 totalSeized = seizedWorkingCapital + seizedBond + totalUnwound;
// We borrowed enough capital, so use it to pay
// Deposit minPayment to our working capital
vault.locked_depositToWrapper(
baseWrapper,
WORKING_CAPITAL,
minPayment
);
// Anything extra from seized collateral is our profit
// It stays in this contract and we withdraw it in first callback
}
function _unwindWrappers() internal returns (uint256 totalUnwound) {
address[] memory wrappers = vault.registeredWrappers(owner);
for (uint256 i = 0; i < wrappers.length; i++) {
if (wrappers[i] == address(baseWrapper)) continue;
bytes32[] memory positionIds = _getPositionIds(owner, wrappers[i]);
for (uint256 j = 0; j < positionIds.length; j++) {
uint256 value = IAsset(wrappers[i]).getValue(
address(vault),
owner,
positionIds[j]
);
if (value > 0) {
vault.locked_withdrawFromWrapper(
IAsset(wrappers[i]),
positionIds[j],
type(uint256).max
);
address token = IAsset(wrappers[i]).underlyingAsset();
uint256 balance = IERC20(token).balanceOf(address(this));
uint256 baseReceived = _swapToBase(token, balance);
totalUnwound += baseReceived;
}
}
}
}
function _swapToBase(address token, uint256 amount) internal returns (uint256) {
// Same as previous examples
IERC20(token).approve(address(swapRouter), amount);
return swapRouter.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: token,
tokenOut: address(baseAsset),
fee: 3000,
recipient: address(this),
deadline: block.timestamp,
amountIn: amount,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
})
);
}
function _getPositionIds(address manager, address wrapper) internal view returns (bytes32[] memory) {
// Implementation depends on wrapper tracking
// Could use subgraph, events, or on-chain enumeration
bytes32[] memory ids = new bytes32[](1);
ids[0] = WORKING_CAPITAL;
return ids;
}
}
function fluxUnlockCallback(bytes calldata data) external override {
require(msg.sender == address(vault), "Only vault");
try this._executeLiquidation(data) {
// Success
} catch Error(string memory reason) {
// Handle revert with reason
emit LiquidationFailed(reason);
// Ensure we still meet minimum payment
_depositMinimumPayment(data);
} catch (bytes memory) {
// Handle revert without reason
emit LiquidationFailed("Unknown error");
// Emergency: deposit any available capital
_depositAvailableCapital();
}
}
// Dangerous: External call before state update
function fluxUnlockCallback(bytes calldata data) external {
externalProtocol.doSomething(); // Could reenter!
vault.locked_depositToWrapper(...);
}
// Safe: Update state first
function fluxUnlockCallback(bytes calldata data) external {
vault.locked_depositToWrapper(...); // Update vault state
externalProtocol.doSomething(); // Then external call
}
// Check position was liquidatable BEFORE flash loan
function liquidateWithFlashLoan(address manager) external {
// Pre-check
(bool canLiquidate, , ) = strategy.evaluateLiquidation(vault, manager);
require(canLiquidate, "Not liquidatable");
// Then take flash loan
// ...
}