#46984 [SC-Low] Incomplete Token Supply Check After Token Share Recalculation in `_selfCloseExitTo`

Submitted on Jun 7th 2025 at 08:43:37 UTC by @light279 for Audit Comp | Flare | FAssets

  • Report ID: #46984

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Smart contract unable to operate due to lack of token funds

Description

Brief/Intro

In the CollateralPool::_selfCloseExitTo function, after the recalculation of _tokenShare due to insufficient agent redemption capacity, no validation is performed to ensure the updated _tokenShare satisfies the minimum token supply constraint (MIN_TOKEN_SUPPLY_AFTER_EXIT). This breaks an important invariant enforced earlier in the function and could result in an invalid or inconsistent state post-exit.

Vulnerability Details

The CollateralPool::_selfCloseExitTo function initially validates the following condition:

function _selfCloseExitTo(
        uint256 _tokenShare,
        bool _redeemToCollateral,
        address payable _recipient,
        string memory _redeemerUnderlyingAddress,
        address payable _executor
    )
        private
    {
        require(_tokenShare > 0, "token share is zero");
        require(_tokenShare <= token.balanceOf(msg.sender), "token balance too low");
        AssetData memory assetData = _getAssetData();
@>        require(assetData.poolTokenSupply == _tokenShare ||
            assetData.poolTokenSupply - _tokenShare >= MIN_TOKEN_SUPPLY_AFTER_EXIT,
            "token supply left after exit is too low and non-zero");
        uint256 natShare = assetData.poolNatBalance.mulDiv(
            _tokenShare, assetData.poolTokenSupply); // poolTokenSupply >= _tokenShare > 0
        require(natShare > 0, "amount of sent tokens is too small");
        require(assetData.poolNatBalance == natShare ||
            assetData.poolNatBalance - natShare >= MIN_NAT_BALANCE_AFTER_EXIT,
            "collateral left after exit is too low and non-zero");
        uint256 maxAgentRedemption = assetManager.maxRedemptionFromAgent(agentVault);
        uint256 requiredFAssets = _getFAssetRequiredToNotSpoilCR(assetData, natShare);
        // rare case: if agent has too many low-valued open tickets they can't redeem the requiredFAssets
        // in one transaction. In that case we lower/correct the amount of spent tokens and nat share.
        ...........

This check ensures that either:

  • The user is exiting the entire pool (draining all tokens), or

  • The remaining pool token supply after exit stays above a configured minimum.

However, if the (maxAgentRedemption < requiredFAssets), the function recalculates natShare and _tokenShare:

While the function re-checks the natShare constraint above:

It fails to re-validate _tokenShare against the MIN_TOKEN_SUPPLY_AFTER_EXIT constraint. As a result, it is possible for:

to occur silently, breaking pool invariants and possibly leading to unintended or unsafe behavior in downstream logic that assumes this minimum is preserved.

Impact Details

  • Users could unintentionally drain the pool below the minimum allowed token supply.

  • Breaks protocol-level assumptions that rely on MIN_TOKEN_SUPPLY_AFTER_EXIT being respected.

Add any relevant links to documentation or code:

After _tokenShare is recalculated in the if (maxAgentRedemption < requiredFAssets) block in function CollateralPool::_selfCloseExitTo, reapply the token supply check:

Proof of Concept

Proof of Concept

1: User initiates selfCloseExit

  • User calls selfCloseExit(_tokenShare, ...) with some initial _tokenShare.

Checks performed:

  • _tokenShare > 0

  • _tokenShare <= user balance

  • poolTokenSupply - _tokenShare >= MIN_TOKEN_SUPPLY_AFTER_EXIT ✅

  • natShare = poolNatBalance * _tokenShare / poolTokenSupply

  • poolNatBalance - natShare >= MIN_NAT_BALANCE_AFTER_EXIT ✅ All checks pass.

2: Required F-assets are too high

  • requiredFAssets = _getFAssetRequiredToNotSpoilCR(...)

  • But:

This triggers the fallback path to adjust.

3: Adjust natShare and recheck only collateral constraint

  • requiredFAssets is capped to maxAgentRedemption

  • A new natShare is computed via _getNatRequiredToNotSpoilCR(...)

  • Now, natShare is lower than the original

  • Check re-applied:

4: _tokenShare is recalculated — but no re-validation!

  • _tokenShare is now recomputed:

  • No check is done again to verify:

Therefore, the new _tokenShare could reduce the pool’s token supply below the minimum required threshold, violating the pool’s safety invariants.

Was this helpful?