# #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**](https://immunefi.com/audit-competition/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:

```
    describe("self-close exit fee retention", async () => {
        it("should retain msg.value when redeemToCollateral=true", async () => {
            // enter pool to get tokens.
            // We put 10 ETH into the pool so the caller gets some pool tokens
            await collateralPool.enter(0, true, { value: ETH(10) });
            // Read the pool contract’s raw ETH balance before the exit
            const before = toBN(await web3.eth.getBalance(collateralPool.address));

            const tokenBalance = await collateralPoolToken.balanceOf(accounts[0]);
            // Define a 1 ETH “executor fee” to pass via msg.value
            const fee = ETH(1);
            // call self-close exit with redeemToCollateral=true and pass fee
            await collateralPool.selfCloseExitTo(tokenBalance, true, accounts[0], "", accounts[0], { value: fee });
            // Read the pool contract’s raw ETH balance after the exit
            const after = toBN(await web3.eth.getBalance(collateralPool.address));
            // Assert that the pool’s balance increased by exactly fee (1 ETH), showing that the ETH was stuck in the contract
            assertEqualBN(after.sub(before), fee, "msg.value should remain in contract");
        });
    });
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://reports.immunefi.com/flare-fassets-or-mainnet-audit-comp/46520-sc-low-eth-loss-on-selfcloseexitto-when-redeeming-to-collateral.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
