<!-- canonical: https://0xsimao.com/findings/superfluid-erc4626-yield-withdraw-fees -->

# ERC4626YieldBackend.withdrawSurplus() uses convertToAssets() which doesn't account for fees

Low/Info · Sherlock · Money streaming · 13th January, 2026

Finding L-4 of the Superfluid Yield Backends security review.

- Protocol: https://superfluid.org/
- Report: /reports/superfluid
- Codebase: https://github.com/0xsimao/protocol-monorepo/tree/d69e4b0394c38d2760b9b54f173a797b630c9ed1
- Source: https://github.com/superfluid-org/protocol-monorepo/blob/dev/packages/ethereum-contracts/audits/2026-01-27%20-%20Final%20-%20Superfluid%20Collaborative%20Audit%20Report%201769517931.pdf

---

## Summary

`ERC4626YieldBackend.withdrawSurplus()` uses `convertToAssets()` to calculate the surplus, which does not account for withdrawal fees, potentially causing the function to revert or withdraw more than the actual surplus.

## Vulnerability Detail

The `withdrawSurplus()` function calculates the vault's asset value using `convertToAssets()`:

```solidity
uint256 vaultAssets = VAULT.convertToAssets(
    VAULT.balanceOf(address(this))
);
```

Per EIP-4626, `convertToAssets()` returns the amount of assets that would be exchanged for the given shares, but it does not account for withdrawal fees. For vaults that charge withdrawal fees, the actual redeemable amount is less than what `convertToAssets()` reports.

This causes the calculated `surplusAmount` to be higher than the actual withdrawable surplus. When `VAULT.withdraw(surplusAmount, ...)` is called, it may:
1. Revert if the vault cannot fulfill the inflated withdrawal amount
2. Withdraw assets that belong to SuperToken holders (not actual surplus)

## Impact

For ERC4626 vaults with withdrawal fees, `withdrawSurplus()` either reverts (DoSing surplus withdrawal) or incorrectly withdraws funds that belong to SuperToken holders, causing a loss for users.

## Code Snippet

https://github.com/sherlock-audit/2026-01-superfluid-update-jan-13th/blob/main/protocol-monorepo/packages/ethereum-contracts/contracts/superfluid/ERC4626YieldBackend.sol#L45-L55

```solidity
function withdrawSurplus(uint256 totalSupply) external {
    (uint256 normalizedTotalSupply, ) = ISuperToken(address(this))
        .toUnderlyingAmount(totalSupply);

    uint256 vaultAssets = VAULT.convertToAssets(
        VAULT.balanceOf(address(this))
    );

    uint256 surplusAmount = vaultAssets + ASSET_TOKEN.balanceOf(address(this)) - normalizedTotalSupply;
    VAULT.withdraw(surplusAmount, SURPLUS_RECEIVER, address(this));
}
```

## Tool Used

Manual Review

## Recommendation

Use `previewRedeem()` instead of `convertToAssets()` to get an accurate estimate that includes fees, or use `maxWithdraw()` to get the maximum withdrawable amount:

```solidity
function withdrawSurplus(uint256 totalSupply) external {
    (uint256 normalizedTotalSupply, ) = ISuperToken(address(this))
        .toUnderlyingAmount(totalSupply);

    // Use maxWithdraw to account for fees and other constraints
    uint256 vaultAssets = VAULT.maxWithdraw(address(this));

    uint256 surplusAmount = vaultAssets + ASSET_TOKEN.balanceOf(address(this)) - normalizedTotalSupply;
    VAULT.withdraw(surplusAmount, SURPLUS_RECEIVER, address(this));
}
```

---

Related findings:

- [`ViewLogic::maxLiquidatable()` doesn't take the bonus into account, making the agent liquidatable again](https://0xsimao.com/findings/cap-view-liquidatable-bonus-agent): Cap
- [RebalanceLogic::rebalanceCallback() doesn't update lastTotalAssets when rebalancing from morpho to morpho](https://0xsimao.com/findings/yieldoor-ii-rebalance-callback-rebalancing-morpho): Yieldoor LoopedVault
- [Users cannot unstake from YiedlETHStakingEtherfi.sol, because YieldAccount.sol is incompatible with ether.fi's WithdrawRequestNFT.sol](https://0xsimao.com/findings/benddao-unstake-eth-staking-yield): BendDAO
- [Liquidating maturies with unassigned earnings will not take into account floating assets increase leading to loss of funds](https://0xsimao.com/findings/exactly-protocol-update-staking-contract-ii-liquidating-maturies-unassigned-floating): Exactly Protocol Update - Staking Contract
