#45554 [SC-Medium] Fee loss during Agent's feeBIPS reduction in `selfMint` function
Submitted on May 16th 2025 at 17:02:47 UTC by @holydevoti0n for Audit Comp | Flare | FAssets
Report ID: #45554
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/facets/MintingFacet.sol
Impacts:
Permanent freezing of unclaimed yield
Description
Brief/Intro
When an agent reduces their feeBIPS
between making an underlying payment and executing selfMint,
fees paid on the underlying chain are lost. This creates a gap where fees aren't properly captured in the protocol, resulting in a loss of funds for collateral pool participants.
Vulnerability Details
Unlike the executeMinting
function which stores the fee amount at the time of collateral reservation, the selfMint
function calculates fees at execution time based on the agent's current feeBIPS
value: https://github.com/flare-labs-ltd/fassets/blob/acb82a27b15c56ce9dfbb6dbbd76008da6753c26/contracts/assetManager/library/Minting.sol#L92
function selfMint(
IPayment.Proof calldata _payment,
address _agentVault,
uint64 _lots
)
internal
{
...
uint64 valueAMG = _lots * Globals.getSettings().lotSizeAMG;
uint256 mintValueUBA = Conversion.convertAmgToUBA(valueAMG);
@> uint256 poolFeeUBA = calculateCurrentPoolFeeUBA(agent, mintValueUBA);
...
if (_lots > 0) {
@> _performMinting(agent, MintingType.SELF_MINT, 0, msg.sender, valueAMG, receivedAmount, poolFeeUBA);
}
}
The calculateCurrentPoolFeeUBA
function uses the agent's current feeBIPS
to calculate the fee:
function calculateCurrentPoolFeeUBA(
Agent.State storage _agent,
uint256 _mintingValueUBA
)
internal view
returns (uint256)
{
@> uint256 mintingFeeUBA = _mintingValueUBA.mulBips(_agent.feeBIPS);
return _calculatePoolFeeUBA(mintingFeeUBA, _agent.poolFeeShareBIPS);
}
Example
An agent has feeBIPS set to 5% (500 basis points)
The agent makes a payment on the underlying chain that includes this 5% fee
The agent has his feeBIPS decreased to 0% (which requires announcement and execution, taking approximately 1 hour based on the current data from mainnet)
After the
feeBIPS
reduction is completed, the agent callsselfMint
When
selfMint
executes, it calculatespoolFeeUBA
based on the currentfeeBIPS
(0%), resulting in zero feesThe protocol mints 0 fee tokens to the collateral pool:
Globals.getFAsset().mint(address(_agent.collateralPool), _poolFeeUBA);
_agent.collateralPool.fAssetFeeDeposited(_poolFeeUBA);
The fees paid on the underlying chain are effectively lost, never making it into the protocol
Notice this vulnerability does not affect the executeMinting
function because it uses a different approach. In executeMinting
, the fee is calculated and stored at the time of collateral reservation:
// From Minting.sol - executeMinting function
function executeMinting(
IPayment.Proof calldata _payment,
uint64 _crtId
)
internal
{
CollateralReservation.Data storage crt = CollateralReservations.getCollateralReservation(_crtId);
...
// @audit crt stores the fee
_performMinting(agent, MintingType.PUBLIC, _crtId, crt.minter, crt.valueAMG,
uint256(_payment.data.responseBody.receivedAmount), calculatePoolFeeUBA(agent, crt));
}
The calculatePoolFeeUBA
function for collateral reservations uses the fee stored in the reservation:
function calculatePoolFeeUBA(
Agent.State storage _agent,
CollateralReservation.Data storage _crt
)
internal view
returns (uint256)
{
...
@> return _calculatePoolFeeUBA(_crt.underlyingFeeUBA, poolFeeShareBIPS);
}
Impact Details
Loss of funds for the collateral pool participants.
Proof of Concept
An agent has feeBIPS set to 5% (500 basis points)
The agent makes a payment on the underlying chain that includes this 5% fee
The agent has his
feeBIPS
decreased to 0% (which requires announcement and execution, taking approximately 1 hour based on the current data from mainnet)After the
feeBIPS
reduction is completed, the agent callsselfMint
When
selfMint
executes, it calculatespoolFeeUBA
based on the currentfeeBIPS
(0%), resulting in zero feesThe protocol mints 0 fee tokens to the collateral pool:
Globals.getFAsset().mint(address(_agent.collateralPool), _poolFeeUBA);
_agent.collateralPool.fAssetFeeDeposited(_poolFeeUBA);
The fees paid on the underlying chain are effectively lost, never making it into the protocol
Was this helpful?