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

Balance validation occurs after state-changing operations allowing for balance manipulation between claim and final check

Summary

The balance accounting and validation sequence in the claimAndSwap function

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
// VULNERABILITY: Balance validation occurs after state-changing operations
// allowing for balance manipulation between claim and final check
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}

The function performs balance validation AFTER executing state-changing operations (claim and swap). The balanceDeployed() calculation includes both underlying and asset tokens, this creates a window where the actual balance can be manipulated between the claim and final check

because the balance check happens after claiming funds, the balanceDeployed() function sums both underlying and asset balances. This means the minOut check can pass even if the swap fails or provides unfavorable rates

This allows for claiming funds from transmuter, manipulating swap conditions, passing the minOut check despite unfavorable swap rates. Resulting in asset value loss while still passing the balance validation

Vulnerability Details

The issue lies in the minimum output enforcement mechanism. The strategies incorrectly validate the minimum output amount by checking the balance difference after both the claim and swap operations have occurred, rather than enforcing the minimum output specifically for the swap operation.

// In StrategyArb.sol:
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IRamsesRouter.route[] calldata _path) external onlyKeepers {
// VULNERABILITY: Balance check includes both claimed and swapped amounts
// This allows the minOut check to pass even with unfavorable swap rates
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
// In StrategyMainnet.sol:
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, uint256 _routeNumber) external onlyKeepers {
// VULNERABILITY: Same issue as above, but with Curve router implementation
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
require(_minOut > _amountClaim, "minOut too low");
router.exchange(...);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}
// In StrategyOp.sol:
function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
// VULNERABILITY: Same issue with Velodrome router implementation
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}

Keepers can execute trades at unfavorable rates while still passing the minOut check. The strategy could receive less alETH than intended from the WETH->alETH swap, this affects all supported networks (Optimism, Ethereum, Arbitrum) since all strategy variants have the same issue.

Impact

function claimAndSwap(uint256 _amountClaim, uint256 _minOut, IVeloRouter.route[] calldata _path) external onlyKeepers {
// FLAW: balanceDeployed() includes both underlying and asset tokens in its calculation
// This means the minOut check can be satisfied even with poor swap rates
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
}

The issue is in balanceDeployed()

function balanceDeployed() public view returns (uint256) {
// VULNERABILITY: Including both underlying and asset in total balance
// allows manipulation of the minOut check
return transmuter.getUnexchangedBalance(address(this)) +
underlying.balanceOf(address(this)) +
asset.balanceOf(address(this));
}

Recommendations

// StrategyArb.sol, StrategyMainnet.sol, StrategyOp.sol
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) external onlyKeepers {
+ // Validate claimable amount before any state changes
+ require(transmuter.getClaimableBalance(address(this)) >= _amountClaim, "Insufficient claimable");
+
+ // Store initial asset balance for accurate swap accounting
+ uint256 initialAssetBalance = asset.balanceOf(address(this));
+ uint256 initialUnderlyingBalance = underlying.balanceOf(address(this));
transmuter.claim(_amountClaim, address(this));
- uint256 balBefore = asset.balanceOf(address(this));
+
+ // Verify claimed amount
+ require(
+ underlying.balanceOf(address(this)) == initialUnderlyingBalance + _amountClaim,
+ "Claim amount mismatch"
+ );
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
- uint256 balAfter = asset.balanceOf(address(this));
+
+ // Verify swap output specifically
+ uint256 swapOutput = asset.balanceOf(address(this)) - initialAssetBalance;
+ require(swapOutput >= _minOut, "Insufficient swap output");
+
+ // Ensure all underlying was swapped
+ require(
+ underlying.balanceOf(address(this)) == initialUnderlyingBalance,
+ "Underlying remainder"
+ );
- require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
+// Add separate swap validation function
+function validateSwapRate(uint256 amountIn, uint256 minOut) internal pure {
+ // Ensure minimum profitable rate (e.g. 1.01 for 1% minimum profit)
+ require(minOut > amountIn * 101 / 100, "Unprofitable swap rate");
+}
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
0xbrett8571 Submitter
10 months ago
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.