Liquidation process walk thru

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.

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's 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