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

Inaccurate Asset Accounting in balanceDeployed() Function

01. Relevant GitHub Links

02. Summary

The balanceDeployed() function in the strategy contracts calculates total deployed assets by summing the transmuter.getUnexchangedBalance(), the underlying token balance, and the asset token balance. However, it omits values that have already been converted (i.e., exchanged or claimable balances) and not reflected as unexchanged. As a result, the function underreports the total amount of deployed assets.

03. Vulnerability Details

The current implementation of balanceDeployed() returns:

function balanceDeployed() public view returns (uint256) {
return transmuter.getUnexchangedBalance(address(this)) + underlying.balanceOf(address(this)) + asset.balanceOf(address(this));
}

This calculation does not include assets that are no longer in the unexchanged state but may still be claimable or have been exchanged by the transmuter. Essentially, the function fails to fully account for all the assets that the strategy could realize and thus misrepresents the true value of the deployed funds.

04. Impact

When the strategy relies on balanceDeployed() for decision-making—such as determining when to harvest, withdraw, or report profits—the underestimation of deployed assets can lead to suboptimal operations. For example, the system may delay harvesting, incorrectly assess risk exposure, or misallocate resources because it does not have an accurate view of the total capital at its disposal.

05. Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;
import "forge-std/console.sol";
import {Setup, ERC20, IStrategyInterface} from "./utils/Setup.sol";
import {IStrategyInterfaceVelo} from "../interfaces/IStrategyInterface.sol";
import {IStrategyInterfaceRamses} from "../interfaces/IStrategyInterface.sol";
import {IVeloRouter} from "../interfaces/IVelo.sol";
import {IRamsesRouter} from "../interfaces/IRamses.sol";
interface Strategy is IStrategyInterface {
function balanceDeployed() external returns (uint256);
}
contract PocTest is Setup {
function setUp() public virtual override {
super.setUp();
}
function test_poc_balanceDeployed() public {
// deposit into strategy
uint256 test_amount = 10e18;
mintAndDepositIntoStrategy(strategy, user, test_amount);
console.log("Before Exchange Strategy(address(strategy)).balanceDeployed(): ", Strategy(address(strategy)).balanceDeployed());
console.log("Before Exchange Unexchanged Balance:", transmuter.getUnexchangedBalance(address(strategy)));
console.log("Before Exchange Exchangable Balance:", transmuter.getExchangedBalance(address(strategy)));
assertEq(Strategy(address(strategy)).balanceDeployed(), test_amount);
keeperExchange(test_amount);
console.log("After Exchange Strategy(address(strategy)).balanceDeployed(): ", Strategy(address(strategy)).balanceDeployed());
console.log("After Exchange Unexchanged Balance:", transmuter.getUnexchangedBalance(address(strategy)));
console.log("After Exchange Exchangable Balance:", transmuter.getExchangedBalance(address(strategy)));
assertLe(Strategy(address(strategy)).balanceDeployed(), test_amount);
}
function keeperExchange(uint256 _amount) public {
vm.roll(1);
deployMockYieldToken();
addMockYieldToken();
depositToAlchemist(_amount);
airdropToMockYield(_amount / 2);
vm.prank(whale);
asset.transfer(user2, _amount);
vm.prank(user2);
asset.approve(address(transmuter), _amount);
vm.prank(user2);
transmuter.deposit(_amount /2 , user2);
vm.roll(1);
harvestMockYield();
vm.prank(address(transmuterKeeper));
transmuterBuffer.exchange(address(underlying)); // @note : Keeper가 직접 exchange 하네
//Note : link to Transmuter tests https://github.com/alchemix-finance/v2-foundry/blob/reward-collector-fix/test/TransmuterV2.spec.ts
skip(7 days);
vm.roll(5);
vm.prank(user2);
transmuter.deposit(_amount /2 , user2);
vm.prank(address(transmuterKeeper));
transmuterBuffer.exchange(address(underlying));
}
}
Ran 1 test for src/test/testpoc.t.sol:PocTest
[PASS] test_poc_balanceDeployed() (gas: 3505736)
Logs:
Before Exchange Strategy(address(strategy)).balanceDeployed(): 10000000000000000000
Before Exchange Unexchanged Balance: 10000000000000000000
Before Exchange Exchangable Balance: 0
After Exchange Strategy(address(strategy)).balanceDeployed(): 9909899439067937440
After Exchange Unexchanged Balance: 9909899439067937440
After Exchange Exchangable Balance: 90100560932062560
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 19.31s (15.29s CPU time)
Ran 1 test suite in 20.27s (19.31s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

06. Tools Used

Manual Code Review and Foundry

07. Recommended Mitigation

Update balanceDeployed() to include all relevant balances from the transmuter, such as transmuter.getClaimableBalance(address(this)) or any other applicable converted/exchanged assets. Accurately reflecting the total accessible asset pool ensures the strategy can make more informed and effective decisions.

Updates

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Validated
Assigned finding tags:

balanceDeployed should include claimable

strapontin Auditor
10 months ago
strapontin Auditor
10 months ago
inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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