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
Rank: 30/75
Findings: 1
Award: $210.49
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: RaymondFam
Also found by: 0xWaitress, 0xhacksmithh, ChrisTina, DadeKuma, LaScaloneta, Rolezn, SAAJ, Sathish9098, T1MOH, bin2chen, btk, catellatech, ernestognw, fatherOfBlocks, hals, hunter_w3b, jaraxxus, matrix_0wl, mgf15, naman1778, niser93, solsaver, turvy_fuzz
210.4947 USDC - $210.49
During the audit, 9 low and 4 non-critical issues were found.
38: "@openzeppelin/contracts-upgradeable": "^4.8.0-rc.1",
VULNERABILITY VULNERABLE VERSION L Missing Authorization >=4.3.0 <4.9.1 L Denial of Service (DoS) >=3.2.0 <4.8.3 M Improper Input Validation >=4.3.0 <4.8.3 M Incorrect Calculation >=4.8.0 <4.8.2 M Incorrect Calculation >=4.8.0 <4.8.2
https://security.snyk.io/package/npm/@openzeppelin%2Fcontracts-upgradeable
Upgrade OZ to version 4.9.0 or higher
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, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
NatSpec comments should be increased in contracts. Follow your our best practice like PermissionedNodeRegistry.sol.
In the following fuction OperatorRewardsCollector.depositFor its no checks there, we recomended put some checks here.
40: function depositFor(address _receiver) external payable { // @audit add the checks here require(_receiver != address(0) && msg.value > 0, "add valid params"); <---------- balances[_receiver] += msg.value; emit DepositedFor(msg.sender, _receiver, msg.value); }
In the contract Penalty.setAdditionalPenaltyAmount
the onlyManagerRole can abuse the penalties by not specifying what will be the maximum amount per extra penalty.
We recommend making a constant variable with the value of max-amount-per-penalty and then if the onlyManagerRole wants to have a bit more flexibility, depending on the possible infraction he can place a require that specifies that the amount can be less than or equal to max-amount-per-penalty.
53: function setAdditionalPenaltyAmount(bytes calldata _pubkey, uint256 _amount) external override { // @audit how could look the change require(_amount <= max-amount-per-penalty); <---------- UtilLib.onlyManagerRole(msg.sender, staderConfig); bytes32 pubkeyRoot = UtilLib.getPubkeyRoot(_pubkey); additionalPenaltyAmount[pubkeyRoot] += _amount; emit UpdatedAdditionalPenaltyAmount(_pubkey, _amount); }
The protocol specifies that it is a multi-pool architecture for node operations, designed for decentralization and scalability. However, it utilizes proxies in the contracts, and the administrators have significant control over the project, specifically being able to make changes to all contracts from StaderConfig.sol
. This introduces risks ranging from potential failures and censorship to lack of transparency and security. When evaluating and using protocols in this environment, it is crucial to consider these risks and seek decentralized and resilient solutions that promote user security, transparency, and freedom as the project evolves.
Stader Labs ETHx is a multi pool architecture for node operations, designed for `decentralization`, scalability, and resilience.
The contract owner or single user with a role is not prevented from renouncing the role/ownership while the contract is paused, which would cause any user assets stored in the protocol, to be locked indefinitely.
PermissionlessNodeRegistry.sol 468: function pause() external override { UtilLib.onlyManagerRole(msg.sender, staderConfig); _pause(); }
In the initialize functions it is not specified in the documentation if the _admin will be an EOA or a contract.
Consider improving the docstrings to reflect the exact intended behaviour, and using Address.isContract
function from OpenZeppelin’s library to detect if an address is effectively a contract.
We have noticed that the contracts PermissionedPool.sol
and PermissionlessPool
implements many type conversions from uint256 to uint64. We recommend using the OpenZeppelin SafeCast library to make the project more robust and take advantage of the gas optimizations and best practices provided by OpenZeppelin.
Arrays without the pop operation in Solidity can lead to inefficient memory management and increase the likelihood of out-of-gas errors.
PermissionedNodeRegistry.sol 174: validatorIdsByOperatorId[operatorId].push(nextValidatorId); PermissionlessNodeRegistry.sol 160: validatorIdsByOperatorId[operatorId].push(nextValidatorId); PoolUtils.sol 44: poolIdArray.push(_poolId);
In the VaultProxy.sol
contract we recommend refactoring the VaultProxy.initialise
function in order to greatly benefit development and code understanding by the reviewer.
Function VaultProxy.initialise
should be renamed to function VaultProxyInitialize()
to better represent what it does.
// @audit refactorize the function with --> VaultProxyInitialize() 21: 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(); }
In the contracts Auction.sol
, ETHx.sol
, PermissionedNodeRegistry.sol
, PermissionedPool.sol
, PermissionlessNodeRegistry.sol
, PermissionlessPool.sol
, PoolSelector.sol
, PoolUtils.sol
, StaderConfig.sol
, StaderConfig.sol
, there are many constanst
used in the system. It is recommended to put the most used ones in one file (for example Constants.sol
) and use inheritance to access these values.
This helps with readability and easier maintenance for future changes. It also helps with any issues, as some of these hard-coded contracts are admin contracts.
Constants.sol
Use and import these files in contracts that require access to these values. This is just a suggestion, in some use cases this may result in higher gas usage in the distribution.
We strongly recommend implementing the constanst variables in a separate file. This will help you avoid errors and confusion in your project, and maintain a more organized and readable code structure. Following these good programming and organizational practices will help you build a solid and scalable project, which will be easier to maintain and update in the future.
In Solidity, any function defined in a non-abstract contract must have a complete implementation that specifies what to do when that function is called. In the contracts VaultProxy.sol
, NodeELRewardVault.sol
there are constructors that do not emit nothing.
constructor() {}
contracts/StaderInsuranceFund.sol // @audit Consider emit something like the contract `StaderInsuranceFund` or simply remove the code block. 22: constructor() { _disableInitializers(); }
VaultProxy.updateOwner
add one more parameter in the event to be able to continue tracing on chain has been its previous owner.
#0 - c4-judge
2023-06-14T06:10:04Z
Picodes marked the issue as grade-a
#1 - c4-sponsor
2023-06-21T13:12:56Z
manoj9april marked the issue as sponsor acknowledged