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: 76/246
Findings: 3
Award: $63.98
🌟 Selected for report: 0
🚀 Solo Findings: 0
11.1318 USDC - $11.13
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L48-L56 https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L63
During the period between the deployment of the SafEth
contract and the addition of derivatives, there is a possibility for users to send Ether to the contract using the stake() payable
function. In this scenario, the funds will become locked and inaccessible, resulting in a potential loss of user funds.
Testing this scenario in Foundry with command forge test -vv --match-test testFailEtherLockedOnStake
. The test checks if Ether becomes locked in the SafEth contract when staking before derivatives are added.
// SPDX-License-Identifier: MIT pragma solidity 0.8.13; import "ds-test/test.sol"; import "./cheatcodes.sol"; // CheatCodes interface from https://book.getfoundry.sh/cheatcodes/ import "../../contracts/SafEth/SafEth.sol"; import "../../contracts/SafEth/derivatives/Reth.sol"; import "../../contracts/SafEth/derivatives/SfrxEth.sol"; import "../../contracts/SafEth/derivatives/WstEth.sol"; import "../../contracts/interfaces/IDerivative.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; contract SafEthTest is Test { uint256 private constant DEFAULT_MAX_SLIPPAGE = 1e16; // 1% uint256 private constant DEFAULT_DERIVATIVE_WEIGHT = 1e18; uint256 private TEST_DEPOSIT_WEI = 1 ether; CheatCodes private cheats; address private admin; SafEth private safEth; Reth private reth; SfrxEth private sfrxEth; WstEth private wstEth; mapping(uint256 => IDerivative) private derivatives; // Can receive unstaked Ether receive() external payable {} function setUp() public { // Test on mainnet fork cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); cheats.createSelectFork("mainnet"); // Set proxy owner address admin = vm.addr(1337); // safEth token deploy safEth = SafEth( payable( new TransparentUpgradeableProxy( address(new SafEth()), admin, abi.encodeWithSelector( SafEth.initialize.selector, "Asymmetry Finance ETH", "safETH" ) ) ) ); // Don't deploy any derivatives } function testFailEtherLockedOnStake() public { // Add Ether to the current contract's balance vm.deal(address(this), TEST_DEPOSIT_WEI); // Stake Ether in the safEth contract using the intended stake() function safEth.stake{value: TEST_DEPOSIT_WEI}(); // Verify that no safEth tokens were minted in response to the Ether deposit assertTrue(safEth.balanceOf(address(this)) != 0, "No safEth minted, ether locked"); // The test should fail at this point, indicating that Ether is locked in the safEth contract } }
The test result shows a passed test with an error message No safEth minted, ether locked
:
Running 1 test for test/foundry/SafEth.t.sol:SafEthTest [PASS] testFailEtherLockedOnStake() (gas: 54552) Logs: Error: No safEth minted, ether locked Error: Assertion Failed Test result: ok. 1 passed; 0 failed; finished in 3.24s
VSCodium, Foundry
To prevent Ether from becoming locked in the safEth
contract initially set staking to be paused. Unpause staking after the deployment of the entire system is completed. Modify the initialize()
function as follows:
/** @notice - Function to initialize values for the contracts @dev - This replaces the constructor for upgradeable contracts @param _tokenName - name of erc20 @param _tokenSymbol - symbol of erc20 */ function initialize( string memory _tokenName, string memory _tokenSymbol ) external initializer { // ... pauseStaking = true; }
#0 - c4-pre-sort
2023-04-02T08:52:10Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-04-04T19:16:56Z
0xSorryNotSorry marked the issue as duplicate of #363
#2 - c4-judge
2023-04-21T16:29:46Z
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
The compiler version is not locked, which may lead to deploying contracts with different or bugged versions:
pragma solidity ^0.8.13;
To avoid potential issues, lock the pragma version and ensure that the chosen compiler version is free of known bugs:
pragma solidity 0.8.19;
An import statement is present in the source file, but the imported file is not used:
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../interfaces/IWETH.sol"; import "../interfaces/uniswap/ISwapRouter.sol"; import "../interfaces/lido/IWStETH.sol"; import "../interfaces/lido/IstETH.sol";
import "../../interfaces/frax/IsFrxEth.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
To optimize the code and avoid redundancy, remove the unnecessary import.
The Initializable
contract is already inherited through the parent contract OwnableUpgradeable
:
contract SafEth is Initializable, ERC20Upgradeable, OwnableUpgradeable, SafEthStorage
contract Reth is IDerivative, Initializable, OwnableUpgradeable {
contract SfrxEth is IDerivative, Initializable, OwnableUpgradeable {
contract WstEth is IDerivative, Initializable, OwnableUpgradeable {
Remove the unnecessary extra inheritance of Initializable
.
In initialize()
function consider using more suitable function names at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L52-L53.
ERC20Upgradeable.__ERC20_init(_tokenName, _tokenSymbol);
with __ERC20_init(_tokenName, _tokenSymbol);
_transferOwnership(msg.sender);
with __Ownable_init();
In initialize()
function replace _transferOwnership()
with transferOwnership()
, this will add an important zero address check:
The stEthPerToken()
function is a more appropriate analogue of getStETHByWstETH()
at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L87. It performs calculations using the default 10 ** 18
value. Example:
function ethPerDerivative(uint256 _amount) public view returns (uint256) { return IWStETH(WST_ETH).stEthPerToken(); }
These changes will make the code more readable and easier to understand.
The _safEthAmount
parameter of the unstake()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L108 does not have a zero check. Add check for the _safEthAmount
parameter. For example:
require(_safEthAmount != 0, "Unstaking zero amount");
The _weight
parameter of the adjustWeight()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L184 does not have any checks. Adjusting a _weight
with a very large amount may cause arithmetic overflow in the system logic. Add check for the _weight
parameter. For example:
require(_weight < 1e20, "Weight is to large");
The _derivativeIndex
parameter of the adjustWeight()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L166 and setMaxSlippage()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L203 do not have out of range checks. To prevent potential issues, add checks for the _derivativeIndex
parameter. For example:
require(_derivativeIndex < derivativeCount, "Index is out of range");
The _minAmount
parameter of the setMinAmount()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L214 should be greater than zero and less than maxAmount
. Add check for the _minAmount
parameter. For example:
require(_minAmount != 0, "Should not be zero"); require(_minAmount < maxAmount, "Shoul be less than maxAmount");
The _maxAmount
parameter of the setMaxAmount()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L223 should be greater than _minAmount
. Add check for the _maxAmount
parameter. For example:
require(_maxAmount > minAmount, "Should be greater than minAmount");
Consider adding these checks to ensure that input parameters are within the expected range.
The _amount
variables at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L86 and https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L111 are not used and should be removed. Solidity supports input parameters without names. Example:
function ethPerDerivative(uint256) public view returns (uint256) { return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18); }
The comment for the adjustWeight()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L158 is incorrect and mistakenly copied from the function addDerivative()
. Update the comment to accurately describe the adjustWeight()
function.
Based on _weight
comment at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L163 is not clear that a direvative can be paused by setting _weight
to zero. Update the comment to clarify this point.
The adjustWeight()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L165 currently updates only one weight at a time. If you have a large number of derivatives and need to update multiple weights in a single transaction, consider implementing a batch update function that takes an array of indices and weights.
#0 - c4-sponsor
2023-04-07T21:26:36Z
toshiSat marked the issue as sponsor confirmed
#1 - c4-judge
2023-04-24T17:02:51Z
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
The derivativeCount
storage variable is accessed multiple times within for-loops, which can lead to inefficient gas usage:
for (uint i = 0; i < derivativeCount; i++)
for (uint i = 0; i < derivativeCount; i++) {
for (uint256 i = 0; i < derivativeCount; i++) {
for (uint i = 0; i < derivativeCount; i++) {
for (uint i = 0; i < derivativeCount; i++) {
for (uint256 i = 0; i < derivativeCount; i++)
for (uint256 i = 0; i < derivativeCount; i++)
derivatives[derivativeCount] = IDerivative(_contractAddress); weights[derivativeCount] = _weight; derivativeCount++;
Additionally, the derivatives[i]
and weights[i]
storage variables are accessed multiple times:
// 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 (uint256 i = 0; i < derivativeCount; i++) { // withdraw a percentage of each asset based on the amount of safETH uint256 derivativeAmount = (derivatives[i].balance() * _safEthAmount) / safEthTotalSupply; if (derivativeAmount == 0) continue; // if derivative empty ignore derivatives[i].withdraw(derivativeAmount); }
for (uint i = 0; i < derivativeCount; i++) { if (derivatives[i].balance() > 0) derivatives[i].withdraw(derivatives[i].balance()); }
for (uint i = 0; i < derivativeCount; i++) { if (weights[i] == 0 || ethAmountToRebalance == 0) continue; uint256 ethAmount = (ethAmountToRebalance * weights[i]) / totalWeight; // Price will change due to slippage derivatives[i].deposit{value: ethAmount}(); }
Consider caching these variables in memory to optimize gas usage.
ethAmountToRebalance
Outside a LoopThe ethAmountToRebalance
variable at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L148 could be checked once before the for-loop. Example:
uint256 ethAmountToRebalance = ethAmountAfter - ethAmountBefore; require(ethAmountToRebalance != 0, "Nothing to rebalance");
totalWeight
CalculationThe totalWeight
calculation in the adjustWeight()
function at https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L170-L173 could be optimized. You can simply subtract the old weight and add the new weight, like this:
totalWeight -= weights[_derivativeIndex] + _weight; weights[_derivativeIndex] = _weight;
The totalWeight
calculation in the addDerivative()
function at lines https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L190-L193 could be optimized as well. You can simply add the new weight, like this:
totalWeight += _weight;
#0 - c4-sponsor
2023-04-07T21:34:31Z
toshiSat marked the issue as sponsor acknowledged
#1 - c4-judge
2023-04-23T14:40:53Z
Picodes marked the issue as grade-b