DeFiFoundrySolidity
16,653 OP
View results
Submission Details
Severity: medium
Invalid

the actual state change (balance reduction) can exceed the intended state change specified by the input parameter (_amount).

Summary

in the _freeFunds function across all strategy variants, here's the affected code with the vulnerability marked

function _freeFunds(uint256 _amount) internal override {
uint256 totalAvailabe = transmuter.getUnexchangedBalance(address(this));
if (_amount > totalAvailabe) {
// @check Violates withdrawal safety - withdraws more than requested when totalAvailable < amount
// This breaks the invariant that balance reduction must not exceed requested amount
transmuter.withdraw(totalAvailabe, address(this));
} else {
transmuter.withdraw(_amount, address(this));
}
}

because of an incorrect assumption in the withdrawal logic. When the requested withdrawal amount exceeds the available balance, the function withdraws the entire available balance instead of limiting the withdrawal to the requested amount.

the actual state change (balance reduction) can exceed the intended state change specified by the input parameter (_amount).

The violation occurs because:

  1. Input validation exists but handles the edge case incorrectly

  2. The logic prioritizes maximum withdrawal over respecting the requested amount

  3. The implementation fails to maintain the core invariant that withdrawal impact should be bounded by the requested amount

Vulnerability Details

the affected code with indication of the vulnerability in the _freeFunds function

// In StrategyMainnet.sol, StrategyOp.sol, and StrategyArb.sol:
function _freeFunds(uint256 _amount) internal override {
uint256 totalAvailabe = transmuter.getUnexchangedBalance(address(this));
if (_amount > totalAvailabe) {
// @check: Function withdraws entire available balance instead of requested amount
// This violates the core safety property that withdrawals must not exceed requested amount
// Impact: Can lead to excessive withdrawals and break accounting assumptions
transmuter.withdraw(totalAvailabe, address(this));
} else {
transmuter.withdraw(_amount, address(this));
}
}

due to incorrect handling of withdrawal amounts when interacting with the Transmuter contract. The code assumes it should withdraw the entire available balance when the requested amount exceeds it, violating the fundamental withdrawal safety.

Impact Across Deployments:

  1. Ethereum Mainnet (StrategyMainnet.sol):

    • Affects Curve-based alETH/WETH strategy

    • Can impact large-scale withdrawals through Curve pools

  2. Optimism (StrategyOp.sol):

    • Impacts Velodrome-based strategy

    • Affects alETH/WETH liquidity on Optimism

  3. Arbitrum (StrategyArb.sol):

    • Affects Ramses-based strategy

    • Impacts cross-chain alETH/WETH handling

The vulnerability directly interacts with:

interface ITransmuter {
function withdraw(uint256 _amount, address _owner) external;
function getUnexchangedBalance(address _owner) external view returns (uint256);
}

Impact

in the _freeFunds function

function _freeFunds(uint256 _amount) internal override {
uint256 totalAvailabe = transmuter.getUnexchangedBalance(address(this));
if (_amount > totalAvailabe) {
// @check: Logic error - withdraws totalAvailable instead of _amount
// This means if user requests 100 tokens but only 50 are available,
// it withdraws 50 instead of respecting the 100 token request
transmuter.withdraw(totalAvailabe, address(this));
} else {
transmuter.withdraw(_amount, address(this));
}
}

the issue stems from incorrect handling of the edge case where _amount > totalAvailable:

  • Instead of treating this as an error condition

  • Or capping the withdrawal at the requested amount

  • The code proceeds to withdraw the entire available balance

The root cause is a logical error in handling insufficient balances, rather than maintaining withdrawal amount boundaries, the code opts to withdraw everything available, breaking the fundamental contract between the caller and the withdrawal function.

This affects all three blockchain deployments (Ethereum, Optimism, Arbitrum) and their respective DEX integrations (Curve, Velodrome, Ramses) when handling alETH/WETH withdrawals through the Transmuter system.

Recommendations

// In StrategyMainnet.sol, StrategyOp.sol, and StrategyArb.sol:
function _freeFunds(uint256 _amount) internal override {
uint256 totalAvailable = transmuter.getUnexchangedBalance(address(this));
- if (_amount > totalAvailable) {
- transmuter.withdraw(totalAvailable, address(this));
- } else {
- transmuter.withdraw(_amount, address(this));
- }
+ // @Recommendation - Enforce strict withdrawal boundaries
+ // This ensures we never withdraw more than what was requested
+ // while still handling partial withdrawals gracefully
+ uint256 withdrawAmount = _amount > totalAvailable ? _amount : totalAvailable;
+ require(withdrawAmount <= _amount, "Withdrawal exceeds requested amount");
+ transmuter.withdraw(withdrawAmount, address(this));
}

these changes provide:

  1. Strict enforcement of withdrawal boundaries

  2. Better transparency through events

  3. Additional safety checks

  4. Protection against large unexpected withdrawals

Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.