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: 99/246
Findings: 1
Award: $42.06
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
Inheriting from OpenZeppelin's OwnableUpgradeable
contract means you are using a single-step ownership transfer pattern. If an admin provides an incorrect address for the new owner this will result in none of the onlyOwner
 marked methods being callable again. The better way to do this is to use a two-step ownership transfer approach, where the new owner should first claim his new rights before they are transferred.
Use OpenZeppelin's Ownable2StepUpgradeable
instead of OwnableUpgradeable
Typically, the contract's owner is the account that deploys the contract. As a result, the owner
is able to perform certain privileged activities.
The Openzeppelin's OwnableUpgradeable
used in this project contract implements renounceOwnership. This can represent a certain risk if the ownership is renounced for any other reason than by design. Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.
onlyOwner
functions:
File: contracts\SafEth\SafEth.sol function rebalanceToWeights() external onlyOwner { } function adjustWeight(uint256 _derivativeIndex,uint256 _weight) external onlyOwner { } function addDerivative(address _contractAddress,uint256 _weight) external onlyOwner { } function setMaxSlippage(uint _derivativeIndex,uint _slippage) external onlyOwner { } function setMinAmount(uint256 _minAmount) external onlyOwner { } function setMaxAmount(uint256 _maxAmount) external onlyOwner { } function setPauseStaking(bool _pause) external onlyOwner { } function setPauseUnstaking(bool _pause) external onlyOwner { }
onlyOwner
functions:
File: contracts\SafEth\derivatives\Reth.sol File: contracts\SafEth\derivatives\SfrxEth.sol File: contracts\SafEth\derivatives\WstEth.sol function setMaxSlippage(uint256 _slippage) external onlyOwner { } function withdraw(uint256 amount) external onlyOwner { } function deposit() external payable onlyOwner returns (uint256) { }
owner
- Single point of failureLikelihood:Â Low, because it requires a malicious/compromised admin
Impact:Â High, because it can brick the protocol
The protocol's owner
currently wields an excessive amount of control. In the event of a hack or compromise, the protocol would fail. This is not just a centralization risk, it represents a major point of failure for the protocol. If a hacker were to attack the owner and gain control over their EOA, the protocol would fail, resulting in financial losses for all customers.
Consider using a role-based access control approach instead of a single admin role as well as a timelock for important admin actions.
No mechanism to add pause/unpause to derivatives except explicitly set weight == 0
but it can be unclear to user.
unstake()
Because unstake()
function makes low-level external call that has no limit to gas spending is a good practice to add ReentrancyGuard. All throw there is no explicit reentrancy in unstake()
function adding ReentrancyGuard protects against potential uncovered reentrancy attacks.
There is no logic to getting user options for slippage. Protocol control minOut of token that user will get.
ethPerDerivative
can return 0If the sqrtPriceX96
value is too low, the poolPrice()
function will return 0 due to rounding, and consequently, the ethPerDerivative
function will also return 0.
function ethPerDerivative(uint256 _amount) public view returns (uint256) { if (poolCanDeposit(_amount)) return RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18); else return (poolPrice() * 10 ** 18) / (10 ** 18); } function poolPrice() private view returns (uint256) { ... return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2); }
adjustWeight()
and addDerivative()
Functions have an identical code snippet
File: contracts\SafEth\SafEth.sol function adjustWeight(uint256 _derivativeIndex,uint256 _weight) external onlyOwner { function addDerivative(address _contractAddress,uint256 _weight) external onlyOwner {
weights[derivativeCount] = _weight; uint256 localTotalWeight = 0; for (uint256 i = 0; i < derivativeCount; i++) localTotalWeight += weights[i]; totalWeight = localTotalWeight;
true
or false
.
File: contracts\SafEth\SafEth.sol require(pauseStaking == false, "staking is paused"); require(pauseUnstaking == false, "unstaking is paused");
uint
or uint256
etcBe consistent when writing code. A lot of uin
t instead of uint256
In the swapExactInputSingleHop(...)
function, the return variable amountOut
is declared inline, which differs from the rest of the code where variables are declared within the function body.
File: contracts\SafEth\derivatives\Reth.sol function swapExactInputSingleHop(...) private returns (uint256 amountOut) {
The current version of OpenZeppelin contracts/contracts-upgradeable is v4.8.2
File: package.json { "@openzeppelin/contracts": "^4.8.0", "@openzeppelin/contracts-upgradeable": "^4.8.1", }
With the inherent upgradeability of Proxies, contracts can be upgraded numerous times, utilizing a methodical approach to track the version of each upgrade. I recommend incorporating an automatic version counter that increments whenever the contract undergoes an upgrade.
Missing @return
in NatSpec
File: contracts\SafEth\derivatives\Reth.sol function rethAddress() private view returns (address) { function swapExactInputSingleHop(address _tokenIn,address _tokenOut,uint24 _poolFee,uint256 _amountIn,uint256 _minOut) private returns (uint256 amountOut) { function poolCanDeposit(uint256 _amount) private view returns (bool) { function deposit() external payable onlyOwner returns (uint256) { function ethPerDerivative(uint256 _amount) public view returns (uint256) { function balance() public view returns (uint256) { function poolPrice() private view returns (uint256) {
File: contracts\SafEth\derivatives\SfrxEth.sol function name() public pure returns (string memory) { function deposit() external payable onlyOwner returns (uint256) { function ethPerDerivative(uint256 _amount) public view returns (uint256) { function balance() public view returns (uint256) {
File: contracts\SafEth\derivatives\WstEth.sol function name() public pure returns (string memory) { function setMaxSlippage(uint256 _slippage) external onlyOwner { function deposit() external payable onlyOwner returns (uint256) { function ethPerDerivative(uint256 _amount) public view returns (uint256) { function balance() public view returns (uint256) {
Missing @param
in NatSpec
File: contracts\SafEth\derivatives\Reth.sol function withdraw(uint256 amount) external onlyOwner {
File: contracts\SafEth\derivatives\SfrxEth.sol function setMaxSlippage(uint256 _slippage) external onlyOwner { function ethPerDerivative(uint256 _amount) public view returns (uint256) {
File: contracts\SafEth\derivatives\WstEth.sol function setMaxSlippage(uint256 _slippage) external onlyOwner { function withdraw(uint256 _amount) external onlyOwner { function ethPerDerivative(uint256 _amount) public view returns (uint256) {
File: contracts\SafEth\SafEth.sol File: contracts\SafEth\derivatives\Reth.sol File: contracts\SafEth\derivatives\SfrxEth.sol File: contracts\SafEth\derivatives\WstEth.sol receive() external payable {}
Function writing
 that does not comply with the Solidity Style Guide
Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
Solidity code is also cleaner in another way that might not be noticeable: the struct Point. We were importing it previously with global import but not using it. The Point struct polluted the source code
 with an unnecessary object we were not using because we did not need it.
This was breaking the rule of modularity and modular programming:Â only import what you need
 Specific imports with curly braces to allow us to apply this rule better.
import {contract1 , contract2} from "filename.sol";
It is generally recommended that lines in the source code should not exceed 80-120 characters. Today’s screens are much larger, so in some cases it makes sense to expand that. The lines above should be split when they reach that length, as the files will most likely be on GitHub and GitHub always uses a scrollbar when the length is more than 164 characters.
why-is-80-characters-the-standard-limit-for-code-width
Multiline output parameters and return statements should follow the same style recommended for wrapping long lines found in the Maximum Line Length section.
141: ); 142: RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface( 143: rocketProtocolSettingsAddress 144: );
Clear function names can increase readability. Follow a standard convertion function names such as using get for getter (view/pure) functions.
File: contracts\SafEth\derivatives\Reth.sol function rethAddress() private view returns (address) { function poolCanDeposit(uint256 _amount) private view returns (bool) { function ethPerDerivative(uint256 _amount) public view returns (uint256) { function balance() public view returns (uint256) { function poolPrice() private view returns (uint256) {
File: contracts\SafEth\derivatives\SfrxEth.sol function ethPerDerivative(uint256 _amount) public view returns (uint256) { function balance() public view returns (uint256) {
File: contracts\SafEth\derivatives\WstEth.sol function ethPerDerivative(uint256 _amount) public view returns (uint256) { function balance() public view returns (uint256) {
File: contracts\SafEth\SafEth.sol minAmount = 5 * 10 ** 17; // initializing with .5 ETH as minimum maxAmount = 200 * 10 ** 18; // initializing with 200 ETH as maximum underlyingValue += (derivatives[i].ethPerDerivative(derivatives[i].balance()) * derivatives[i].balance()) / 10 ** 18; preDepositPrice = 10 ** 18; // initializes with a price of 1 else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply; ) * depositAmount) / 10 ** 18; uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
File: contracts\SafEth\derivatives\Reth.sol maxSlippage = (1 * 10 ** 16); // 1% uint rethPerEth = (10 ** 36) / poolPrice(); uint256 minOut = ((rethPerEth * msg.value / 10 ** 18) * (10 ** 18 - maxSlippage)) / 10 ** 18; else return (poolPrice() * 10 ** 18) / (10 ** 18);
File: contracts\SafEth\derivatives\SfrxEth.sol maxSlippage = (1 * 10 ** 16); // 1% uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) * (10 ** 18 - maxSlippage)) / 10 ** 18; uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets(10 ** 18); return ((10 ** 18 * frxAmount) / IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());
File: contracts\SafEth\derivatives\WstEth.sol maxSlippage = (1 * 10 ** 16); // 1% uint256 minOut = (stEthBal * (10 ** 18 - maxSlippage)) / 10 ** 18; return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18);
File: contracts\SafEth\SafEth.sol import "../interfaces/IWETH.sol"; import "../interfaces/uniswap/ISwapRouter.sol"; import "../interfaces/lido/IWStETH.sol"; import "../interfaces/lido/IstETH.sol";
File: contracts\SafEth\derivatives\Reth.sol import "../../interfaces/frax/IsFrxEth.sol";
indexed
 fieldsEach event
 should use three indexed
 fields if there are three or more fields
File: contracts\SafEth\SafEth.sol event SetMaxSlippage(uint256 indexed index, uint256 slippage); event Staked(address indexed recipient, uint ethIn, uint safEthOut); event Unstaked(address indexed recipient, uint ethOut, uint safEthIn); event WeightChange(uint indexed index, uint weight); event DerivativeAdded(address indexed contractAddress,uint weight,uint index);
All tests use fixed values and don’t coder edge cases.
#0 - c4-sponsor
2023-04-07T21:41:23Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-24T17:03:34Z
Picodes marked the issue as grade-b
#2 - c4-judge
2023-04-24T17:21:58Z
Picodes marked the issue as grade-a