#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?