57740 sc high eulereth strategy will have weth locked in the strategy contract

Submitted on Oct 28th 2025 at 15:28:01 UTC by @oxrex for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #57740

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/strategies/mainnet/EulerWETHStrategy.sol

  • Impacts:

    • Permanent freezing of funds

Description

Brief/Intro

In the EulerWETHStrategy contract, we can allocate and deallocate assets (WETH). When we allocate, we take WETH from the VaultV2 and deposits it into the WETH Yield vault on Euler, when we deallocate, we withdraw the supplied WETH and send it back to the VaultV2. However, there is an issue whereby the WETH will be stuck in the Strategy contract and not go to the VaultV2.

Vulnerability Details

To explain the issue, I will be grabbing relevant code snippets from the VaultV2, EulerWETHStrategy, MYTStrategy, and AlchemistAllocator contracts:

FILE: VaultV2

function allocate(address adapter, bytes memory data, uint256 assets) external {
        require(isAllocator[msg.sender], ErrorsLib.Unauthorized());
        allocateInternal(adapter, data, assets);
    }

    function allocateInternal(address adapter, bytes memory data, uint256 assets) internal {
        require(isAdapter[adapter], ErrorsLib.NotAdapter());

        accrueInterest();

        SafeERC20Lib.safeTransfer(asset, adapter, assets);

        (bytes32[] memory ids, int256 change) = IAdapter(adapter).allocate(data, assets, msg.sig, msg.sender);

        for (uint256 i; i < ids.length; i++) {
            Caps storage _caps = caps[ids[i]];
            _caps.allocation = (int256(_caps.allocation) + change).toUint256();

            require(_caps.absoluteCap > 0, ErrorsLib.ZeroAbsoluteCap());

            require(_caps.allocation <= _caps.absoluteCap, ErrorsLib.AbsoluteCapExceeded());

            require(
                _caps.relativeCap == WAD || _caps.allocation <= firstTotalAssets.mulDivDown(_caps.relativeCap, WAD),
                ErrorsLib.RelativeCapExceeded()
            );
        }
        emit EventsLib.Allocate(msg.sender, adapter, assets, ids, change);
    }

    function deallocate(address adapter, bytes memory data, uint256 assets) external {
        require(isAllocator[msg.sender] || isSentinel[msg.sender], ErrorsLib.Unauthorized());
        deallocateInternal(adapter, data, assets);
    }

    function deallocateInternal(address adapter, bytes memory data, uint256 assets)
        internal
        returns (bytes32[] memory)
    {
        require(isAdapter[adapter], ErrorsLib.NotAdapter());

        (bytes32[] memory ids, int256 change) = IAdapter(adapter).deallocate(data, assets, msg.sig, msg.sender);

        for (uint256 i; i < ids.length; i++) {
            Caps storage _caps = caps[ids[i]]; // e.g 500
            require(_caps.allocation > 0, ErrorsLib.ZeroAllocation());
            _caps.allocation = (int256(_caps.allocation) + change).toUint256(); // 0
        }

        SafeERC20Lib.safeTransferFrom(asset, adapter, address(this), assets);
        emit EventsLib.Deallocate(msg.sender, adapter, assets, ids, change);
        return ids;
    }

Operators as well as the AlchemistAllocator admin contract allocate user deposited assets from the VaultV2 contract into third party contracts such as Euler Yield vaults. In this case, the strategies (EulerWETHStrategy) is the middle man contract. This contract is never meant to hold onto any asset, it only passes it around. However, during the period when the kill switch is on, the contract will wrongly hold assets it shouldn't have held in the first place. Now, even when the kill switch is then turned off (set to false), these assets it held earlier cannot be recovered by the VaultV2 contract.

Consider the scenario below:

  1. Bob deposits 200 WETH into the VaultV2 contract

  2. Operator then allocates 100 WETH to the EulerWETHStrategy contract. This strategy contract deposits that 100 WETH into Euler WETH vault and receives e.g 100 shares

  3. Then the kill switch gets set to true

  4. While trying to allocate new funds to EulerWETHStrategy, the EulerWETHStrategy returns early since we can't supply assets into Euler at this time (because switch is on)

  1. However, the VaultV2 contract has already sent 100 more WETH into the EulerWETHStrategy contract and the change returned is 0, which means we do not upscale the allocation in this case and the allocation of this strategy will remain 100 WETH whereas it has taken 200 WETH from the VaultV2:

This is problematic because now, we have a state whereby:

  • 100 WETH is in Euler WETH vault on Mainnet

  • 100 WETH is inside the EulerWETHStrategy (which it shouldn't be there in the first place and should have been sent back to the VaultV2 contract when the change is 0 since kill switch is true)

  • Now, regardless of the kill switch staying on (true) or us, setting it to off (false), these 100 WETH sitting inside the contract cannot be recovered.

  • When we deallocate, only the first 100 WETH we supplied to Euler Yield vault will be withdrawn and sent to the VaultV2. The other 100 WETH will be locked.

Impact Details

Assets being supplied to the 3rd party protocol, which in this case is the Euler WETH yield vault on Ethereum mainnet, will be stuck inside the EulerWETHStrategy strategy contract and cannot be recovered since we currently expose no recovery functions in the MYTStrategy or EulerWETHStrategy contracts.

Since the assets are locked in the strategy, I have decided to log this as critical but it can also be argued to be high severity.

When the kill switch is on and the change is 0 but the assets is non-zero, the EulerWETHStrategy::allocate() function should send back the assets it received.

For example, add that implementation in here before the return statement:

References

https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/MYTStrategy.sol?utm_source=immunefi#L102-L116

https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/strategies/mainnet/EulerWETHStrategy.sol?utm_source=immunefi

Proof of Concept

Proof of Concept

Paste the POC inside the AlchemistAllocator.t.sol test file and run with verbosity of 3 (vvv)

Was this helpful?