#46520 [SC-Low] ETH loss on `selfCloseExitTo` when redeeming to collateral

Submitted on Jun 1st 2025 at 01:19:34 UTC by @Rhaydden for Audit Comp | Flare | FAssets

  • Report ID: #46520

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Permanent freezing of funds

Description

Brief/Intro

In CollateralPool.selfCloseExitTo, if a user passes msg.value (intended as an executor fee) but sets _redeemToCollateral = true, the contract never forwards or refunds that ETH. It remains locked in the pool contract resulting in permanent user fund loss.

Vulnerability Details

Inside _selfCloseExitTo, the code handles f-asset redemption like this:

// … after calculating `requiredFAssets` …
if (requiredFAssets > 0) {
    if (requiredFAssets < assetManager.lotSize() || _redeemToCollateral) {
        // Non-payable call: msg.value is ignored
        assetManager.redeemFromAgentInCollateral(
            agentVault, _recipient, requiredFAssets
        );
    } else {
        // Cross-chain redemption: msg.value is forwarded as executor fee
        assetManager.redeemFromAgent{ value: msg.value }(
            agentVault, _recipient, requiredFAssets, _redeemerUnderlyingAddress, _executor
        );
    }
}

When the first branch is taken—either because the redemption amount is too small or _redeemToCollateral==true—the contract calls the non-payable redeemFromAgentInCollateral and never touches msg.value. That ETH is neither wrapped into wNat nor returned to the sender. Since there is no fallback path for raw ETH (the receive() guard only allows internal withdrawals), any ETH sent is irretrievably stuck.

Impact Details

Any ETH sent as a fee under these conditions is locked in the pool contract. Users cannot retrieve it, nor can the protocol recover it via any existing withdrawal mechanism. Even accidental small-value calls lead to direct loss. This falls squarely under Permanent freezing of funds in the impacts in scope.

Fix

In any branch that accepts msg.value, either

immediately call wNat.deposit{value: msg.value}() (via _depositWNat()) so it’s tracked as collateral, OR revert if _redeemToCollateral or requiredFAssets < lotSize but msg.value > 0

References

https://github.com/flare-labs-ltd/fassets//blob/fc727ee70a6d36a3d8dec81892d76d01bb22e7f1/contracts/assetManager/implementation/CollateralPool.sol#L363-L372

Proof of Concept

Proof of Concept

Add this test to CollateralPool.ts:

Was this helpful?