58077 sc low reward tokens are incorrectly claimed to strategy contract during deallocation leads to permanent token loss

Submitted on Oct 30th 2025 at 13:09:18 UTC by @dobrevaleri for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58077

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Permanent freezing of unclaimed royalties

Description

Brief/Intro

The TokeAutoEthStrategy::_deallocate() function incorrectly claims reward tokens to the strategy contract instead of the intended MYT vault during withdrawal operations, causing permanent loss of TOKE rewards and any additional reward tokens distributed by the rewarder.

Vulnerability Details

In the TokeAutoEthStrategy::_deallocate() function, when withdrawing staked shares from the Tokemak rewarder, the contract calls rewarder.withdraw(address(this), sharesNeeded, true) on line 85:

// Withdraws auto eth shares from the rewarder with any claims
// redeems same amount of shares from auto eth vault to weth  
function _deallocate(uint256 amount) internal override returns (uint256) {
    uint256 sharesNeeded = autoEth.convertToShares(amount);
    uint256 actualSharesHeld = rewarder.balanceOf(address(this));
    uint256 shareDiff = actualSharesHeld - sharesNeeded;
    if (shareDiff <= 1e18) {
        sharesNeeded = actualSharesHeld;
    }
    // withdraw shares, claim any rewards
@>  rewarder.withdraw(address(this), sharesNeeded, true);
    // ... rest of function
}

The third parameter true in the withdraw call instructs the rewarder to claim accrued rewards. According to the IMainRewarder interface, this function signature is:

When claim is set to true, the rewarder sends all accrued TOKE rewards and any extra reward tokens to the account parameter, which in this case is address(this) (the strategy contract).

However, the protocol's intended behavior is demonstrated in the _claimRewards() function:

This function correctly uses getReward(address(this), address(MYT), false) to claim rewards directly to the MYT vault (address(MYT)), not to the strategy contract.

The root cause is that rewarder.withdraw(address(this), sharesNeeded, true) claims rewards to address(this) (the strategy), while the intended recipient should be the MYT vault. The strategy contract has no mechanism to recover these trapped reward tokens.

Impact Details

All accrued TOKE rewards are sent to the strategy contract during every _deallocate() call. The strategy has no recovery mechanism for these trapped tokens

The Tokemak rewarder system supports additional reward tokens beyond TOKE through "extra rewarders". When claim=true, ALL reward types are claimed to the strategy contract. The _claimRewards() function only handles TOKE tokens via getReward(..., false), so extra rewards are never recovered. Extra reward tokens become permanently trapped in the strategy contract

References

The Tokemak rewarder implementation can be found at: https://github.com/Tokemak/v2-core-pub/blob/main/src/rewarders/MainRewarder.sol

This is the function that is called when withdrawing: https://github.com/Tokemak/v2-core-pub/blob/de163d5a1edf99281d7d000783b4dc8ade03591e/src/rewarders/MainRewarder.sol#L84-L102

This is the function that processes the rewards: https://github.com/Tokemak/v2-core-pub/blob/de163d5a1edf99281d7d000783b4dc8ade03591e/src/rewarders/MainRewarder.sol#L141-L151

Proof of Concept

Proof of Concept

Was this helpful?