Summary
The vulnerability exists in the _harvestAndReport function, where the underlyingBalance is directly added to the total asset calculation. Due to the fact that the conversion ratio between alETH and WETH is typically not 1:1, this leads to inaccurate asset valuation, which impacts performance fee calculation, locking and unlocking mechanisms, and Price Per Share (PPS) calculations.
Vulnerability Details
The _harvestAndReport function is used to calculate the total assets held by the strategy when the keeper calls the report function.
StrategyMainnet:: _harvestAndReport:
* @dev Internal function to harvest all rewards, redeploy any idle
* funds and return an accurate accounting of all funds currently
* held by the Strategy.
*
* This should do any needed harvesting, rewards selling, accrual,
* redepositing etc. to get the most accurate view of current assets.
*
* NOTE: All applicable assets including loose assets should be
* accounted for in this function.
*
* Care should be taken when relying on oracles or swap values rather
* than actual amounts as all Strategy profit/loss accounting will
* be done based on this returned value.
*
* This can still be called post a shutdown, a strategist can check
* `TokenizedStrategy.isShutdown()` to decide if funds should be
* redeployed or simply realize any profits/losses.
*
* @return _totalAssets A trusted and accurate account for the total
* amount of 'asset' the strategy currently holds including idle funds.
*/
function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
{
uint256 claimable = transmuter.getClaimableBalance(address(this));
if (claimable > 0) {
}
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
@> uint256 underlyingBalance = underlying.balanceOf(address(this));
@> _totalAssets = unexchanged + asset.balanceOf(address(this)) + underlyingBalance;
}
StrategyArb:: _harvestAndReport:
* @dev Internal function to harvest all rewards, redeploy any idle
* funds and return an accurate accounting of all funds currently
* held by the Strategy.
*
* This should do any needed harvesting, rewards selling, accrual,
* redepositing etc. to get the most accurate view of current assets.
*
* NOTE: All applicable assets including loose assets should be
* accounted for in this function.
*
* Care should be taken when relying on oracles or swap values rather
* than actual amounts as all Strategy profit/loss accounting will
* be done based on this returned value.
*
* This can still be called post a shutdown, a strategist can check
* `TokenizedStrategy.isShutdown()` to decide if funds should be
* redeployed or simply realize any profits/losses.
*
* @return _totalAssets A trusted and accurate account for the total
* amount of 'asset' the strategy currently holds including idle funds.
*/
function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
{
uint256 claimable = transmuter.getClaimableBalance(address(this));
if (claimable > 0) {
}
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
@> uint256 underlyingBalance = underlying.balanceOf(address(this));
@> _totalAssets = unexchanged + asset.balanceOf(address(this)) + underlyingBalance;
}
StrategyOp:: _harvestAndReport:
* @dev Internal function to harvest all rewards, redeploy any idle
* funds and return an accurate accounting of all funds currently
* held by the Strategy.
*
* This should do any needed harvesting, rewards selling, accrual,
* redepositing etc. to get the most accurate view of current assets.
*
* NOTE: All applicable assets including loose assets should be
* accounted for in this function.
*
* Care should be taken when relying on oracles or swap values rather
* than actual amounts as all Strategy profit/loss accounting will
* be done based on this returned value.
*
* This can still be called post a shutdown, a strategist can check
* `TokenizedStrategy.isShutdown()` to decide if funds should be
* redeployed or simply realize any profits/losses.
*
* @return _totalAssets A trusted and accurate account for the total
* amount of 'asset' the strategy currently holds including idle funds.
*/
function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
{
uint256 claimable = transmuter.getClaimableBalance(address(this));
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
@> uint256 underlyingBalance = underlying.balanceOf(address(this));
@> _totalAssets = unexchanged + asset.balanceOf(address(this)) + underlyingBalance;
}
In calculating total assets, the function also considers WETH that has not yet been converted, but this portion is directly added by calculating the WETH balance without converting it to alETH. Since the alETH to WETH price ratio is typically not 1:1, this method leads to inaccurate asset valuation.
Impact
Inaccurate Total Asset Calculation
The misestimation of total assets directly affects the calculation of profits and losses, leading to incorrect performance fee and protocol fee distributions.
Overestimating or underestimating total assets may damage user trust in the protocol.
PPS (Price Per Share) Calculation Inaccuracy
Over- or underestimating PPS impacts all operations tied to asset value, such as redemption and reinvestment.
Long-term PPS deviations can accumulate risks and destabilize the strategy.
Locking and Unlocking Mechanism Failure
Inaccurate asset valuation compromises the fairness of share locking and unlocking, impacting yield distribution.
Tools Used
Manual
Recommendations
Ensure that when calculating total assets, the underlyingBalance is converted to its equivalent asset value (e.g., alETH) based on the current real-time price (which may require the use of an oracle). This ensures a more accurate valuation of total assets, rather than directly adding the WETH balance.