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: 49/246
Findings: 4
Award: $116.33
π Selected for report: 0
π Solo Findings: 0
π Selected for report: rbserver
Also found by: 0xAgro, DadeKuma, DeStinE21, HollaDieWaldfee, IgorZuk, J4de, P7N8ZK, Parad0x, Stiglitz, bytes032, carrotsmuggler, csanuragjain, dec3ntraliz3d, kaden, koxuan, lukris02, rvierdiiev, tnevler
58.9366 USDC - $58.94
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L115 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L118
There is no way to remove derivative and even if the owner will set weight = 0 it will not help (it does not remove a protocol address from derivatives mapping). If one protocol from derivatives will be compromised it is still will be called in unstake() function and will be reverted everytime. the impossibility of removing a bad protocol from derivatives may cause all funds to get stuck on the derivative addresses.
If all funds are stolen from one of the derivative protocols then unstake function will always revert here. All funds will get frozen in derivatives because unstake() function is the only way to withdraw funds.
Manual review.
Add a function that will delete protocol from derivatives mapping by index. For example:
/** @notice - Delete derivative from the index fund @param _derivativeIndex - index of the derivative to delete */ function deleteDerivative( uint256 _derivativeIndex ) external onlyOwner { totalWeight = totalWeight - weights[_derivativeIndex]; derivatives[_derivativeIndex] = derivatives[derivativeCount-1]; weights[_derivativeIndex] = weights[derivativeCount-1]; derivativeCount--; }
#0 - c4-pre-sort
2023-04-04T17:33:51Z
0xSorryNotSorry marked the issue as duplicate of #703
#1 - c4-judge
2023-04-21T15:07:12Z
Picodes marked the issue as satisfactory
π Selected for report: adriro
Also found by: 0xMirce, 0xRajkumar, 0xepley, BPZ, Bahurum, Bauer, Co0nan, Emmanuel, Franfran, HollaDieWaldfee, IgorZuk, MiloTruck, NoamYakov, RedTiger, Ruhum, T1MOH, Tricko, ad3sh_, auditor0517, bin2chen, carrotsmuggler, eyexploit, handsomegiraffe, igingu, jasonxiale, koxuan, lukris02, monrel, nadin, peanuts, rbserver, rvierdiiev, shaka, sinarette, tnevler, y1cunhui
4.5426 USDC - $4.54
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L86-L88 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L73 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L92
@notice - Get price of derivative in terms of ETH
But WstEth.ethPerDerivative() call getStETHByWstETH() function that returns the price of WstETH in terms of stETH (not ETH).
WstEth.ethPerDerivative() is called in stake():
The incorrect calculation of mintAmount will cause the user to receive the wrong amount of safEth.
/** * @notice Get amount of stETH for a given amount of wstETH * @param _wstETHAmount amount of wstETH * @return Amount of stETH for a given wstETH amount */ function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256) { return stETH.getPooledEthByShares(_wstETHAmount); }
IWStETH(WST_ETH).getStETHByWstETH() function converts WstEth to stEth.
But ethPerDerivative() must returns the value of WstEth in terms of ΠTH not in terms of stEth:
@notice - Get price of derivative in terms of ETH
Manual review.
Converte stETH to ETH in ethPerDerivative() function.
#0 - c4-pre-sort
2023-03-31T16:12:17Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T17:20:39Z
0xSorryNotSorry marked the issue as duplicate of #588
#2 - c4-judge
2023-04-21T17:09:41Z
Picodes marked the issue as satisfactory
#3 - c4-judge
2023-04-22T08:55:20Z
Picodes marked the issue as partial-50
#4 - Picodes
2023-04-22T08:55:29Z
Partial Credit in absence of a PoC
#5 - c4-judge
2023-04-24T20:44:43Z
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
42.0604 USDC - $42.06
Context:
function unstake(uint256 _safEthAmount) external {
L108
Recommendation: add this to the beginning of the function:
require(balanceOf(msg.sender) >= _safEthAmount, "not enough tokens");
Context:
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L5import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L6import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L9import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L13Description:
There is another Openzeppelin Ownable contract Ownable2StepUpgradeable.sol. It also has transferOwnership function, but it is more secure due to 2-stage ownership transfer.
Context:
@param _owner - owner of the contract which handles stake/unstake
L31@param _owner - owner of the contract which handles stake/unstake
L40@param _owner - owner of the contract which handles stake/unstake
L34Recommendation:
Change to @param _owner - address of SafEth contract
.
Context:
function setMaxSlippage(
L201function setMinAmount(uint256 _minAmount) external onlyOwner {
L213function setMaxAmount(uint256 _maxAmount) external onlyOwner {
L222function setPauseStaking(bool _pause) external onlyOwner {
L231function setPauseUnstaking(bool _pause) external onlyOwner {
L240Recommendation:
The best practice is to use two-step procedure for critical changes to make them less error-prone.
Context:
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L5import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L6import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L9import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L13Description:
"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol" used in this project contracts implements renounceOwnership() function. Renouncing ownership will leave the contract without an owner, protected functions may become permanently inaccessible.
Recommendation:
You need to reimplement the function.
Context:
function ethPerDerivative(uint256 _amount) public view returns (uint256) {
L86 (_amount)function ethPerDerivative(uint256 _amount) public view returns (uint256) {
L111 (_amount)Description: this variable is not actually used in the functions. Forcing users to enter a value can mislead them.
Recommendation: Write a comment that the variable is not used.
Context:
@notice - Adds new derivative to the index fund
L158
Description: Misleading comment, function does not add new derivative, it updates the weight.
Context:
function stake() external payable {
L63Description: msg.value is not equal to the number of received tokens. Users will be more convenient to see the number of the received tokens.
Recommendation:
Add return mintAmount;
at the end of the function.
Context:
event ChangeMinAmount(uint256 indexed minAmount);
L21 (the events should include the new value and old value where possible)event ChangeMaxAmount(uint256 indexed maxAmount);
L22 (the events should include the new value and old value where possible)event WeightChange(uint indexed index, uint weight);
L28 (totalWeight changes too, new totalWeight must be included in event)event DerivativeAdded(;
L29 (totalWeight changes too, new totalWeight must be included in event)Description: Events are generally emitted when sensitive changes are made to the contracts. Some events are missing important parameters.
Context:
Description: The owner may accidentally set the same value of pauseStaking/pauseUnstaking that has already been set, and not notice it for a long time. This can lead to disastrous consequences. Recommendation: To avoid this, you can add checks:
Context:
function setMaxSlippage(uint256 _slippage) external onlyOwner {
L48 (_slippage)function withdraw(uint256 _amount) external onlyOwner {
L56 (_amount)function ethPerDerivative(uint256 _amount) public view returns (uint256) {
L86 (_amount)function setMaxSlippage(uint256 _slippage) external onlyOwner {
L51 (_slippage)function ethPerDerivative(uint256 _amount) public view returns (uint256) {
L111 (_amount)function withdraw(uint256 amount) external onlyOwner {
107 (_amount)Context:
derivatives[i].balance()) / 10 ** 18;
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L74-L75 Change to:
derivatives[i].balance()) / 10 ** 18;
(bool sent, ) = address(msg.sender).call{value: ethAmountToWithdraw}( "" );
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L124-L126 Change to:
(bool sent, ) = address(msg.sender).call{value: ethAmountToWithdraw}("");
Also here:
Context:
function setMaxSlippage(uint256 _slippage) external onlyOwner {
L48 (external function can not go after public function)receive() external payable {}
L97 (receive function can not go after public function)function setMaxSlippage(uint256 _slippage) external onlyOwner {
L51 (external function can not go after public function)receive() external payable {}
L126 (receive function can not go after public function)receive() external payable {}
L245 (receive function can not go after external function)function setMaxSlippage(uint256 _slippage) external onlyOwner {
L58 (external function can not go after public function)receive() external payable {}
L244 (receive function can not go after public function)Description:
According to official solidity documentation functions should be grouped according to their visibility and ordered:
constructor
receive function (if exists)
fallback function (if exists)
external
public
internal
private
Within a grouping, place the view and pure functions last.
Recommendation:
Put the functions in the correct order according to the documentation.
Context:
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L5import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
L6import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L6import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
L7import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
L4import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L9import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
L11import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
L6import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
L13Recommendation:
Example:
Instead of:
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
You can do your imports like this:
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"
;
Context:
function rethAddress() private view returns (address) {
L66function swapExactInputSingleHop(
L83function poolCanDeposit(uint256 _amount) private view returns (bool) {
L120function poolPrice() private view returns (uint256) {
L228Description: Internal and private functions, state variables, constants, and immutables should starting with an underscore.
Context:
RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(
L142
Description:
Maximum suggested line length is 120 characters.
Context:
event WeightChange(uint indexed index, uint weight);
L28 (Change WeightChange to WeightChanged)
#0 - c4-sponsor
2023-04-07T21:50:54Z
toshiSat marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-24T17:15:37Z
Picodes marked the issue as grade-a
π Selected for report: Rolezn
Also found by: 0x3b, 0xGordita, 0xSmartContract, 0xhacksmithh, 0xnev, 0xpanicError, 4lulz, Angry_Mustache_Man, ArbitraryExecution, Aymen0909, Bason, BlueAlder, EvanW, Franfran, HHK, Haipls, IgorZuk, JCN, KrisApostolov, Madalad, MiksuJak, MiniGlome, RaymondFam, ReyAdmirado, Rickard, Sathish9098, Udsen, adriro, alexzoid, anodaram, arialblack14, c3phas, carlitox477, ch0bu, chaduke, codeslide, d3e4, dicethedev, ernestognw, fatherOfBlocks, georgits, hunter_w3b, inmarelibero, lukris02, mahdirostami, maxper, pavankv, pixpi, rotcivegaf, smaul, tank, tnevler, wen, yac
10.7864 USDC - $10.79
Context:
for (uint i = 0; i < derivativeCount; i++)
L71 (derivativeCount)for (uint i = 0; i < derivativeCount; i++) {
L84 (derivativeCount)for (uint256 i = 0; i < derivativeCount; i++) {
L113 (derivativeCount)for (uint i = 0; i < derivativeCount; i++) {
L140 (derivativeCount)for (uint i = 0; i < derivativeCount; i++) {
L147 (derivativeCount)for (uint256 i = 0; i < derivativeCount; i++)
L171 (derivativeCount)derivatives[derivativeCount] = IDerivative(_contractAddress);
L185 (derivativeCount)weights[derivativeCount] = _weight;
L186 (derivativeCount)for (uint256 i = 0; i < derivativeCount; i++)
L190 (derivativeCount)emit DerivativeAdded(_contractAddress, _weight, derivativeCount);
L193 (derivativeCount)Description:
Every reading from storage costs about 100 gas while every reading from memory costs only 3 gas. If state variable referred more than once within a function then it is cheaper to cache it in local memory (100 gas) and then read it from memory wnen it is neaded (3 gas) rather than read state variable from storage everytime (100 gas).
Recommendation:
Example how to fix. Change:
for (uint i = 0; i < derivativeCount; i++)
to:
uint256 _derivativeCount = derivativeCount; for (uint i = 0; i < _derivativeCount; i++)
Context:
Description:
Total weight of all derivatives is already stored in totalWeight. It is unnecessary to calculate it in loop again.
Recommendation:
Instead of this:
uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight;
Do this:
totalWeight += _weight;
Instead of this:
weights[_derivativeIndex] = _weight; uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight;
Do this:
uint256 oldWeight = weights[_derivativeIndex]; weights[_derivativeIndex] = _weight; totalWeight -= oldWeight; totalWeight += _weight;
Context:
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L201 (checked in L200)
Description:
Some gas can be saved by using an unchecked {} block if an underflow isn't possible because of a previous require() or if-statement.
Context:
(derivatives[i].ethPerDerivative(derivatives[i].balance()) *
L73 (derivatives[i]) derivatives[i].balance()) /
L74 (derivatives[i])if (derivatives[i].balance() > 0)
L141 (derivatives[i])derivatives[i].withdraw(derivatives[i].balance());
L142 (derivatives[i])if (weights[i] == 0 || ethAmountToRebalance == 0) continue;
L148 (weights[i])uint256 ethAmount = (ethAmountToRebalance * weights[i]) /
L149 (weights[i])Description:
If you read value from mapping/array more than once within a function then it is cheaper to cache it in local memory and then read it from memory wnen it is neaded. This will save about 100 gas.
Recommendation: Example how to fix. Change:
for (uint i = 0; i < derivativeCount; i++) underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18;
to:
for (uint i = 0; i < derivativeCount; i++) IDerivative derivative = derivatives[i]; underlyingValue += (derivative.ethPerDerivative(derivative.balance()) * derivative.balance()) / 10 ** 18;
#0 - c4-sponsor
2023-04-10T16:43:48Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-23T15:15:47Z
Picodes marked the issue as grade-b