The base strategy calculates the profit/loss of users and since AlETH will always be swapped at a premium price they should always record profit for their users but this is tracked wrongly and AlETH that are claimable are not added/swapped back to AlEth thus Claimable WETH are excluded are a wrong asset value is declared.
This exist int the Op, Mainnet and ARB strategies.
Wrong total asset will be returned.
* @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)
{
@audit>> checked not used>> uint256 claimable = transmuter.getClaimableBalance(address(this));
if (claimable > 0) {
}
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
uint256 underlyingBalance = underlying.balanceOf(address(this));
@audit>> wrong asset value>> _totalAssets = unexchanged + asset.balanceOf(address(this)) + underlyingBalance;
}
We report the AlETH in the transmitter But we fails to include the Claimable (WETH) in the transmitter hence the wrong total value will always be less than the actual token asset and the report will return a loss, impacting the asset to shares ratio and enabling new users to mint more shares than they should and also old depositors loses some asset.
The yearn Vault has a report function that the keeper calls to calculate and report profit/loss
* @notice Function for keepers to call to harvest and record all
* profits accrued.
*
* @dev This will account for any gains/losses since the last report
* and charge fees accordingly.
*
* Any profit over the fees charged will be immediately locked
* so there is no change in PricePerShare. Then slowly unlocked
* over the `maxProfitUnlockTime` each second based on the
* calculated `profitUnlockingRate`.
*
* In case of a loss it will first attempt to offset the loss
* with any remaining locked shares from the last report in
* order to reduce any negative impact to PPS.
*
* Will then recalculate the new time to unlock profits over and the
* rate based on a weighted average of any remaining time from the
* last report and the new amount of shares to be locked.
*
* @return profit The notional amount of gain if any since the last
* report in terms of `asset`.
* @return loss The notional amount of loss if any since the last
* report in terms of `asset`.
*/
function report()
external
nonReentrant
onlyKeepers
returns (uint256 profit, uint256 loss)
{
StrategyData storage S = _strategyStorage();
@audit>> wrong AlETH value returned>> uint256 newTotalAssets = IBaseStrategy(address(this))
.harvestAndReport();
@audit>> actual AlETH deposited>>> uint256 oldTotalAssets = _totalAssets(S);
uint256 sharesToBurn = _unlockedShares(S);
uint256 totalFees;
uint256 protocolFees;
uint256 sharesToLock;
uint256 _profitMaxUnlockTime = S.profitMaxUnlockTime;
@audit>>>>>
@audit>> wrong value triggers wrong calculations >>> if (newTotalAssets > oldTotalAssets) {
unchecked {
@audit>>>>> profit = newTotalAssets - oldTotalAssets;
}
sharesToLock = _convertToShares(S, profit, Math.Rounding.Down);
uint16 fee = S.performanceFee;
uint256 totalFeeShares;
if (fee != 0) {
unchecked {
@audit>>>>> totalFees = (profit * fee) / MAX_BPS;
@audit>>>>> totalFeeShares = (sharesToLock * fee) / MAX_BPS;
}
(
uint16 protocolFeeBps,
address protocolFeesRecipient
) = IFactory(FACTORY).protocol_fee_config();
uint256 protocolFeeShares;
if (protocolFeeBps != 0) {
unchecked {
@audit>>>>> protocolFeeShares =
(totalFeeShares * protocolFeeBps) /
MAX_BPS;
@audit>>>>> protocolFees = (totalFees * protocolFeeBps) / MAX_BPS;
}
@audit>>>>> _mint(S, protocolFeesRecipient, protocolFeeShares);
}
unchecked {
_mint(
S,
S.performanceFeeRecipient,
totalFeeShares - protocolFeeShares
);
}
}
if (_profitMaxUnlockTime != 0) {
unchecked {
sharesToLock -= totalFeeShares;
}
if (sharesToBurn > sharesToLock) {
unchecked {
_burn(S, address(this), sharesToBurn - sharesToLock);
}
} else if (sharesToLock > sharesToBurn) {
unchecked {
_mint(S, address(this), sharesToLock - sharesToBurn);
}
}
}
} else {
unchecked {
@audit>>>>> loss = oldTotalAssets - newTotalAssets;
}
if (loss != 0) {
@audit>>>>> sharesToBurn = Math.min(
S.balances[address(this)],
_convertToShares(S, loss, Math.Rounding.Down) + sharesToBurn
);
}
@audit>>>>> if (sharesToBurn != 0) {
_burn(S, address(this), sharesToBurn);
}
}
@audit>>>>> uint256 totalLockedShares = S.balances[address(this)];
if (totalLockedShares != 0) {
uint256 previouslyLockedTime;
uint96 _fullProfitUnlockDate = S.fullProfitUnlockDate;
if (_fullProfitUnlockDate > block.timestamp) {
unchecked {
previouslyLockedTime =
(_fullProfitUnlockDate - block.timestamp) *
(totalLockedShares - sharesToLock);
}
}
uint256 newProfitLockingPeriod = (previouslyLockedTime +
sharesToLock *
_profitMaxUnlockTime) / totalLockedShares;
S.profitUnlockingRate =
(totalLockedShares * MAX_BPS_EXTENDED) /
newProfitLockingPeriod;
S.fullProfitUnlockDate = uint96(
block.timestamp + newProfitLockingPeriod
);
} else {
S.fullProfitUnlockDate = 0;
}
@audit>>>>> S.totalAssets = newTotalAssets;
@audit>>>>> S.lastReport = uint96(block.timestamp);
emit Reported(
profit,
loss,
protocolFees,
totalFees - protocolFees
);
}
The Base strategy will always return a loss intent of the actual profit made by the strategy this will impact the asset to shares calculation and the old users who withdraws before this is corrected will loss some assets and new users also will be minted more shares since the total asset has been affected.
Include the claimable amount in the total AlETH calculation or better still swap and then report as done before commenting out the process.