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

double-counting creates a state where claimed + unexchanged assets exceed the actual deployed balance

Summary

In StrategyOp.balanceDeployed, trategyMainnet.sol, and StrategyArb.sol

function balanceDeployed() public view returns (uint256) {
// @FOUND Asset accounting vulnerability where underlying.balanceOf() gets counted twice:
// 1. First through transmuter.getUnexchangedBalance() which includes deposited underlying
// 2. Second through direct underlying.balanceOf() call
// This breaks the core invariant: claimable + unexchanged <= balanceDeployed()
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}

The transmuter's getUnexchangedBalance() already includes the underlying assets deposited, the strategy then adds underlying.balanceOf() again, creating a double-counting scenario.

The vulnerability is because of incorrect assumptions about state transitions in the transmuter system:

  • When assets are deposited into the transmuter, they're counted in getUnexchangedBalance()

  • The strategy incorrectly assumes these assets should also be counted separately through underlying.balanceOf()

  • This creates an accounting overlap where the same assets are counted in multiple places

The relationship between transmuter balances and underlying token balances was misunderstood in the original implementation. claimable + unexchanged can exceed balanceDeployed() due to this double-counting, breaking the fundamental accounting assumptions of the system.

Vulnerability Details

In all three strategies (StrategyOp.sol, StrategyMainnet.sol, StrategyArb.sol), the core issue lies in

function balanceDeployed() public view returns (uint256) {
// @FOUND Critical accounting error: Double counting of underlying assets
// 1. transmuter.getUnexchangedBalance() includes deposited underlying tokens
// 2. underlying.balanceOf() counts these same tokens again
// This breaks the invariant: claimable + unexchanged <= balanceDeployed()
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}

The underlying issue arises from a lack of understanding of the Transmuter's accounting system.

interface ITransmuter {
// @FOUND getUnexchangedBalance already includes the underlying tokens
// that are deposited into the transmuter
function getUnexchangedBalance(address _owner) external view returns (uint256);
function getClaimableBalance(address _owner) external view returns (uint256);
}

When assets are deposited via transmuter.deposit(), they're counted in getUnexchangedBalance(). The strategy then counts these same assets again through underlying.balanceOf()

Impact

  1. Inflated Total Value Reporting

  2. Incorrect Share Price Calculations

Because of incorrect assumptions about state transitions in the transmuter system, where the relationship between transmuter balances and underlying token balances was fundamentally misunderstood in the implementation.

Recommendations

In StrategyOp.sol, StrategyMainnet.sol, and StrategyArb.sol

- function balanceDeployed() public view returns (uint256) {
- return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
- }
+ function balanceDeployed() public view returns (uint256) {
+ // @Mitigation - Remove double counting by only including actual unexchanged balance and loose assets
+ // Underlying tokens are already accounted for in getUnexchangedBalance()
+ return transmuter.getUnexchangedBalance(address(this)) + asset.balanceOf(address(this));
+ }
+ function getTotalUnderlying() public view returns (uint256) {
+ // @Mitigation - Add separate function to track underlying tokens for monitoring
+ // This does not affect accounting but provides visibility
+ return underlying.balanceOf(address(this));
+ }
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.