Liquidation process walk through
This page provides a more detailed explanation of Fathom's FXD liquidation process
For a position to be considered safe, the below should hold true.
where
RawPrice is the price of collateral from PriceFeed
and LTV is Loan to Ratio.
If a position's value of lockedCollateral(lockedCollateralAmount * PriceWithSafetyMargin)
goes below the debtValue(DebtShare * debtAccumulatedRate)
, the position is considered unsafe/underwater and will be subject to liquidation process. The liquidator bot monitors the health of each position in the Fathom protocol.
Once a position is underwater and spotted by the liquidator bot, the bot can call the liquidate
function in the LiquidationEngine
contract.
function liquidate(
bytes32 _collateralPoolId, // the collateralPoolId of the position that is to be liquidated
address _positionAddress, // the positionHandler's address of a position that is to be liquidated
uint256 _debtShareToBeLiquidated, // a param to set how much of debtShare will be liquidated from a position
uint256 _maxDebtShareToBeLiquidated, // the ceiling for the debtShare that can be liquidated
address _collateralRecipient, // the recipient of the collateral once the liquidation process ends
bytes calldata _data // is used when the liquidation process includes FlashLendingCallee
) external;
Or the bot can call the batchLiquidate
function if it spots multiple underwater positions.
function batchLiquidate(
bytes32[] calldata _collateralPoolIds,
address[] calldata _positionAddresses,
uint256[] calldata _debtShareToBeLiquidateds,
uint256[] calldata _maxDebtShareToBeLiquidateds,
address[] calldata _collateralRecipients,
bytes[] calldata _datas
) external;
If a bot calls liquidate
or batchLiquidate
functions to LiquidationEngine
without using the _data
param, the liquidator bot needs to have enough balance of FXD token available and should have approved FixedSpreadLiquidationStrategy
as a spender. This is because the process of depositing FXD to liquidate and receive collateral is done in the execute function in FixedSpreadLiquidationStrategy
.
If a bot calls liquidate
or batchLiquidate
functions and has the _data
param filled with encoded data that can be used for flashLendingCalle's address, the liquidator bot doesn't necessarily need to have an FXD token available.
Once liquidation functions are called, the liquidation process starts from the internal function _liquidate.
function _liquidate(
bytes32 _collateralPoolId,
address _positionAddress,
uint256 _debtShareToBeLiquidated,
uint256 _maxDebtShareToBeLiquidated,
address _collateralRecipient,
bytes calldata _data,
address sender
) internal;
_liquidate
function first goes through some checks in the function args and checks whether the position targeted for liquidation is safe.
require(
_collateralPoolLocalVars.priceWithSafetyMargin > 0 &&
_vars.positionLockedCollateral * _collateralPoolLocalVars.priceWithSafetyMargin <
_vars.positionDebtShare * _collateralPoolLocalVars.debtAccumulatedRate,
"LiquidationEngine/position-is-safe"
);
Once the position is underwater, the liquidation task is tossed to FixedSpeadLiquidationStrategy
execute function.
_strategy.execute(
_collateralPoolId,
_vars.positionDebtShare,
_vars.positionLockedCollateral,
_positionAddress,
_debtShareToBeLiquidated,
_maxDebtShareToBeLiquidated,
sender,
_collateralRecipient,
_data
);
The execute
function in FixedSpeadLiquidationStrategy
starts by verifying if the caller has the LIQUIDATION_ENGINE_ROLE
from the AccessControlConfig
associated with the BookKeeper
. This ensures that only authorized entities can initiate the liquidation process.
require(
IAccessControlConfig(bookKeeper.accessControlConfig()).hasRole(
IAccessControlConfig(bookKeeper.accessControlConfig()).LIQUIDATION_ENGINE_ROLE(),
msg.sender
),
"!liquidationEngingRole"
);
The function then validates the input parameters, such as the debt share, collateral amount, and position address. It checks for zero values and valid price feeds to ensure the integrity of the liquidation data.
_validateValues(_collateralPoolId, _positionDebtShare, _positionCollateralAmount, _positionAddress);
Then, it calculates the liquidation information based on the current market price, debt share, and collateral amount. This step involves determining the actual debt value to be liquidated, the collateral amount that will be seized, and any applicable treasury fees.
LiquidationInfo memory info = _calculateLiquidationInfo(
_collateralPoolId,
_debtShareToBeLiquidated,
getFeedPrice(_collateralPoolId),
_positionCollateralAmount,
_positionDebtShare
);
The function then calls confiscatePosition
on the BookKeeper
, adjusting the collateral and debt share of the position in question.
bookKeeper.confiscatePosition(
_collateralPoolId,
_positionAddress,
address(this),
address(systemDebtEngine),
-int256(info.collateralAmountToBeLiquidated),
-int256(info.actualDebtShareToBeLiquidated)
);
Treasury fees are moved from the liquidation contract to the SystemDebtEngine
if any treasury fees are due.
if (info.treasuryFees > 0) {
bookKeeper.moveCollateral(_collateralPoolId, address(this), address(systemDebtEngine), info.treasuryFees);
}
If flash lending is enabled and valid, the contract facilitates flash lending calls. This step is optional and based on the encoded data provided.
if (info.treasuryFees > 0) {
bookKeeper.moveCollateral(_collateralPoolId, address(this), address(systemDebtEngine), info.treasuryFees);
}
Without flash lending, the contract withdraws the collateral to the liquidator and transfers the required FXD amount to cover the debt. The FXD is then deposited in the BookKeeper
to clear the debt.
IGenericTokenAdapter(ICollateralPoolConfig(bookKeeper.collateralPoolConfig()).getAdapter(_collateralPoolId)).withdraw(
_collateralRecipient,
info.collateralAmountToBeLiquidated - info.treasuryFees,
abi.encode(0)
);
address _stablecoin = address(stablecoinAdapter.stablecoin());
_stablecoin.safeTransferFrom(_liquidatorAddress, address(this), ((info.actualDebtValueToBeLiquidated / RAY) + 1));
_stablecoin.safeApprove(address(stablecoinAdapter), ((info.actualDebtValueToBeLiquidated / RAY) + 1));
stablecoinAdapter.depositRAD(_liquidatorAddress, info.actualDebtValueToBeLiquidated, _collateralPoolId, abi.encode(0));
Finally, an event LogFixedSpreadLiquidate
is emitted to record the details of the liquidation transaction.
emit LogFixedSpreadLiquidate(
_collateralPoolId,
info.positionDebtShare,
info.positionCollateralAmount,
_positionAddress,
info.debtShareToBeLiquidated,
info.maxDebtShareToBeLiquidated,
_liquidatorAddress,
_collateralRecipient,
info.actualDebtShareToBeLiquidated,
info.actualDebtValueToBeLiquidated,
info.collateralAmountToBeLiquidated,
info.treasuryFees
);
After the execute
function of the FixedSpreadLiquidationStrategy
completes, the remaining liquidation process in the LiquidationEngine
is verification of successful liquidation and recording of systemBadDebt
if needed.
First, updated position data is retrieved:
(_vars.newPositionLockedCollateral, _vars.newPositionDebtShare) = bookKeeper.positions(_collateralPoolId, _positionAddress);
Then, the decrease of position debt Share is validated, ensuring effective liquidation:
require(_vars.newPositionDebtShare < _vars.positionDebtShare, "LiquidationEngine/debt-not-liquidated");
Once the liquidation validation is good with debtShare
, receipt of the expected stablecoin amount by the SystemDebtEngine
is checked:
_vars.wantStablecoinValueFromLiquidation = (_vars.positionDebtShare - _vars.newPositionDebtShare) * _collateralPoolLocalVars.debtAccumulatedRate; // [rad]
require(
bookKeeper.stablecoin(address(systemDebtEngine)) - _vars.systemDebtEngineStablecoinBefore >= _vars.wantStablecoinValueFromLiquidation,
"LiquidationEngine/payment-not-received"
);
In case the collateral is fully liquidated with remaining debt, the function records this as systemBadDebt
:
if (_vars.newPositionLockedCollateral == 0 && _vars.newPositionDebtShare > 0) {
require(_vars.newPositionDebtShare < 2 ** 255, "LiquidationEngine/overflow");
bookKeeper.confiscatePosition(
_collateralPoolId,
_positionAddress,
_positionAddress,
address(systemDebtEngine),
0,
-int256(_vars.newPositionDebtShare)
);
}
Last updated
Was this helpful?