Stader Labs - Sathish9098's results

Decentralized ETH liquid staking protocol with 4 ETH bond for anyone to be a node operator.

General Information

Platform: Code4rena

Start Date: 02/06/2023

Pot Size: $100,000 USDC

Total HM: 15

Participants: 75

Period: 7 days

Judge: Picodes

Total Solo HM: 5

Id: 249

League: ETH

Stader Labs

Findings Distribution

Researcher Performance

Rank: 25/75

Findings: 2

Award: $272.43

QA:
grade-b
Gas:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

18.5651 USDC - $18.57

Labels

bug
grade-b
QA (Quality Assurance)
edited-by-warden
Q-34

External Links

LOW FINDINGS

[L-1] Don’t use payable.call()

payable.call() is a low-level method that can be used to send ether to a contract, but it has some limitations and risks as you've pointed out. One of the primary risks of using payable.call() is that it doesn't guarantee that the contract's payable function will be called successfully. This can lead to funds being lost or stuck in the contract

The contract does not have a payable callback The contract’s payable callback spends more than 2300 gas (which is only enough to emit something) The contract is called through a proxy which itself uses up the 2300 gas Use OpenZeppelin’s Address.sendValue() instead

FILE: 2023-06-stader/contracts/Auction.sol

131:  (bool success, ) = payable(msg.sender).call{value: withdrawalAmount}('');

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/Auction.sol#LL131C8-L131C82

FILE: 2023-06-stader/contracts/SocializingPool.sol

91:  (bool success, ) = payable(staderConfig.getStaderTreasury()).call{value: _rewardsData.protocolETHRewards} 
     ('');
121: (success, ) = payable(operatorRewardsAddr).call{value: totalAmountETH}('');

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/SocializingPool.sol#LL121C13-L121C88

FILE: 2023-06-stader/contracts/StaderStakePoolsManager.sol

102: (bool success, ) = payable(staderConfig.getUserWithdrawManager()).call{value: _amount}('');

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderStakePoolsManager.sol#L102

Use OpenZeppelin’s Address.sendValue()

[L-2] Use .call instead of .transfer to send ether

.transfer will relay 2300 gas and .call will relay all the gas. If the receive/fallback function from the recipient proxy contract has complex logic, using .transfer will fail, causing integration issues

FILE: 2023-06-stader/contracts/Auction.sol

114:  if (!IERC20(staderConfig.getStaderToken()).transfer(staderConfig.getStaderTreasury(), _sdAmount)) {

```
https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/Auction.sol#LL114C8-L114C108

```solidity
FILE: 2023-06-stader/contracts/SDCollateral.sol

68:  if (!IERC20(staderConfig.getStaderToken()).transfer(payable(operator), _requestedSD)) {

```
https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/SDCollateral.sol#LL68C8-L68C96

### Recommended Mitigation

Replace .transfer with .call. Note that the result of .call need to be checked. 


## [L-3] Unbounded loop

### This instance not found by bot race. This is most important Unbounded loop logic 

``nextOperatorId`` the value is only incremented .

PermissionedNodeRegistry.allocateValidatorsAndUpdateOperatorId() will iterate all the OperatorId.

Currently, ``nextOperatorId`` can grow indefinitely. E.g. there’s no maximum limit and there’s no functionality to remove OperatorId

If the value grows too large, calling PermissionedNodeRegistry.allocateValidatorsAndUpdateOperatorId() might run out of gas and revert. 

```solidity
FILE: Breadcrumbs2023-06-stader/contracts/PermissionedNodeRegistry.sol

206:  for (uint256 i = 1; i < nextOperatorId; ) {

488: for (uint256 i = 1; i < nextOperatorId; ) {

```
https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L488

##

## [L-4] Inconsistency between the comment and the actual value assigned

 It appears that there is an inconsistency between the comment and the actual value assigned to the constant variable. The comment states that the value represents a 24-hour duration, but the assigned value of 7200 actually corresponds to a duration of 2 hours

```diff
FILE: Breadcrumbs2023-06-stader/contracts/Auction.sol

- 22: uint256 public constant MIN_AUCTION_DURATION = 7200; // 24 hours
+ 22: uint256 public constant MIN_AUCTION_DURATION = 7200; // 2 hours
```
https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/Auction.sol#LL22C5-L22C69

##

## [L-5] Resolve the warning to avoid unexpected behavior 

```
Warning (3628): Warning: This contract has a payable fallback function, but no receive ether function. Consider adding a receive ether function.
 --> contracts/VaultProxy.sol:8:1:
  |
8 | contract VaultProxy is IVaultProxy {
  | ^ (Relevant source part starts here and spans across multiple lines).
Note: The payable fallback function is defined here.
  --> contracts/VaultProxy.sol:41:5:
   |
41 |     fallback(bytes calldata _input) external payable returns (bytes memory) {
   |     ^ (Relevant source part starts here and spans across multiple lines).

```
##

## [L-6] Consider using upgradable ERC20 contracts 

Upgradable ERC20 contracts provide flexibility and allow for future upgrades and improvements to the contract's functionality without disrupting the existing ecosystem.

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/Auction.sol#L9

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/SDCollateral.sol#L14

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/SocializingPool.sol#L15

##

## [L-7] nextValidatorId inrement should be placed on top of the for loop

When increase bottom of the for loop nextValidatorId is increased even next iteration condition check is false.

```solidity
FILE: 2023-06-stader/contracts/PermissionedNodeRegistry.sol

173:  validatorIdByPubkey[_pubkey[i]] = nextValidatorId;
174:            validatorIdsByOperatorId[operatorId].push(nextValidatorId);
175:            emit AddedValidatorKey(msg.sender, _pubkey[i], nextValidatorId);
176:            nextValidatorId++;
177:            unchecked {
178:                ++i;
179:            }

```
https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L173-L179

Recommended Mitigation:
nextValidatorId value should be increased when enter to new iterations 

##

## [L-8] Missing Event for initialize

Description: Events help non-contract tools to track changes, and events prevent users from being surprised by changes Issuing event-emit during initialization is a detail that many projects skip


```solidity
FILE: 2023-06-stader/contracts/PoolSelector.sol

function initialize(address _admin, address _staderConfig) external initializer {
        UtilLib.checkNonZeroAddress(_admin);
        UtilLib.checkNonZeroAddress(_staderConfig);
        __AccessControl_init_unchained();
        poolAllocationMaxSize = 50;
        staderConfig = IStaderConfig(_staderConfig);
        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
    }

```
https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PoolSelector.sol#L36-L43

```solidity
FILE: Breadcrumbs2023-06-stader/contracts/VaultProxy.sol

function initialise(
        bool _isValidatorWithdrawalVault,
        uint8 _poolId,
        uint256 _id,
        address _staderConfig
    ) external {
        if (isInitialized) {
            revert AlreadyInitialized();
        }
        UtilLib.checkNonZeroAddress(_staderConfig);
        isValidatorWithdrawalVault = _isValidatorWithdrawalVault;
        isInitialized = true;
        poolId = _poolId;
        id = _id;
        staderConfig = IStaderConfig(_staderConfig);
        owner = staderConfig.getAdmin();
    }


```

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/VaultProxy.sol#LL20C5-L36C6

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/factory/VaultFactory.sol#L23-L32

Recommendation: Add Event-Emit

##


# NON CRITICAL

## [NC-1] Tokens accidentally sent to the contract cannot be recovered

### Context
contracts/staking/NeoTokyoStaker.sol:

It can’t be recovered if the tokens accidentally arrive at the contract address, which has happened to many popular projects, so I recommend adding a recovery code to your critical contracts.

### Recommended Mitigation Steps
Add this code:

 /**
  * @notice Sends ERC20 tokens trapped in contract to external address
  * @dev Onlyowner is allowed to make this function call
  * @param account is the receiving address
  * @param externalToken is the token being sent
  * @param amount is the quantity being sent
  * @return boolean value indicating whether the operation succeeded.
  *
 */
  function rescueERC20(address account, address externalToken, uint256 amount) public onlyOwner returns (bool) {
    IERC20(externalToken).transfer(account, amount);
    return true;
  }
}

## [NC-2] Use SMTChecker

The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification.

https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19

##

[NC-3] According to the syntax rules, use => mapping ( instead of => mapping( using spaces as keyword

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PoolSelector.sol#L23

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderOracle.sol#L44-L47


##

## [NC-4] Assembly Codes Specific – Should Have Comments

Since this is a low level language that is more difficult to parse by readers, include extensive documentation, comments on the rationale behind its use, clearly explaining what each assembly instruction does.

This will make it easier for users to trust the code, for reviewers to validate the code, and for developers to build on or update the code.

Note that using Assembly removes several important security features of Solidity, which can make the code more insecure and more error-prone.

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L511-L513

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L581-L583

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L605-L607


##

## [NC-5] NOT USING THE NAMED RETURN VARIABLES ANYWHERE IN THE FUNCTION IS CONFUSING

Consider changing the variable to be an unnamed one

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PoolSelector.sol#L76-L79

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PoolSelector.sol#L50-L54

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L720

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L513

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L680

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L93

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L432

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L692

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L648

##

## [NC-6] NATSPEC COMMENTS SHOULD BE INCREASED IN CONTRACTS

It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation.
In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
(https://docs.soliditylang.org/en/v0.8.15/natspec-format.html)

### Recommendation
NatSpec comments should be increased in Contracts


##

## [NC-7] Use a more recent version of solidity

Use at least solidity version 0.8.17 

INSTANCE
ALL CONTRACTS

##

## [NC-8] For functions, follow Solidity standard naming conventions (internal function style rule)

Description
The bellow codes don’t follow Solidity’s standard naming convention,

internal and private functions and variables : the mixedCase format starting with an underscore (_mixedCase starting with an underscore)

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L680

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L687-L692

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L720

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L732

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L738

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L747

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L752

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L758

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedPool.sol#L279

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedPool.sol#L283

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedPool.sol#L288-L293

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L598-L602

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L618

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L625

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L633

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L643-L648

##

## [NC-9] Critical changes should use two-step procedure

Lack of two-step procedure for critical operations leaves them error-prone. Consider adding two-step procedure on the critical functions.

Consider adding a two-steps pattern on critical changes to avoid mistakenly transferring ownership of roles or critical functionalities to the wrong address

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedPool.sol#L236-L245

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessPool.sol#L66-L75

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PoolSelector.sol#L147-L151

##

## [NC-10] add a timelock to critical functions

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedPool.sol#L236-L245

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessPool.sol#L66-L75

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PoolSelector.sol#L147-L151

##

## [NC-11] No Same value input control

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PoolSelector.sol#L140-L144

##

## [NC-12] Use Fuzzing Test for math code bases

I recommend fuzzing testing in math code structures,

Recommendation:
Use should fuzzing test like Echidna.

Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.

https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05

##

## [NC-13] Project Upgrade and Stop Scenario should be

At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an " EMERGENCY STOP (CIRCUIT BREAKER) PATTERN ".

https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol






















#0 - Picodes

2023-06-14T07:06:53Z

Is it me or the 2 issues are use .call and do not use .call ?

#1 - c4-judge

2023-06-14T07:06:57Z

Picodes marked the issue as grade-c

#2 - sathishpic22

2023-07-04T12:56:24Z

@Picodes

I think the findings 1 is understood in wrong way.

My suggestion is payable(address).call should be avoided not normal .call

primary risks of using payable(address).call() is that it doesn't guarantee that the contract's payable function will be called successfully. This can lead to funds being lost or stuck in the contract.

This finding is accepted in many contests as LOW

#3 - c4-judge

2023-07-07T12:32:13Z

Picodes marked the issue as grade-b

#4 - Picodes

2023-07-07T12:33:05Z

Thanks for clarifying.

Awards

253.86 USDC - $253.86

Labels

bug
G (Gas Optimization)
grade-a
edited-by-warden
G-26

External Links

GAS OPTIMIZATIONS

All the gas optimizations are determined based opcodes and sample tests.

CountIssuesInstancesGas Saved
[G-1]Using bools for storage incurs overhead7119700
[G-2]Using storage instead of memory for structs/arrays saves gas48400
[G-3]External function calls should be cached outside the loop3PerCall (2100 gas)
[G-4]Avoid emitting storage variable when stack variable available4400
[G-5]Refactor the for loop to avoid unwanted variable creation inside the loop113
[G-6]Cache the state variable out side the loop1PerCall (100 gas)
[G-7]Caching global variables is more expensive than using the actual variable (use msg.sender instead of caching it)339
[G-8]Repack the state variable to avoid on extra slot12000

[G-1] Using bools for storage incurs overhead

All instances are not found by bot race

Results : INSTANCES 7, Saves 119700 GAS

// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27 Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing from false to true, after having been true in the past

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#LL54C4-L54C61

FILE: Breadcrumbs2023-06-stader/contracts/PermissionedNodeRegistry.sol

54: mapping(address => bool) public override permissionList;

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderOracle.sol#L18-L19

FILE: 2023-06-stader/contracts/StaderOracle.sol

18: bool public override erInspectionMode;
19: bool public override isPORFeedBasedERData;

44:  mapping(address => bool) public override isTrustedNode;
45:  mapping(bytes32 => bool) private nodeSubmissionKeys;

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/VaultProxy.sol#L10-L11

FILE: 2023-06-stader/contracts/VaultProxy.sol

10: bool public override isValidatorWithdrawalVault;
11: bool public override isInitialized;

[G-2] Using storage instead of memory for structs/arrays saves gas

All instances are not found by bot race

Results : INSTANCES 3, Saves 8400 GAS

When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct.

Using storage variables can potentially save gas by eliminating the cost of copying the struct from storage to memory

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L733

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionedNodeRegistry.sol#L739

Saves 4200 GAS

FILE: 2023-06-stader/contracts/PermissionedNodeRegistry.sol

- 733:  Validator memory validator = validatorRegistry[_validatorId];
+ 733:  Validator storage validator = validatorRegistry[_validatorId];


- 739:  Validator memory validator = validatorRegistry[_validatorId];
+ 739:  Validator storage validator = validatorRegistry[_validatorId];

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#LL692C9-L692C70

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L702

Saves 4200 GAS

FILE: 2023-06-stader/contracts/PermissionlessNodeRegistry.sol

- 692: Validator memory validator = validatorRegistry[_validatorId];
+ 692: Validator storage validator = validatorRegistry[_validatorId];

- 702: Validator memory validator = validatorRegistry[_validatorId];
+ 702: Validator storage validator = validatorRegistry[_validatorId];

[G-3] External function calls should be cached outside the loop

Results : INSTANCES 3, Saves 2100 GAS per call

Results : Saves Approximately 2100 gas for single call

When caching external call values, it's generally more efficient to perform the external call outside of any loops to avoid unnecessary redundant calls.

In terms of opcode savings, caching the external call result outside the loop can eliminate repeated opcodes such as CALL that would have been used for the external calls within the loop. This can reduce the overall gas consumption.

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderStakePoolsManager.sol#L231-L239

staderConfig.getStakedEthPerNode() external call should be cached outside the loop to avoid redundant calls saves at least 700 GAS for every iterations.


FILE: 2023-06-stader/contracts/StaderStakePoolsManager.sol

+  uint256 stakedEthPerNode=staderConfig.getStakedEthPerNode();
231: uint256 poolCount = poolIdArray.length;
232:        for (uint256 i = 0; i < poolCount; i++) {
233:            uint256 validatorToDeposit = selectedPoolCapacity[i];
235:            if (validatorToDeposit == 0) {
236:                continue;
237:            }
238:            address poolAddress = IPoolUtils(poolUtils).poolAddressById(poolIdArray[i]);
- 239:           uint256 poolDepositSize = staderConfig.getStakedEthPerNode() -
+ 239:           uint256 poolDepositSize = stakedEthPerNode -
240:                IPoolUtils(poolUtils).getCollateralETH(poolIdArray[i]);

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessPool.sol#L94-L99

INodeRegistry((staderConfig).getPermissionlessNodeRegistry()).POOL_ID() external call should be cached outside the loop to avoid redundant calls saves 1400 GAS for every iterations.

FILE: Breadcrumbs2023-06-stader/contracts/PermissionlessPool.sol

+ uint8 poolId= INodeRegistry((staderConfig).getPermissionlessNodeRegistry()).POOL_ID() ;
94: for (uint256 i; i < pubkeyCount; ) {
95:            address withdrawVault = IVaultFactory(vaultFactory).computeWithdrawVaultAddress(
- 96:                INodeRegistry((staderConfig).getPermissionlessNodeRegistry()).POOL_ID(),
+ 96:                poolId,
97:                _operatorId,
98:                _operatorTotalKeys + i
99:            );

[G-4] Avoid emitting storage variable when stack variable available

Results : INSTANCES 4, Saves 400 GAS

  • When accessing a storage variable, an opcode like SLOAD is used to read the value from storage into the stack. This incurs a gas cost based on the complexity of accessing storage and the amount of data being loaded

  • Stack variables, on the other hand, do not require the use of SLOAD or SSTORE opcodes. They are directly manipulated within the function's stack frame, which is a more efficient operation in terms of gas usage

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/Auction.sol#L147-L148

_duration stack variable should be used instead of storage variable duration saves 1 SLOD (100 gas )

FILE: 2023-06-stader/contracts/Auction.sol

147:  duration = _duration;
- 148:        emit AuctionDurationUpdated(duration);
+ 148:        emit AuctionDurationUpdated(_duration);

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L270-L271

_nextQueuedValidatorIndex stack variable should be used instead of storage variable nextQueuedValidatorIndex saves 1 SLOD (100 gas )

FILE: Breadcrumbs2023-06-stader/contracts/PermissionlessNodeRegistry.sol

270:    nextQueuedValidatorIndex = _nextQueuedValidatorIndex;
- 271:        emit UpdatedNextQueuedValidatorIndex(nextQueuedValidatorIndex);
+ 271:        emit UpdatedNextQueuedValidatorIndex(_nextQueuedValidatorIndex);

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/PermissionlessNodeRegistry.sol#L338-L339

_maxNonTerminalKeyPerOperator stack variable should be used instead of storage variable maxNonTerminalKeyPerOperator saves 1 SLOD (100 gas )

FILE: Breadcrumbs2023-06-stader/contracts/PermissionlessNodeRegistry.sol

338: maxNonTerminalKeyPerOperator = _maxNonTerminalKeyPerOperator;
- 339:        emit UpdatedMaxNonTerminalKeyPerOperator(maxNonTerminalKeyPerOperator);
+ 339:        emit UpdatedMaxNonTerminalKeyPerOperator(_maxNonTerminalKeyPerOperator);

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderOracle.sol#L535-L536

_erChangeLimit stack variable should be used instead of storage variable erChangeLimit saves 1 SLOD (100 gas )


FILE: Breadcrumbs2023-06-stader/contracts/StaderOracle.sol

535: erChangeLimit = _erChangeLimit;
- 536:         emit UpdatedERChangeLimit(erChangeLimit);
+ 536:         emit UpdatedERChangeLimit(_erChangeLimit);

[G-5] Refactor the for loop to avoid unwanted variable creation inside the loop

Results : INSTANCES 1, Saves 13 GAS per call

The variable pubkeyRoot is declared within each iteration of the for loop, but it is only used once. This results in unnecessary variable creation.This refactoring helps to optimize gas costs and code readability by reducing the number of variable declarations within the loop, especially for variables that are only used once.

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderOracle.sol#L485-L490

UtilLib.getPubkeyRoot(_mapd.sortedPubkeys[i]) can apply value directly instead of cache with local variable. For every iteration can saves 15-20 GAS

FILE: 2023-06-stader/contracts/StaderOracle.sol

485: for (uint256 i; i < keyCount; ) {
- 486:                 bytes32 pubkeyRoot = UtilLib.getPubkeyRoot(_mapd.sortedPubkeys[i]);
- 487:                missedAttestationPenalty[pubkeyRoot]++;
+ 487:                missedAttestationPenalty[UtilLib.getPubkeyRoot(_mapd.sortedPubkeys[i])]++;

[G-6] Cache the state variable out side the loop

Results : INSTANCES 1, Saves 100 GAS per call

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/Penalty.sol#L100-L101

staderConfig value should be cached with local stack variable outside the loops. For every iteration this saves 1 SLOD 100 GAS

FILE: 2023-06-stader/contracts/Penalty.sol
+ IStaderConfig public _staderConfig = staderConfig ;
100: for (uint256 i; i < reportedValidatorCount; ) {
101:            if (UtilLib.getValidatorSettleStatus(_pubkey[i], _staderConfig)) {

[G-7] Caching global variables is more expensive than using the actual variable (use msg.sender instead of caching it)

Results : INSTANCES 3, Saves 39 GAS

Caching global variable consumes extra 13 gas

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/SDCollateral.sol#L59

msg.sender cache global variables is more expensive. This cost extra 13 GAS

FILE: Breadcrumbs2023-06-stader/contracts/SDCollateral.sol

INSTANCE 1: 

- 59:  address operator = msg.sender;
- 60:        uint256 opSDBalance = operatorSDBalance[operator];
+ 60:        uint256 opSDBalance = operatorSDBalance[msg.sender];
61:
- 62:        if (opSDBalance < getOperatorWithdrawThreshold(operator) + _requestedSD) {
+ 62:        if (opSDBalance < getOperatorWithdrawThreshold(msg.sender) + _requestedSD) {
63:            revert InsufficientSDToWithdraw(opSDBalance);
64:        }
- 65:        operatorSDBalance[operator] -= _requestedSD;
+ 65:        operatorSDBalance[msg.sender] -= _requestedSD;
66:
67:        // cannot use safeERC20 as this contract is an upgradeable contract
- 68:        if (!IERC20(staderConfig.getStaderToken()).transfer(payable(operator), _requestedSD)) {
+ 68:        if (!IERC20(staderConfig.getStaderToken()).transfer(payable(msg.sender), _requestedSD)) {
69:            revert SDTransferFailed();
70:        }
71:
- 72:        emit SDWithdrawn(operator, _requestedSD);
+ 72:        emit SDWithdrawn(msg.sender, _requestedSD);
73:    }

INSTANCE: 2:

- 44:  address operator = msg.sender;
- 45:        operatorSDBalance[operator] += _sdAmount;
+ 45:        operatorSDBalance[msg.sender] += _sdAmount;
46:
- 47:        if (!IERC20(staderConfig.getStaderToken()).transferFrom(operator, address(this), _sdAmount)) {
+ 47:        if (!IERC20(staderConfig.getStaderToken()).transferFrom(msg.sender, address(this), _sdAmount)) {
48:            revert SDTransferFailed();
49:        }
50:
- 51:        emit SDDeposited(operator, _sdAmount);
+ 51:        emit SDDeposited(msg.sender, _sdAmount);

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/SocializingPool.sol#L113

msg.sender cache global variables is more expensive. This cost extra 13 GAS

FILE: Breadcrumbs2023-06-stader/contracts/SocializingPool.sol

- 113: address operator = msg.sender;
- 114:        (uint256 totalAmountSD, uint256 totalAmountETH) = _claim(_index, operator, _amountSD, _amountETH, _merkleProof);
+ 114:        (uint256 totalAmountSD, uint256 totalAmountETH) = _claim(_index, msg.sender, _amountSD, _amountETH, _merkleProof);

- 116:        address operatorRewardsAddr = UtilLib.getOperatorRewardAddress(operator, staderConfig);
+ 116:        address operatorRewardsAddr = UtilLib.getOperatorRewardAddress(msg.sender, staderConfig);

[G-8] Repack the state variable to avoid on extra slot

Results : INSTANCES 1, Saves 2000 GAS

If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables can also be cheaper

_erChangeLimit > ER_CHANGE_MAX_BPS as per this condition check the erChangeLimit not exceeds 10000. So uint64 alone more than enough instead of uint256

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderOracle.sol#L18-L28

Saves 1 slot and 2000 gas

FILE: 2023-06-stader/contracts/StaderOracle.sol

18:    bool public override erInspectionMode;
19:    bool public override isPORFeedBasedERData;
20:    SDPriceData public lastReportedSDPriceData;
+ 28:    uint64 public override erChangeLimit;
21:    IStaderConfig public override staderConfig;
22:    ExchangeRate public inspectionModeExchangeRate;
23:    ExchangeRate public exchangeRate;
24:    ValidatorStats public validatorStats;
25:
26:    uint256 public constant MAX_ER_UPDATE_FREQUENCY = 7200 * 7; // 7 days
27:    uint256 public constant ER_CHANGE_MAX_BPS = 10000;
- 28:    uint256 public override erChangeLimit;

#0 - c4-judge

2023-06-14T05:55:10Z

Picodes marked the issue as grade-b

#1 - sathishpic22

2023-07-04T12:51:04Z

@Picodes

I think my reports may save more than some of the grade a reports. Yes other wardens sent more findings than me still those mostly informational and less gas optimized findings. Even some wardens sent wrong instances still they got grade a.

https://github.com/code-423n4/2022-06-stader-findings/issues/413, https://github.com/code-423n4/2022-06-stader-findings/issues/382, https://github.com/code-423n4/2022-06-stader-findings/issues/378

Yes, My title suggests that some these findings are already found in bot race. But i have checked all my findings this instances are not found in bot race. In other wardens case the missing instances are accepted i don't what is wrong in my case. All other wardens also sent missing instances.

I have only focused more impact findings than less impact findings. The point is how much gas saved that particular reports instead of the findings count. You can check my instances.

[G-1] | Using bools for storage incurs overhead | 7 | 119700 [G-2] Using storage instead of memory for structs/arrays saves gas - 8400 gas [G-3] External function calls should be cached outside the loop - 2100 gas [G-4] Avoid emitting storage variable when stack variable available - 400 gas [G-6] Cache the state variable out side the loop - 100 gas [G-8] Repack the state variable to avoid on extra slot - 2000 gas

If ignore bool findings still my reports saves - 12000 gas which is higher than some of the grade a reports

#2 - sathishpic22

2023-07-04T14:45:52Z

Even JCN saved more gas via Structs can be packed into fewer storage slots, State variables can be packed into fewer storage slots saved 38000 gas. The problem is he simply down casting from uint256 to some other types without sufficiently proving that down casting not affect the protocol. In that way anyone can simply downcast and save more SLOTs.

If we down casting any state variable warden must prove that down casting not affect protocols.

Also half of his findings are missing instances in bot races. This is my thoughts . If anything wrong or any comments about my findings I am ready to accept yours comments.

Thank you

#3 - sathishpic22

2023-07-07T09:15:53Z

@Picodes

#4 - Picodes

2023-07-07T09:33:35Z

@sathishpic22 don't forget to reference your comments during Q&As in the discussion as the judge can't properly keep track of what's happening on the 400+ issues otherwise. My apologies for missing your comments.

I still think your report is quite far from the best reports in terms of formatting and originality of the findings. However, you are right that there is no out of scope issues and it adds some value (aside from the bool thing which in my opinion is not very interesting)

#5 - c4-judge

2023-07-07T09:33:40Z

Picodes marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter