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

Balance calculation includes underlying tokens which shouldn't be counted twice

Summary

the affected code with the root cause indication

function balanceDeployed() public view returns (uint256) {
// @check Double counting vulnerability: When assets are deposited into transmuter,
// they are counted both in getUnexchangedBalance() and asset.balanceOf(), leading to inflated total balance
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}

from an incorrect assumption about asset accounting after transmuter deposits. When deployFunds() executes transmuter.deposit(), the same assets are counted twice:

  1. First count: through transmuter.getUnexchangedBalance()

  2. Second count: through asset.balanceOf()

the code fails to account for how the deposit operation changes the asset's location while maintaining the same economic value. The assets don't exist in both places simultaneously, but the accounting function treats them as if they do.

Vulnerability Details

affected code path StrategyMainnet.sol#_deployFunds, balanceDeployed()

// In StrategyMainnet.sol, StrategyOp.sol, StrategyArb.sol
function _deployFunds(uint256 _amount) internal override {
// @check Funds are deposited into transmuter but balance accounting doesn't properly track the state transition
transmuter.deposit(_amount, address(this));
}
function balanceDeployed() public view returns (uint256) {
// @check Incorrect state tracking: Assets deposited in transmuter are counted twice
// 1. In transmuter.getUnexchangedBalance()
// 2. In asset.balanceOf()
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}

The bug s from incorrect state transition tracking in the deployment process. When alETH is deposited into the transmuter via _deployFunds(), the assets undergo a state change but the balanceDeployed() function fails to properly account for this transition.

Impact Across Chains:

  1. Ethereum (StrategyMainnet.sol)

  • Affects Curve-based swaps between WETH/alETH

  • Incorrect PPS (Price Per Share) calculations due to inflated TVL

  1. Optimism (StrategyOp.sol)

  • Impacts Velodrome-based trading pairs

  • Double counting affects keeper arbitrage opportunities

  1. Arbitrum (StrategyArb.sol)

  • Affects Ramses-based trading

  • Compromises premium calculations in claimAndSwap operations

The bug creates a systemic issue across all implementations where:

  1. Total Value Locked (TVL) is artificially inflated

  2. Share price calculations become inaccurate

  3. Arbitrage calculations based on balances become unreliable

This affects core Alchemix mechanics where these strategies handle WETH/alETH conversions and yield generation across all three supported networks.

Impact

State Transition Error #_deployFunds

// In all strategy implementations
function _deployFunds(uint256 _amount) internal override {
// @check When funds are deposited, they move from strategy to transmuter
// but balanceDeployed() counts them in both places
transmuter.deposit(_amount, address(this));
}

Double Counting in Balance Tracking #balanceDeployed

function balanceDeployed() public view returns (uint256) {
// @check Assets are counted twice:
// 1. transmuter.getUnexchangedBalance() includes deposited assets
// 2. asset.balanceOf() includes the same assets
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}

The core issue lies in the mishandling of asset state transitions:

  • When assets are deposited into the transmuter, they move from the strategy's direct control to the transmuter's control

  • However, balanceDeployed() counts these assets both in the transmuter's balance AND in the strategy's direct balance

  • This creates an artificial inflation of the total assets under management

The accounting error affects:

  • Share price calculations

  • TVL reporting

  • Arbitrage calculations for keepers

  • Overall strategy performance metrics

Recommendations

// In StrategyMainnet.sol, StrategyOp.sol, and StrategyArb.sol
function balanceDeployed() public view returns (uint256) {
- return transmuter.getUnexchangedBalance(address(this)) +
- underlying.balanceOf(address(this)) +
- asset.balanceOf(address(this));
+ // @Recommendation - Only count assets in their current location
+ // Unexchanged balance in transmuter + any unclaimed underlying + loose assets not yet deposited
+ return transmuter.getUnexchangedBalance(address(this)) +
+ underlying.balanceOf(address(this)) +
+ (asset.balanceOf(address(this)) - transmuter.getUnexchangedBalance(address(this)));
}
+ // @Recommendation - Add explicit balance verification after deployments
+ function verifyBalance(uint256 expectedBalance) public view returns (bool) {
+ uint256 actualBalance = transmuter.getUnexchangedBalance(address(this)) +
+ underlying.balanceOf(address(this)) +
+ (asset.balanceOf(address(this)) - transmuter.getUnexchangedBalance(address(this)));
+ return actualBalance == expectedBalance;
+ }

In this way we:

  1. Fix the double-counting issue by properly tracking asset locations

  2. Add verification mechanisms to ensure balance consistency

  3. Improve transparency through event emissions

  4. Implement safety checks in critical functions

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.