Platform: Code4rena
Start Date: 24/03/2023
Pot Size: $49,200 USDC
Total HM: 20
Participants: 246
Period: 6 days
Judge: Picodes
Total Solo HM: 1
Id: 226
League: ETH
Rank: 3/246
Findings: 3
Award: $1,363.11
π Selected for report: 1
π Solo Findings: 0
π Selected for report: whoismatthewmc1
Also found by: m_Rassska
1309.3354 USDC - $1,309.34
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L68-L81 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L98
Potential inability to stake (ie: DoS) if sole safETH user (ie: this would also make them the sole safETH holder) unstakes totalSupply - 1
.
The goal of this POC is to prove that this line can revert https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L98
uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
This can occur if the attacker can cause preDepositPrice = 0
.
A user who is the first staker will be the sole holder of 100% of totalSupply
of safETH.
They can then unstake (and therefore burn) totalSupply - 1
leaving a total of 1 wei of safETH in circulation.
In earlier lines in stake()
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L77-L81, we see
uint256 totalSupply = totalSupply(); uint256 preDepositPrice; // Price of safETH in regards to ETH if (totalSupply == 0) preDepositPrice = 10 ** 18; // initializes with a price of 1 else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;
With totalSupply = 1
, we see that the above code block will execute the else
code path, and that if underlyingValue = 0
, then preDepositPrice = 0
.
underlyingValue
is set in earlier lines: https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L68-L75
uint256 underlyingValue = 0; // Getting underlying value in terms of ETH for each derivative for (uint i = 0; i < derivativeCount; i++) underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18;
For a simple case, assume there is 1 derivative with 100% weight. Let's use rETH for this example since the derivative can get its ethPerDerivative
price from an AMM. In this case:
ethPerDerivative()
value has been manipulated in the underlying AMM pool such that 1 derivative ETH is worth less than 1 ETH. eg: 1 rETH = 9.99...9e17 ETHderivatives[i].balance() = 1 wei
.This case will result in underlyingValue += (9.99...9e17 * 1) / 10 ** 18 = 0
.
We can see that it is therefore possible to cause a divide by 0 revert and malfunction of the stake()
function.
Manual Review
Assuming the deployment process will set up at least 1 derivative with a weight, simply adding a stake()
operation of 0.5 ETH as the first depositor as part of the deployment process avoids the case where safETH totalSupply drops to 1 wei.
Otherwise, within unstake()
it is also possible to require that totalSupply
does not fall between 0 and minimumSupply
where minimumSupply
is, for example, the configured minAmount
.
#0 - c4-pre-sort
2023-04-04T18:39:23Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-04-06T22:44:40Z
toshiSat marked the issue as disagree with severity
#2 - c4-sponsor
2023-04-06T22:47:19Z
toshiSat marked the issue as sponsor confirmed
#3 - toshiSat
2023-04-06T22:48:04Z
Seems like a pretty big edge case and it would leave the contract with basically no funds which doesn't seem like a High severity to me
#4 - Picodes
2023-04-17T09:33:44Z
Indeed, the described scenario isn't of high severity although the finding is valid. Basically, the first or last SafETH user could force the owner to redeploy, so downgrading to Med.
#5 - c4-judge
2023-04-17T09:33:56Z
Picodes changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-04-22T10:35:55Z
Picodes marked the issue as selected for report
π Selected for report: RaymondFam
Also found by: 0xepley, BPZ, Franfran, Parad0x, RedTiger, d3e4, fyvgsk, handsomegiraffe, ladboy233, rbserver, silviaxyz, whoismatthewmc1, yac
40.6368 USDC - $40.64
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L91 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L83-L102 https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L173-L183
While the protocol-allowed stake()
maxAmount
is 200 ETH, the actual maxAmount
accepted may be much lower.
When taken to the extreme, with 0 liquidity in the underlying rETH/ETH UNI pool, stake()
will always fail.
Similar cases apply for the CRV pools used in the SfrxETH and WstEth contracts.
Currently, the TVL of the the rETH/ETH UNI pool is equivalent to ~$5M with 1.44k rETH and 1.43 ETH. https://info.uniswap.org/#/pools/0xa4e0faa58465a2d369aa21b3e42d43374c6f9613
In SafEth.sol, the maxAmount
for stake()
is set to 200 ETH here: https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L55
In Reth.sol, the maxSlippage
for rETH UNI swaps is set to 1% here: https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L44
Due to the 1% maxSlippage, Reth.sol's swapExactInputSingleHop
will fail if the amount out is less than
amountOutMinimum: _minOut,
Therefore, if a significant portion of the liquidity is withdrawn and the Reth derivative maintains a portion of the total weight, this could result in a DoS.
Again, this can be similarly applied across the CRV pools used in the SfrxEth and WstEth contracts.
Manual Review
There are a few options for mitigation:
maxAmount
. In the event that TVL drops below this threshold for a given derivative's pool, use adjustWeight()
to set that derivative's weight to 0.maxAmount
so that hitting 1% slippage is less likely even in shallower pools.#0 - c4-pre-sort
2023-04-04T21:21:28Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-pre-sort
2023-04-04T21:56:00Z
0xSorryNotSorry marked the issue as duplicate of #365
#2 - c4-judge
2023-04-24T18:17:02Z
Picodes marked the issue as not a duplicate
#3 - c4-judge
2023-04-24T18:18:13Z
Picodes marked the issue as duplicate of #588
#4 - c4-judge
2023-04-24T18:18:18Z
Picodes marked the issue as satisfactory
#5 - Picodes
2023-04-24T18:18:41Z
Flagging this issue as dup of the global issue: "hardcoded slippage / price can lead to DoS"
#6 - c4-judge
2023-04-24T21:12:41Z
Picodes marked the issue as not a duplicate
#7 - c4-judge
2023-04-24T21:13:31Z
Picodes marked the issue as duplicate of #150