#46587 [SC-Low] Overpayment loss in `transferToCoreVault` due to incorrect refund condition
Submitted on Jun 2nd 2025 at 04:58:52 UTC by @nnez for Audit Comp | Flare | FAssets
Report ID: #46587
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/library/CoreVault.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Vulnerability Details
In the transferToCoreVault function, callers must attach enough native token (FLR/SGB) via msg.value to cover a computed transfer fee (transferFeeWei). If they overpay, the contract is intended to refund any excess. The relevant code snippet is:
See: https://github.com/flare-foundation/fassets/blob/main/contracts/assetManager/library/CoreVault.sol#L81-L88
// 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) {
Transfers.transferNAT(state.nativeAddress, transferFeeWei);
Transfers.transferNATAllowFailure(payable(msg.sender), msg.value - transferFeeWei);
} else {
Transfers.transferNAT(state.nativeAddress, msg.value);
}TRANSFER_GAS_ALLOWANCEis a gas‐unit constant (100 000 gas) used to cap the gas limit when issuing the refund via a low‐levelcall. It has no direct relationship with wei or FLR/SGB denominations.In the
ifbranch (msg.value > transferFeeWei + TRANSFER_GAS_ALLOWANCE), the contract correctly sends exactlytransferFeeWeito the fee recipient and issues a refund ofmsg.value – transferFeeWei(with a limit gas). This covers any overpayment above the required fee.In the
elsebranch, whentransferFeeWei ≤ msg.value ≤ transferFeeWei + TRANSFER_GAS_ALLOWANCE), the contract treats the entiremsg.valueas the fee and does not refund the caller’s overpayment. Since gas costs are deducted separately from the sender’s wallet and do not reducemsg.value, this branch effectively causes any excess betweentransferFeeWeiandtransferFeeWei + TRANSFER_GAS_ALLOWANCEto be incorrectly sent to fee receiver.
Example Scenario
Let’s assume:
transferFeeWei = 1_000_000 wei(0.001 FLR)TRANSFER_GAS_ALLOWANCE = 100_000(gas)
Case – Small Overpayment
Caller sends
msg.value = 1_000_050 weiSince
1_000_050 < 1_000_000 + 100_000, the contract enters theelsebranchResult: Entire 1_000_050 wei is forwarded as fee; 50 wei overpayment is not refunded
Thus, only when msg.value falls in the interval:
[transferFeeWei, transferFeeWei + TRANSFER_GAS_ALLOWANCE]the contract incorrectly retains any overpayment (send to fee receiver) instead of refunding it.
Impact
The refund logic is incorrect, users overpaying by up to TRANSFER_GAS_ALLOWANCE wei will not be refunded.
Recommended Mitigations
- if (msg.value > transferFeeWei + Transfers.TRANSFER_GAS_ALLOWANCE) {
+ if (msg.value > transferFeeWei) {
Transfers.transferNAT(state.nativeAddress, transferFeeWei);
Transfers.transferNATAllowFailure(payable(msg.sender), msg.value - transferFeeWei);
} else {
Transfers.transferNAT(state.nativeAddress, msg.value);
}Proof of Concept
Proof-of-Concept
The following test demonstrates that if a fee is paid between [transferFeeWei, transferFeeWei + TRANSFER_GAS_ALLOWANCE], the refund is not triggered due to the misconception on gas consumption (TRANSFER_GAS_ALLOWANCE).
Steps
Put the following test in
test/integration/fasset-simulation/14-CoreVault.ts
it("nnez - incorrect transfer fee refund", async () => {
const agent = await Agent.createTest(context, agentOwner1, underlyingAgent1);
const minter = await Minter.createTest(context, minterAddress1, underlyingMinter1, context.underlyingAmount(1000000));
// make agent available
await agent.depositCollateralLotsAndMakeAvailable(100);
// minter2 also deposits to pool (so some fasset fees will go to them)
await agent.collateralPool.enter(0, false, { from: minterAddress2, value: toWei(3e8) });
// mint
const [minted] = await minter.performMinting(agent.vaultAddress, 10);
// update time
await context.updateUnderlyingBlock();
// agent requests transfer for all backing to core vault
const info = await agent.getAgentInfo();
const transferAmount = info.mintedUBA;
// calculate the transfer fee
const cbTransferFee = await context.assetManager.transferToCoreVaultFee(transferAmount);
// paid fee must be large enough, otherwise the request fails
await expectRevert(context.assetManager.transferToCoreVault(agent.vaultAddress, transferAmount, { from: agent.ownerWorkAddress, value: cbTransferFee.subn(1) }),
"transfer fee payment too small");
// Overpay within [transferFeeWei, transferFeeWei + 100_000]
const payTransferFee = toBN(90_000).add(cbTransferFee);
console.log("Acutal fee: ", cbTransferFee.toString());
console.log("We pay: ", payTransferFee.toString());
console.log("Diff: +", payTransferFee.sub(cbTransferFee).toString());
const res = await context.assetManager.transferToCoreVault(agent.vaultAddress, transferAmount, { from: agent.ownerWorkAddress, value: payTransferFee });
console.log("Invoke transferToCoreVault()");
const actuallyPaidFee = (await calculateReceivedNat(res)).neg();
console.log("We actually paid: +", actuallyPaidFee.toString());
// Assert that overpaid amount is not refunded
assertWeb3Equal(actuallyPaidFee, payTransferFee);
});Run
yarn hardhat test "test/integration/fasset-simulation/14-CoreVault.ts" --grep "nnez - incorrect transfer fee refund"Observe that the test passes (last assert passes)
Was this helpful?