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

Protocol incorrectly assumes that WETH and ALETH are 1:1 pegged

Summary

alETH and WETH are two different tokens and although alETH aims to be, its not always pegged to WETH at a 1 to 1 ratio. The protocol in certain places however, incorrectly assumes that the two tokens are pegged at a 1 to 1 ratio leading to potential issues downstream when used.

Vulnerability Details

From the displayed price on etherscan, we can see that WETH price differs from alETH price. As at time of writing, WETH's price is 3279,69 and alETH's price is 3223,84. This is important to take into context as in certain functions, the protocol operations involving both tokens are conducted with an implication of them being handled at a 1 to 1 ratio.

In a less impactful function, balanceDeployed, sums up the alETH unexchanged balance from the transmuter, the contracts alETH balance and the contracts WETH balance. This is bad practice as WETH != alETH, even in price. Anyone/contract that depends on the function will be affected.

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

In a function with potentially bigger impact, _harvestAndReport is an internal function which appears to be in use in the report function in TokenizedStrategy.sol which is OOS.

function _harvestAndReport()
internal
override
returns (uint256 _totalAssets)
{
uint256 claimable = transmuter.getClaimableBalance(address(this));
if (claimable > 0) {
// transmuter.claim(claimable, address(this));
}
// NOTE : we can do this in harvest or can do seperately in tend
// if (underlying.balanceOf(address(this)) > 0) {
// _swapUnderlyingToAsset(underlying.balanceOf(address(this)));
// }
uint256 unexchanged = transmuter.getUnexchangedBalance(address(this));
// NOTE : possible some dormant WETH that isn't swapped yet (although we can restrict to only claim & swap in one tx)
uint256 underlyingBalance = underlying.balanceOf(address(this));
@> _totalAssets = unexchanged + asset.balanceOf(address(this)) + underlyingBalance;
}

The same calculation is made and returned as _totalAssets which is used in the report function. Currently the token prices are of a small discrepancy, at a ratio of about 1 to 0,98, but this could change and in a more serious case, like a flash crash/rise, depeg event in either direction, the returned _totalAssets will not be indicative of actual value of assets being held in the strategy which can lead to various issues.

The same issues can be found in functions of the same signature in StrategyArb.sol and StrategyOp.sol.

https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyArb.sol#L123

https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyArb.sol#L170

https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyOp.sol#L136

https://github.com/Cyfrin/2024-12-alchemix/blob/82798f4891e41959eef866bd1d4cb44fc1e26439/src/StrategyOp.sol#L173

Impact

The returned _totalAssets is set as S.totalAssets in the strategy, which is then used the _totalAssets function. _totalAssets appears to be in use extensively in the contract and used to determine how much a user can earn upon deposit and withdrawal. In case of a serious price descripancy or a depeg event, malicious users can take advantage of this to extract value from the protocol.

Tools Used

Manual Review

Recommendations

Recommend using an oracle price to make amount calculations via the token price. Strategy mainnet does have the useOracle parameter which can also be integrated into StrategyArb.sol and StrategyOp.sol and used to calculate how much the amount of WETH in the contract is worth in terms of alETH.

Updates

Appeal created

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

balanceDeployed() and _harvestAndReport() add WETH and alETH, but they have different prices

Support

FAQs

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