#45769 [SC-Medium] Permanent blocking of Agent's fund by allowed minters

Submitted on May 20th 2025 at 09:10:47 UTC by @pseudoArtist for Audit Comp | Flare | FAssets

  • Report ID: #45769

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/library/CollateralReservations.sol

  • Impacts:

    • Temporary freezing of funds

Description

Brief/Intro

Collateral reservation logic has a flaw which will lead to a malicious allowedMinter to act maliciously and block all the free collateral of an Agent for further use of genuine minters thus permanently breaking the minting process.

This is not OOS because it has already been confirmed from the team screenshot attached below for reference.

Vulnerability Details

When minting the minters first need to reserve the collateral by calling CollateralReservation::reserveCollateral() which further calls the reserveCollateral in the CollateralReservations library.

The function reserveCollateral() does some basic checks, It also checks if the lots can be processed by checking the free collateral of the agent:

 require(collateralData.freeCollateralLots(agent) >= _lots, "not enough free collateral");

and then calculates the lot size by:

 uint64 valueAMG = _lots * Globals.getSettings().lotSizeAMG;

After calculating the amount of collateral required to be locked, it then moves on to reserve the free collaterals:

  _reserveCollateral(agent, valueAMG + _currentPoolFeeAMG(agent, valueAMG));

The above reservation process looks something like this :

  function _reserveCollateral(
        Agent.State storage _agent,
        uint64 _reservationAMG//@note this includes the lots*lotSizeAMG + poolFeeUBA
    )
        private
    {
        AssetManagerState.State storage state = AssetManagerState.get();
        Minting.checkMintingCap(_reservationAMG);//@note this checks if the total minting cap is exceeded in the total protocol and not for the agent
        _agent.reservedAMG += _reservationAMG;//@note this updates the reserved amount for the agent
        state.totalReservedCollateralAMG += _reservationAMG;//@audit-info the free collateral is updated in the next call to reserveCollateral.
    }

Now if we look at the above process it increases the reservedAMG by the _reservationAMG and which means it updates the amount of reervedAMG for the agent and based on this the freeCollaterals are also updated in the next call to the reserveCollateral().

Now let's take a look at how the fee are charged for reservations, If a minter wants to reserve the collateral he needs to pay a fee so when the minter fails to deliver the underlying assets he loses the fee which disincentivises him to act maliciously and block the free collateral. However there is a catch here, the fee are not charged for the alwaysAllowedMinters:

// - only charge reservation fee for public minting, not for alwaysAllowedMinters on non-public agent

 // - poolCollateral is WNat, so we can use its price for calculation of CR fee
        uint256 reservationFee = agent.availableAgentsPos != 0
            ? _reservationFee(collateralData.poolCollateral.amgToTokenWeiPrice, valueAMG)
            : 0;

The reservationFee will be 0 for the alwaysAllowedMinters which gives them the power to act maliciously and when they call the function reserveCollateral() combined with the fact they aren't being charged any fee for it , they can block the whole collateral of an agent indefinitely without losing anything. An agent only have the option to call rejectCollateralReservation which will reject and free up the collateral but the malicious allowed minter can do this again until he is removed from the alwaysAllowedMinters list.

Impact Details

Malicious minter can grief genuine minters for certain no of blocks and also block the capital of the agent for free.

References

// - poolCollateral is WNat, so we can use its price for calculation of CR fee
        uint256 reservationFee = agent.availableAgentsPos != 0
            ? _reservationFee(collateralData.poolCollateral.amgToTokenWeiPrice, valueAMG)
            : 0;

Proof of Concept

Proof of Concept

Step 1: alwaysAllowedMinters calls the reserveCollateral() and reserves the freeCollateral to its max capacity without any fee.

Step 2: Malicious Minter doesn't deliver the underlying tokens.

Step 3: An agent after waiting for certain time rejects the collateral reservations.

Step 4: Malicious minter repeats the same process again and keep dossing and griefing genuine minters for certain time unless he is removed from the always allowed minters list.

Was this helpful?