58019 sc high flawed killswitch implementation in mytstrategy leads to permanent loss of funds
Submitted on Oct 30th 2025 at 02:24:05 UTC by @fullstop for Audit Comp | Alchemix V3
Report ID: #58019
Report Type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/MYTStrategy.sol
Impacts:
Permanent freezing of funds
Description
Brief/Intro
A critical logic flaw exists in the killSwitch implementation within the MYTStrategy.sol contract. Instead of reverting transactions when activated, the allocate and deallocate functions silently succeed and return a change of 0. When the managing VaultV2 calls allocate, it first transfers assets to the strategy and then calls the strategy's allocate function for accounting. This flaw creates a fatal state mismatch: VaultV2 physically transfers the funds, but its internal accounting ledger (_caps.allocation) is never updated. These funds become permanently trapped in the MYTStrategy contract, irrecoverable by either the VaultV2 (which believes its allocation is zero) or the strategy's owner (who has no sweep function).
Vulnerability Details
The killSwitch is intended to be a safety mechanism to pause all strategy interactions. However, its implementation in allocate and deallocate achieves the opposite, creating a fund-destroying black hole.
The exploit flow is as follows:
An administrator, believing the protocol is in danger, calls
setKillSwitch(true)on a MYTStrategy contract.An allocator (e.g., AlchemistAllocator) calls
VaultV2.allocate(strategy_address, data, amount)to deposit funds.VaultV2's allocateInternal function executes.
The vault first physically transfers the assets:
SafeERC20Lib.safeTransfer(asset, adapter, assets);. At this moment, amount of the asset is now owned by the MYTStrategy contract.VaultV2 then calls the strategy for accounting:
IAdapter(adapter).allocate(data, assets, msg.sig, msg.sender);.This calls
MYTStrategy.allocate, which immediately checks the killSwitch:
The Core Flaw: The function does not revert. It returns
(ids(), 0), signaling a successful transaction with "zero change."Execution returns to
VaultV2.allocateInternal.VaultV2 receives the
change = 0value.VaultV2 updates its internal ledger:
_caps.allocation = (int256(_caps.allocation) + change).toUint256();.The vault's ledger is updated from
0 + 0, resulting in 0.The transaction successfully completes.
The system is now in an inconsistent state: MYTStrategy physically holds amount of the assets, but VaultV2's internal ledger (allocation) for that strategy is 0.
The same logic applies in reverse for deallocate, where funds sent from a sub-protocol would get stuck in MYTStrategy instead of being returned to the VaultV2.
Impact Details
The impact is the permanent and irrecoverable loss of all funds that are allocated or deallocated while the killSwitch is active.
The funds are permanently lost for two reasons:
VaultV2 Cannot Recover Funds: The vault is now "blind" to these funds. Any attempt by an allocator to call
vault.deallocate(...)to rescue the funds will fail. The deallocateInternal function checks the (incorrect) ledger before attempting withdrawal:require(_caps.allocation > 0, ErrorsLib.ZeroAllocation());. Since the ledger is 0, this check will always fail, and deallocate will revert, making recovery impossible.MYTStrategy Owner Cannot Recover Funds: The MYTStrategy contract itself acts as the prison for the funds. It inherits Ownable, but the owner has no functions to sweep or withdraw arbitrary ERC20 tokens that are held by the contract. The owner's only powers are to change parameters (e.g., setRiskClass, setKillSwitch).
Because the VaultV2's accounting is corrupted and the MYTStrategy has no emergency sweep function, any funds sent to it during the "emergency" are permanently locked.
References
Flawed allocate Function: MYTStrategy.sol
Proof of Concept
Proof of Concept
This Foundry test can be added to src/test/MYTStrategy.t.sol. The test fully reproduces the vulnerability: it configures the vault, activates the killSwitch, and then shows that an allocate call causes funds to be permanently trapped, failing a subsequent deallocate with ZeroAllocation.
Was this helpful?