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

Incorrect total value calculations, affecting share price computations and potentially creating arbitrage opportunities in the protocol.

Summary

function balanceDeployed() public view returns (uint256) {
// @check Double counting vulnerability: claimableBalance is already included in unexchangedBalance
// This creates an accounting error since claimableBalance is a subset of unexchangedBalance
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}

The claimableBalance is actually a subset of unexchangedBalance, not a separate component. When the strategy calculates total assets, it's counting the claimable portion twice:

  1. Once through unexchangedBalance

  2. Again through underlying.balanceOf()

Vulnerability Details

The bug is in the balance accounting system across all three strategy implementations (StrategyArb.sol, StrategyMainnet.sol, and StrategyOp.sol) when interacting with the Alchemix Transmuter system it affect function balanceDeployed

// @check accounting error: The strategy's total balance calculation
// incorrectly assumes unexchangedBalance and claimableBalance are independent
// when claimableBalance is actually a subset of unexchangedBalance
function balanceDeployed() public view returns (uint256) {
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}

The root cause lies in the fundamental misunderstanding of the Transmuter's balance accounting system:

  • When users deposit alETH into the strategy

function _deployFunds(uint256 _amount) internal override {
transmuter.deposit(_amount, address(this));
}

The Transmuter tracks these deposits as unexchanged balance

interface ITransmuter {
function getUnexchangedBalance(address _owner) external view returns (uint256);
function getClaimableBalance(address _owner) external view returns (uint256);
}

When exchange conditions are met, a portion becomes claimable

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, ...) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
// ... swap logic
}

Impact across supported chains:

  • Ethereum Mainnet: Affects interactions with Curve pools

  • Optimism: Impacts Velodrome trading routes

  • Arbitrum: Affects Ramses exchange operations

The bug creates a systemic overstatement of total assets because:

  1. unexchangedBalance includes all deposited alETH

  2. When portions become claimable, they remain part of unexchangedBalance

  3. After claiming, the same value is counted again through underlying.balanceOf()

This affects:

  • Share price calculations

  • Deposit/withdrawal accounting

  • TVL reporting

  • Profit/loss calculations

Particularly severe because it operates across three major networks with different DEX integrations (Curve, Velodrome, Ramses) but shares the same fundamental accounting error in the base strategy logic.

Can be exploited through carefully timed deposits and withdrawals that take advantage of the double-counting during claim periods.

Impact

In the Transmuter Contract Design:

interface ITransmuter {
function getClaimableBalance(address _owner) external view returns (uint256);
function getUnexchangedBalance(address _owner) external view returns (uint256);
}

The Transmuter maintains two key balances:

  • UnexchangedBalance: Total amount of synthetic tokens (alETH) deposited

  • ClaimableBalance: Portion of unexchanged balance that can be claimed as underlying (WETH)

In the Strategy Implementation

function balanceDeployed() public view returns (uint256) {
// @check Architectural flaw: Double counting occurs here
// unexchangedBalance already includes claimableBalance, but underlying.balanceOf()
// will count claimed amounts again
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}

This creates a systematic overstatement of total assets that compounds with each claim operation, affecting all three network implementations (Ethereum, Optimism, Arbitrum) regardless of which DEX integration is used for swaps.

The bug spreads through the entire accounting system because this balanceDeployed() function is used as the source of truth for total strategy assets.

balanceDeployed()
balanceDeployed()
balanceDeployed()

ITransmuter

Recommendations

// In StrategyArb.sol, StrategyMainnet.sol, and StrategyOp.sol
- function balanceDeployed() public view returns (uint256) {
- return transmuter.getUnexchangedBalance(address(this)) +
- underlying.balanceOf(address(this)) +
- asset.balanceOf(address(this));
- }
+ // @Recommendation - Implement correct balance accounting that prevents double counting
+ function balanceDeployed() public view returns (uint256) {
+ uint256 unexchangedBalance = transmuter.getUnexchangedBalance(address(this));
+ uint256 looseAssets = asset.balanceOf(address(this));
+ uint256 pendingClaims = underlying.balanceOf(address(this));
+
+ // Only count unexchanged balance once and add any loose assets
+ return unexchangedBalance + looseAssets;
+ }
+ // @Recommendation - Add tracking for claimed amounts
+ mapping(address => uint256) public claimedAmounts;
+ // @Recommendation - Update claim tracking in claimAndSwap
+ function claimAndSwap(uint256 _amountClaim, uint256 _minOut, ...) external onlyKeepers {
+ // Track the claim before executing
+ claimedAmounts[address(this)] += _amountClaim;
+
+ transmuter.claim(_amountClaim, address(this));
+ // ... swap logic
+ }
Updates

Lead Judging Commences

inallhonesty Lead Judge
10 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.