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: 53/246
Findings: 2
Award: $101.00
🌟 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#L108-L129
If a bugged or malicious derivative contract were added, funds can be locked.
The unstake
function seen below loops over all derivatives, if one derivative function reverts the function will always revert - hence pausing unstaking.
108: function unstake(uint256 _safEthAmount) external { ... 113: for (uint256 i = 0; i < derivativeCount; i++) { 114: // withdraw a percentage of each asset based on the amount of safETH 115: uint256 derivativeAmount = (derivatives[i].balance() * 116: _safEthAmount) / safEthTotalSupply; 117: if (derivativeAmount == 0) continue; // if derivative empty ignore 118: derivatives[i].withdraw(derivativeAmount); 119: } ... 129: }
Manual Review
Consider adding a removeDerivative
function.
#0 - c4-pre-sort
2023-04-02T12:54:52Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-04-04T17:31:28Z
0xSorryNotSorry marked the issue as duplicate of #703
#2 - c4-judge
2023-04-20T17:16:56Z
Picodes marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-04-21T15:06:27Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-04-24T19:36:11Z
Picodes changed the severity to 3 (High Risk)
🌟 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
All contracts in scope (ex) use a Solidity version of ^0.8.13
. There is a known bug that is present in Solidity 0.8.13
as well as 0.8.14
. Consider using a Solidity version past 0.8.14
.
When setting the state variables minAmount
and maxAmount
in SafEth.sol
, there is no input sanitization to ensure minAmount <= maxAmount
. See setMaxAmount
and setMinAmount
.
214: function setMinAmount(uint256 _minAmount) external onlyOwner { 215: minAmount = _minAmount; 216: emit ChangeMinAmount(minAmount); 217: }
223: function setMaxAmount(uint256 _minAmount) external onlyOwner { 224: maxAmount = _maxAmount; 225: emit ChangeMaxAmount(maxAmount); 226: }
There is no logical reason minAmount > maxAmount
should be true, and admin error may make it so.
Consider adding a require check to make sure minAmount <= maxAmount
when changing either variable.
All contracts in scope use openzeppelin's OwnableUpgradeable
contract (ex) which uses a single step owner transfer seen here:
83: function _transferOwnership(address newOwner) internal virtual { 84: address oldOwner = _owner; 85: _owner = newOwner; 86: emit OwnershipTransferred(oldOwner, newOwner); 87: }
Single step owner transfers are prone to admin error.
When using (bool sent, ) = address(msg.sender).call
, leaving out the returnData does not prevent it from being copied to memory. If transactions were relayed, gas griefing is possible. Consider using assembly to prevent such a grief.
/contracts/SafEth/SafEth.sol
124: (bool sent, ) = address(msg.sender).call{value: ethAmountToWithdraw}(
/contracts/SafEth/derivatives/WstEth.sol
63: (bool sent, ) = address(msg.sender).call{value: address(this).balance}( 76: (bool sent, ) = WST_ETH.call{value: msg.value}("");
/contracts/SafEth/derivatives/SfrxEth.sol
84: (bool sent, ) = address(msg.sender).call{value: address(this).balance}(
/contracts/SafEth/derivatives/Reth.sol
110: (bool sent, ) = address(msg.sender).call{value: address(this).balance}(
For more information please see this post.
The setPauseStaking
function is used to pause staking function which emits an event:
232: function setPauseStaking(bool _pause) external onlyOwner { 233: pauseStaking = _pause; 234: emit StakingPaused(pauseStaking); 235: }
The StakingPaused
event can help users listen for if the protocol has paused staking; however, the owner can bypass this. The owner can add a derivative that forces a reversion in it's ethPerDerivative
function. The stake
function seen below loops over all derivatives, if one reverts the function will always revert - hence pausing staking.
63: function stake() external payable { ... 70: // Getting underlying value in terms of ETH for each derivative 71: for (uint i = 0; i < derivativeCount; i++) 72: underlyingValue += 73: (derivatives[i].ethPerDerivative(derivatives[i].balance()) * 74: derivatives[i].balance()) / 75: 10 ** 18; ... 101: }
If the protocol is paused in the manner descibed above the honesty of the protocol is impacted.
As described in the Solidity documentation:
"
uint
andint
are aliases foruint256
andint256
, respectively".
There are moments in the codebase where uint
is used instead of the explicit uint256
. It is best to be explicit with variable names to lessen confusion. Consider replacing instances of uint
with uint256
.
/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); 31: uint weight, 32: uint index 92: uint derivativeReceivedEthValue = (derivative.ethPerDerivative( 203: uint _derivativeIndex, 204: uint _slippage
Consider using bytes.concat()
instead of abi.encodePacked()
in contracts with Solidity version >= 0.8.4
.
/contracts/SafEth/derivatives/Reth.sol
70: abi.encodePacked("contract.address", "rocketTokenRETH") 125: abi.encodePacked("contract.address", "rocketDepositPool") 136: abi.encodePacked( 162: abi.encodePacked("contract.address", "rocketDepositPool") 191: abi.encodePacked("contract.address", "rocketTokenRETH") 233: abi.encodePacked("contract.address", "rocketTokenRETH")
Some functions use named returns and others do not. It is best for code clearity to keep a consistent style.
returns(uint256 foo)
).returns(uint256)
): WstEth.sol, and SfrxEth.sol.There is a spelling mistake in the codebase. Consider fixing all spelling mistakes.
contracts/SafEth/SafEth.sol
derivatives
is misspelled as derivates
.There are moments in the codebase where lines (if expanded) less than 120 characters are collapsed - thus not voiding the Solidity Style Guide. The code here and here are examples. Some instances of this may void the Solidity Style Guide (see later in the report). Consider expanding code that is less than 120 characters.
This block and this block use single line conditional statements; however, in both cases the if
style differs from the else
style. The if
statements use a newline where the else
statement does not. Consider using a single consistant style.
Almost all comments do not have a trailing period, with the exception of this line and this line. Consider removing all trailing periods to maintain style.
There is an inconsistency in the capitalization of comments. For example, this line is not capitilized, while this line is. Consider capitilizing all comments.
The value 10 ** 18
is used consistantly throughout the codebase. Consider using a constant DECIMALS
in place of 10 ** 18
.
The value 10 ** 18
is used in the style seen for almost all instances in the codebase (ex). There is one instance where 10 ** 18
is in the style 1e18
. Consider changing this style to 10 ** 18
or all instances to 1e18
.
The ERC20 Token names do not indicate they are a derivative contract and use the native token names. They should be more descriptive. Consider changing the token names to better reflect the use and not confuse users.
/contracts/SafEth/derivatives/WstEth.sol
41: function name() public pure returns (string memory) { 42: return "Lido"; 43: }
/contracts/SafEth/derivatives/SfrxEth.sol
44: function name() public pure returns (string memory) { 45: return "Frax"; 46: }
/contracts/SafEth/derivatives/Reth.sol
50: function name() public pure returns (string memory) { 51: return "RocketPool"; 52: }
Consider adding a recovery function for Tokens that are accidently sent to the core contracts of the protocol.
All contracts in scope use a compiler version of ^0.8.13
. No one knows what will come in future compiler versions < 0.9.0
, thus it is best to put an upper limit on the compiler version.
Lines with greater length than 120 characters are used. The Solidity Style Guide suggests that all lines should be 120 characters or less in width.
The following lines are longer than 120 characters, it is suggested to shorten these lines:
contracts/SafEth/SafEth.sol
contracts/SafEth/derivatives/Reth.sol
The Solidity Style Guide suggests the following function order: constructor, receive function (if exists), fallback function (if exists), external, public, internal, private.
The following contracts are not compliant (examples are only to prove the functions are out of order NOT a full description):
The Solidity Style Guide recommends to
"[a]void extraneous whitespace [i]mmediately inside parenthesis, brackets or braces, with the exception of single line function declarations".
The Style Guide also recommends against spaces before semicolons. Consider removing spaces before semicolons.
The following do follow Solidity style conventions, but technically void the style guide.
/contracts/SafEth/derivatives/WstEth.sol
63: (bool sent, ) = address(msg.sender).call{value: address(this).balance}( 76: (bool sent, ) = WST_ETH.call{value: msg.value}("");
/contracts/SafEth/derivatives/SfrxEth.sol
84: (bool sent, ) = address(msg.sender).call{value: address(this).balance}(
/contracts/SafEth/SafEth.sol
124: (bool sent, ) = address(msg.sender).call{value: ethAmountToWithdraw}(
/contracts/SafEth/derivatives/Reth.sol
110: (bool sent, ) = address(msg.sender).call{value: address(this).balance}( 240: (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
The Solidity Style Guide states that
"[f]or short function declarations, it is recommended for the opening brace of the function body to be kept on the same line as the function declaration".
It is not clear what length is exactly meant by "short" or "long". The maximum line length of 120 characters may be an indication, and in that case any expanded function declaration under 120 characters should be on a single line.
The following functions in their respective contracts are not compliant by this standard:
contracts/SafEth/SafEth.sol
#0 - c4-sponsor
2023-04-10T20:26:03Z
elmutt marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-24T18:33:54Z
Picodes marked the issue as grade-a