ReUSD Vault Arbitrary LP Withdraw via Unchecked Calldata

On 2026-02-04 13:46:59 UTC (block 24,383,881), tx 0xee2b216b7d649513dc8ba102e130d3d86d189b393a0d5f387e479be3dbda799d on Ethereum deployed helper contracts and invoked depositWithCalldataMultiToken and withdrawWithCalldataMultiToken on SingleAdapterRouter (Vault_reUSD) at 0x169a5effcae91ab33bc9e97f49b513b81008c453, draining vault-held LP value via attacker-controlled lpAmount and routerCalldata. The vulnerable contract is a non-proxy router with adapter() set to 0xe5b2fabf3b2000eb6b03bb4ebea80fabc6159cf0 at this block.

Attack vector: logic/authorization flaw in withdrawWithCalldataMultiToken(address,uint256,uint256,bytes) that accepts arbitrary lpAmount and routerCalldata without verifying the caller’s userShares or burning shares. The attacker contract 0xaf650b6dea7ffa0a54434d7527cba78c73889e22 first deposited amount=10,000,000 (10 USDC) and then withdrew with amountUnderlying=1 (0.000001 USDC) but lpAmount=12,838,251,626,546,795, a 2,585x multiple of its recorded shares. The call selectors in the trace are 0x0d6190ca (deposit) and 0x15e34014 (withdraw), and the withdraw calldata embeds the attacker contract address, indicating attacker-controlled routing.

Vulnerable code snippet (Vault_reUSD.sol):

    function withdrawWithCalldataMultiToken(
        address tokenOut,
        uint256 amountUnderlying,
        uint256 lpAmount,
        bytes calldata routerCalldata
    ) external whenNotPaused nonReentrant {
        if (amountUnderlying == 0) revert AmountZero();
        if (lpAmount == 0) revert AmountZero();
        if (tokenOut == address(0)) revert ZeroAddress();
        address a = address(adapter);
        if (a == address(0)) revert ZeroAddress();

        IERC20 tokenContract = IERC20(tokenOut);

        uint256 bal = userBalance[msg.sender];
        if (amountUnderlying > bal) revert ExceedsBalance();

        // 1) esegui zap-out con calldata pre-costruito (generic version)
        uint256 received = adapter.withdrawWithCalldataGeneric(tokenOut, lpAmount, routerCalldata);

        // sicurezza: vogliamo almeno amountUnderlying
        require(received >= amountUnderlying, "under-withdraw");

        // 2) aggiorna contabilità
        userBalance[msg.sender] = bal - amountUnderlying;
        totalDeposits -= amountUnderlying;

        // 3) claim hook
        claim.onWithdraw(msg.sender, amountUnderlying);

        // 4) invia tutto quello che è arrivato (in the requested token)
        tokenContract.safeTransfer(msg.sender, amountUnderlying);
    }

Flaw description: the function checks only userBalance (principal in underlying units) and never compares lpAmount against userShares nor burns shares. Because routerCalldata is fully attacker-controlled, the adapter can be instructed to redeem arbitrary LP amounts and route the output to attacker-controlled addresses. The require(received >= amountUnderlying) is ineffective when amountUnderlying is attacker-selected to be near zero, so a large LP redemption can pass while the vault’s accounting decreases by only 1 unit.

Call flow (per trace): EOA 0x53695b... -> create 0x1198e6... -> create/call 0xaf650b... -> SingleAdapterRouter.depositWithCalldataMultiToken -> adapter.depositWithCalldataGeneric -> SingleAdapterRouter.withdrawWithCalldataMultiToken -> adapter.withdrawWithCalldataGeneric -> 1inch router 0x888888... -> swaps/LP exits -> USDC transfers to attacker.

Financial impact: ERC20 Transfer topic 0xddf252ad... shows 25,774,896,133 USDC units (25,774.896133 USDC) transferred from 0xaf650b6d... to attacker EOA 0x53695bc3... (log index 0x112), with the corresponding inbound transfer from 0xf74c91b3... to 0xaf650b6d... (log index 0x10f). The loss is borne by the vault’s LP position and backing reserves (users/LPs), because the adapter redeemed LP value far in excess of the attacker’s recorded shares; protocol solvency is reduced by the extracted value.

Evidence: userBalance(0xaf650b6d...) is 0 before the tx and 9,999,999 after the tx while userShares(0xaf650b6d...) is 4,964,664,819,277, yet the withdraw call used lpAmount=12,838,251,626,546,795. Transfer logs show USDC moving from the vault to the adapter (0x169a5e... -> 0xe5b2f...), then through Pendle/1inch routes, and finally to the attacker EOA via the helper contract, consistent with a withdrawal that bypasses share accounting.