#55025 [SC-Insight] corevault refund failure can permanently freeze overpaid nat on assetmanager

  • Report ID: #55025

  • Report Type: Smart Contract

  • Report severity: Insight

  • Target: https://github.com/flare-foundation/fassets/commit/7aa02b62285cd5313032103710c2e083b166bf60

  • Impacts: Permanent freezing of funds

Description

When an agent transfers underlying to the Core Vault via transferToCoreVault and overpays the required fee, the contract attempts to refund the difference using a 100k gas native transfer. If that refund fails (for example, the caller is a contract that reverts in receive() or requires more than 100k gas), the return value is ignored and the overpaid NAT remains stuck on the Asset Manager (diamond) contract with no recovery path.

Vulnerability Details

Relevant snippet from CoreVault:

 // pay the transfer fee and return overpaid transfer fee when the difference is larger than gas use
        // (all transfers are guarded by nonReentrant in the facet)
        if (msg.value > transferFeeWei + Transfers.TRANSFER_GAS_ALLOWANCE * tx.gasprice) {
            Transfers.transferNAT(state.nativeAddress, transferFeeWei);
            Transfers.transferNATAllowFailure(payable(msg.sender), msg.value - transferFeeWei);
        } else {
            Transfers.transferNAT(state.nativeAddress, msg.value);
        }

Transfers.transferNATAllowFailure(address payable, uint256) returns bool to indicate success/failure of the 100k-gas NAT transfer, but the result is ignored here. If the recipient cannot accept the transfer (reverts in receive() or needs more than the fixed TRANSFER_GAS_ALLOWANCE = 100_000 gas), the function returns false. Because the return is ignored, the overpaid amount remains held by the asset manager contract and is never forwarded or burned.

For context: the threshold transferFeeWei + Transfers.TRANSFER_GAS_ALLOWANCE * tx.gasprice is correct and ensures wei-to-wei comparison. The bug is independent of that threshold — it’s about ignoring the refund failure.

A similar pattern is handled correctly in CollateralReservations.sol:

Preconditions for the issue:

  • Caller is the authorized agent vault owner but is a contract that reverts in receive or requires >100k gas to accept ETH.

  • Caller overpays msg.value enough to enter the refund branch.

Impact

Overpaid NAT remains in the Asset Manager’s balance indefinitely, with no generic sweep or recovery function exposed to return it.

Update CoreVault.transferToCoreVault to handle failed refunds the same way as CollateralReservations — check the return value of Transfers.transferNATAllowFailure(...) and, on failure, execute an alternative such as burning the fee or forwarding it to a recovery/burn address so funds are not permanently stuck.

(Reference to the function: https://github.com/flare-foundation/fassets/blob/59373cee12e6d2a9fa0a9cc8735bb486faa51b36/contracts/assetManager/library/CoreVault.sol#L45)

Proof of Concept

The PoC demonstrates the issue by calling transferToCoreVault from a contract whose receive() reverts, ensuring the 100k gas refund fails and the overpaid NAT stays in the AssetManager.

PoC helper contract:

Integration test (create file test/integration/fasset-simulation/15-CoreVault-RefundPoC.ts):

Run the test with:

yarn hardhat test test/integration/fasset-simulation/15-CoreVault-RefundPoC.ts

Logs from the PoC run:

Steps to reproduce (high-level)

1

Prepare environment

Deploy the AssetManager/fassets stack and ensure Core Vault is enabled (so transferToCoreVault is allowed).

2

Create agent and mint

  • Create an agent and make it available.

  • Perform minting to create backing for the agent so there is underlying to transfer.

3

Deploy RefundRejector and set as agent work address

  • Deploy the RefundRejector helper contract (its receive() reverts).

  • Set RefundRejector as the agent's work address so that it is recognized as the agent owner.

4

Trigger transfer with overpayment

  • Call transferToCoreVault from RefundRejector, forwarding a msg.value that overpays the transfer fee enough to trigger the refund branch.

  • The contract will attempt the 100k-gas refund to RefundRejector, which will fail due to its reverting receive().

5

Observe balances

  • AssetManager balance increases by the overpaid amount.

  • RefundRejector does not receive the refund — funds remain stuck on AssetManager.

Was this helpful?