58400 sc low alchemist allocator does not actually enforce caps

Submitted on Nov 2nd 2025 at 00:00:35 UTC by @a16 for Audit Comp | Alchemix V3arrow-up-right

  • Report ID: #58400

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Contract fails to deliver promised returns, but doesn't lose value

Description

Brief/Intro

Both the allocate() and deallocate() functions perform cap calculations that are later just completely abandoned, possibly resulting in these functions not enforcing intended caps.

Vulnerability Details

The two functions:

// Overriden vault actions
function allocate(address adapter, uint256 amount) external {
    require(msg.sender == admin || operators[msg.sender], "PD");
    bytes32 id = IMYTStrategy(adapter).adapterId();
    uint256 absoluteCap = vault.absoluteCap(id);
    uint256 relativeCap = vault.relativeCap(id);
    // FIXME get this from the StrategyClassificationProxy for the respective risk class
    uint256 daoTarget = type(uint256).max;
    uint256 adjusted = absoluteCap > relativeCap ? absoluteCap : relativeCap;
    if (msg.sender != admin) {
        // caller is operator
        adjusted = adjusted > daoTarget ? adjusted : daoTarget;
    }
    // pass the old allocation to the adapter
    bytes memory oldAllocation = abi.encode(vault.allocation(id));
    vault.allocate(adapter, oldAllocation, amount);
}


function deallocate(address adapter, uint256 amount) external {
    require(msg.sender == admin || operators[msg.sender], "PD");
    bytes32 id = IMYTStrategy(adapter).adapterId();
    uint256 absoluteCap = vault.absoluteCap(id);
    uint256 relativeCap = vault.relativeCap(id);
    // FIXME get this from the StrategyClassificationProxy for the respective risk class
    uint256 daoTarget = type(uint256).max;
    uint256 adjusted = absoluteCap < relativeCap ? absoluteCap : relativeCap;
    if (msg.sender != admin) {
        // caller is operator
        adjusted = adjusted < daoTarget ? adjusted : daoTarget;
    }
    // pass the old allocation to the adapter
    bytes memory oldAllocation = abi.encode(vault.allocation(id));
    vault.deallocate(adapter, oldAllocation, amount);
}

calculate the adjusted variable based on the absoluteCap and relativeCap variables, but that variable is never actually used anywhere.

Furthermore, the daoTarget variable is being set to type(uint256).max, which is likely a value that was intended to be changed later given the following comment:

However, as things stand, that forces the adjusted calculation to be redundant and deterministic, as adjusted can never be greater than daoTarget, forcing this evaluation in allocate():

to always end up being type(uint256).max, and this evaluation in deallocate():

to always end up being the previous value of adjusted.

Impact Details

If the intention was for the AlchemistAllocator itself to revert if caps are not enforced, then that is not the case.

Proof of Concept

You can add the following function to AlchemistAllocator.t.sol:

function testAllocator_Forwards_OverCap_Allocation_InsteadOf_EnforcingCap() public { // Setup: deposit so the vault has assets and the adapter exists. // Caps set in setUp(): absolute = 200 ether, relative = 100% of TVL. _magicDepositToVault(address(vault), user1, 150 ether); require(vault.adaptersLength() == 1, "adaptersLength must be 1");

}

Was this helpful?