49835 sc insight dex aggregator unused eth loss

Submitted on Jul 19th 2025 at 21:56:53 UTC by @Blobism for Attackathon | Plume Network

  • Report ID: #49835

  • Report Type: Smart Contract

  • Report severity: Insight

  • Target: https://github.com/immunefi-team/attackathon-plume-network-nucleus-boring-vault/blob/main/src/helper/DexAggregatorWrapperWithPredicateProxy.sol

  • Impacts:

    • Permanent freezing of funds

Description

Brief / Intro

If a user calls either of the Dex Aggregator deposit methods with a value of ETH larger than the amount they request to wrap, the remaining ETH gets left in the contract, resulting in the funds being locked/stolen.

Vulnerability Details

Calling depositOneInch or depositOkxUniversal with the nativeValueToWrap parameter will result in only that amount of sent ETH getting wrapped to WETH, and any remaining ETH gets left in the contract.

The wrapping of the WETH is shown below. Notice an amount can be wrapped that is smaller than msg.value:

function _checkAndMintNativeAmount(uint256 nativeAmount) internal returns (bool useNative) {
    if (nativeAmount > msg.value) {
        revert DexAggregatorWrapper__InsufficientEthForSwap();
    }
    if (nativeAmount > 0) {
        // Direct WETH call, no SafeTransferLib needed here
        canonicalWrapToken.deposit{ value: nativeAmount }();
        useNative = true;
    }
    // Implicitly returns false if nativeAmount is 0
}

After the ETH is wrapped and sent, the remaining ETH is NOT refunded in depositOneInch and depositOkxUniversal, so the remaining ETH gets left in the contract. See the 1inch version:

function depositOneInch(
    // ...
    uint256 nativeValueToWrap,
    PredicateMessage calldata predicateMessage
)
    external
    payable
    nonReentrant
    returns (uint256 shares)
{
    // ...

    // Use safeTransfer to send shares to msg.sender
    ERC20(vaultAddress).safeTransfer(msg.sender, shares);

    // <----------- BUG: no excess ETH refund

    _calcSharesAndEmitEvent(
        supportedAsset,
        CrossChainTellerBase(address(teller)),
        address(desc.srcToken),
        desc.amount,
        supportedAssetAmount
    );
}

Compare this to depositAndBridgeOneInch and depositAndBridgeOkxUniversal, where remaining ETH is refunded at the end of the methods. See the 1inch version:

function depositAndBridgeOneInch(
    // ...
)
    external
    payable
    nonReentrant
{
    // ...

    // Refund any excess ETH
    _refundExcessEth(payable(msg.sender));

    _calcSharesAndEmitEvent(supportedAsset, teller, address(desc.srcToken), desc.amount, supportedAssetAmount);
}

Impact Details

Because nativeValueToWrap is a parameter of the deposit methods, it is reasonable for a user to pass a larger amount of ETH than what they are wrapping. It is a critical vulnerability that the remaining ETH is not returned to the user.

The "deposit and bridge" methods handle this correctly in principle (disregarding the distinct refund vulnerability) because they refund remaining ETH at the end of the methods.

References

See src/helper/DexAggregatorWrapperWithPredicateProxy.sol in the target repository: https://github.com/immunefi-team/attackathon-plume-network-nucleus-boring-vault/blob/main/src/helper/DexAggregatorWrapperWithPredicateProxy.sol

Proof of Concept

The following path leads to the vulnerability:

1

User calls depositOneInch or depositOkxUniversal with a nonzero nativeValueToWrap which is smaller than msg.value (native ETH sent).

2

Some of the native ETH is not wrapped and remains in the contract.

3

The remaining ETH is now locked in the contract, or can potentially be stolen by an attacker using a separate exploit to drain the unused ETH.

Was this helpful?