58088 sc low inadequate enforcement of global cap enables cumulative over allocation
Submitted on Oct 30th 2025 at 15:14:39 UTC by @Arkindyo for Audit Comp | Alchemix V3
Report ID: #58088
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/alchemix-finance/v3-poc/blob/immunefi_audit/src/AlchemistStrategyClassifier.sol
Impacts:
Smart contract unable to operate due to lack of token funds
Description
Brief/Intro
The AlchemistStrategyClassifier defines globalCap as an MYT‑wide, per‑risk‑class maximum allocation ceiling intended to bound total exposure over time. However, PerpetualGauge enforces allocations against the current totalIdleAssets and a per‑execution accumulator totalRiskAllocated inside a single allocation loop. Because enforcement is scoped to the current execution context instead of the cumulative total allocated across all prior rounds, repeated executions can incrementally exceed the intended global cap, bypassing governance limits and increasing systemic risk.
Vulnerability Details
The system’s cap semantics and enforcement are misaligned: globalCap is documented as a global, cumulative ceiling per risk class across the entire MYT scope, yet PerpetualGauge only constrains the amount allocated within a single execution against totalIdleAssets. Over multiple executions, the per‑round constraint fails to account for already allocated assets, allowing total risk‑class allocation to drift above governance’s upper bound. In production, this can lead to concentration of funds beyond approved limits, magnifying the blast radius of downstream protocol failures or malicious upgrades.
The globalCap in AlchemistStrategyClassifier is described as “the aggregate maximum allocation for this risk class within MYT,” implying a cumulative, cross‑round, cross‑time constraint on outstanding allocations.
src/AlchemistStrategyClassifier.sol:RiskClass#L20
However, allocation is governed by values derived from totalIdleAssets (the current idle base) and a local per‑loop accumulator totalRiskAllocated for the running execution in PerpetualGauge. The check ensures the loop’s incremental allocations don’t exceed a bound computed from idle assets and per‑iteration totals, but it does not subtract previously allocated amounts nor compare against the risk class’s outstanding allocation across the system.
src/PerpetualGauge.sol:executeAllocation#L152-L161
Consequence: On the first execution, the loop may correctly keep the per‑round increment under globalCap. On subsequent executions, totalRiskAllocated is reset for the loop, and the base remains tied to totalIdleAssets. As idle replenishes (e.g., deposits, yield, deallocations to idle), the loop can allocate again up to the per‑round bound, cumulatively pushing the aggregate outstanding allocation for the risk class beyond globalCap.
Impact Details
Governance policy bypass: The system can hold more exposure than permitted for a risk class, nullifying governance‑mandated ceilings.
Amplified loss potential: If any strategy or downstream protocol within an over‑allocated risk class fails (rug, exploit, malicious upgrade), losses scale with the excess exposure above globalCap.
Up to the sum of all per‑round increments accumulated above globalCap. In the worst case, repeated executions can push exposure to the entire vault capacity for that risk class if idle continues to refill, making potential losses unbounded relative to the intended cap.
Recommendations
Enforce cumulative cap: Maintain a persistent counter riskClassOutstanding[RiskClass] representing total currently allocated assets per risk class across the system. Before allocating delta, enforce riskClassOutstanding[risk] + delta <= globalCap[risk].
References
Contract semantics: src/AlchemistStrategyClassifier.sol (global cap meaning and per‑risk configuration). src/AlchemistStrategyClassifier.sol:RiskClass#L20
Enforcement logic: src/PerpetualGauge.sol (allocation loop using totalIdleAssets and totalRiskAllocated per execution). src/PerpetualGauge.sol:executeAllocation#L152-L161
Proof of Concept
Proof of Concept
Creates a focused test src/test/PerpetualGaugeGlobalCapPoC.t.sol that proves globalCap is enforced per execution only, allowing cumulative over-allocation across rounds.
Sets globalCap for risk class 1 to 3000 bps and indivCap to 10000 bps.
Calls executeAllocation four times. Each run allocates 300,000 (30%), totaling 1,200,000 , exceeding the intended global cap if interpreted as cumulative outstanding exposure.
Was this helpful?