The collateral that liquidators receive is valued below their initial expectations as a result of the price fall
| Title | The collateral that liquidators receive is valued below their initial expectations as a result of the price fall |
|---|---|
| Reward | $33790, Unique |
| Contest | FAssets - 19 August 2025 on Code4rena |
| Author | pashap9990 |
| Context | Wrong Tracking |
With no safeguard against a price drop, liquidators receive less collateral than expected from the vault and pool, and lose money.
Suppose:
- F-asset = FXRP
- 1 XRP = 1 USD
- Vault’s collateral is USDC
- 1 NAT = 1 USD
- Liquidation factor = 1.05
Based on the above assumptions, the liquidator is expected to receive 10,000 USDC and 500 NAT for one lot.
- An agent is established, after which the necessary collateral is deposited into the agent’s vault and collateral pool. The agent is then made publicly accessible.
- A minter reserves the required collateral and initiates the corresponding payment transaction.
- The function MintingFacet::executeMinting is invoked, resulting in the minter receiving fassets.
- The agent executes a decrease transaction on the underlying network without prior notification.
- An illegal payment is detected, and the challenger submits proof of the illegal payment. As a result, the agent’s status changes to FULL_LIQUIDATION.
- The liquidator submits the transaction. If the price of XRP falls to 0.90 USD by the time it executes, they receive 9,000 USDC and 450 NAT instead — less than expected.
function liquidate(
address _agentVault,
uint256 _amountUBA
)
external
notEmergencyPaused
nonReentrant
returns (uint256 _liquidatedAmountUBA, uint256 _amountPaidVault, uint256 _amountPaidPool)
{
Agent.State storage agent = Agent.get(_agentVault);
// calculate both CRs
Liquidation.CRData memory cr = Liquidation.getCollateralRatiosBIPS(agent);
// allow one-step liquidation (without calling startLiquidation first)
bool inLiquidation = _startLiquidation(agent, cr);
require(inLiquidation, NotInLiquidation());
// liquidate redemption tickets
(uint64 liquidatedAmountAMG, uint256 payoutC1Wei, uint256 payoutPoolWei) =
_performLiquidation(agent, cr, Conversion.convertUBAToAmg(_amountUBA));
_liquidatedAmountUBA = Conversion.convertAmgToUBA(liquidatedAmountAMG);
// pay the liquidator
if (payoutC1Wei > 0) {
_amountPaidVault = AgentPayout.payoutFromVault(agent, msg.sender, payoutC1Wei);
}
if (payoutPoolWei > 0) {
uint256 agentResponsibilityWei = _agentResponsibilityWei(agent, payoutPoolWei);
_amountPaidPool = AgentPayout.payoutFromPool(agent, msg.sender, payoutPoolWei, agentResponsibilityWei);
}
// if the agent was already safe due to price changes, there should be no LiquidationPerformed event
// we do not revert, because it still marks agent as healthy (so there will still be a LiquidationEnded event)
if (_liquidatedAmountUBA > 0) {
// burn liquidated fassets
Redemptions.burnFAssets(msg.sender, _liquidatedAmountUBA);
// notify about liquidation
emit IAssetManagerEvents.LiquidationPerformed(_agentVault, msg.sender,
_liquidatedAmountUBA, _amountPaidVault, _amountPaidPool);
}
// try to pull agent out of liquidation
Liquidation.endLiquidationIfHealthy(agent);
}
Alpha: check for slippage protection wherever a price oracle is used. Oracles update on an interval, and a single update can move the price enough to change the outcome.
Conclusion
This finding would earn you $33790. It requires understanding the liquidation process and the oracle-slippage attack vector.