Stader Labs - josephdara'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: 22/75

Findings: 2

Award: $704.54

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

Awards

102.2712 USDC - $102.27

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-383

External Links

Lines of code

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

Vulnerability details

Impact

The SocializingPool.sol imports and inherits the Openzeppelin Pausable contract

import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; import '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; import '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol'; import '@openzeppelin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.sol'; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; contract SocializingPool is ISocializingPool, Initializable, AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable {

But there is no function in the contract that implements the pausabiility functionality. Even though some functions implement the modifier.

function claim( uint256[] calldata _index, uint256[] calldata _amountSD, uint256[] calldata _amountETH, bytes32[][] calldata _merkleProof ) external override nonReentrant whenNotPaused {

Therefore this contract cannot be paused

Proof of Concept

Below is a snapshot of the PausableUpgradeable, and it implements no public/external pause and unpause functions

abstract contract PausableUpgradeable is Initializable, ContextUpgradeable { /** * @dev Emitted when the pause is triggered by `account`. */ event Paused(address account); /** * @dev Emitted when the pause is lifted by `account`. */ event Unpaused(address account); bool private _paused; /** * @dev Initializes the contract in unpaused state. */ function __Pausable_init() internal onlyInitializing { __Pausable_init_unchained(); } function __Pausable_init_unchained() internal onlyInitializing { _paused = false; } /** * @dev Modifier to make a function callable only when the contract is not paused. * * Requirements: * * - The contract must not be paused. */ modifier whenNotPaused() { _requireNotPaused(); _; } /** * @dev Modifier to make a function callable only when the contract is paused. * * Requirements: * * - The contract must be paused. */ modifier whenPaused() { _requirePaused(); _; } /** * @dev Returns true if the contract is paused, and false otherwise. */ function paused() public view virtual returns (bool) { return _paused; } /** * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { require(!paused(), "Pausable: paused"); } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { require(paused(), "Pausable: not paused"); } /** * @dev Triggers stopped state. * * Requirements: * * - The contract must not be paused. */ function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } /** * @dev Returns to normal state. * * Requirements: * * - The contract must be paused. */ function _unpause() internal virtual whenPaused { _paused = false; emit Unpaused(_msgSender()); } /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ uint256[49] private __gap; }

Tools Used

Manual review

Implement an access-controlled pause and unpause function

Assessed type

Other

#0 - c4-judge

2023-06-10T10:44:36Z

Picodes marked the issue as duplicate of #383

#1 - c4-judge

2023-06-14T19:14:30Z

Picodes changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-07-02T09:44:18Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: josephdara

Also found by: Aymen0909, ChrisTina, NoamYakov, bin2chen, ksk2345

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
satisfactory
selected for report
sponsor acknowledged
M-10

Awards

602.27 USDC - $602.27

External Links

Lines of code

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

Vulnerability details

Impact

The owner variable in the VaultProxy.sol is going to be zero because of a bug in staderConfig. The intialize function in staderConfig grants th admin role to an admin passed in via the native _grantRole(), but does not update the mapping that getAdmin reads from.

    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);

        //@audit-issue Admin is zero on initialize
        //address(0) is going to  be returned, wrt stader.initialize
        owner = staderConfig.getAdmin();
    }

https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderConfig.sol#L85-L103 https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderConfig.sol#L361-L363

The intialization of the vaults would not revert and all functions restricted to the owner would be inaccessible unless function updateAdmin is called. https://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/StaderConfig.sol#L176-L183

The update admin does not revoke functions for the current admin in the AccessControl contract too because the address returned is address zero

Proof of Concept


pragma solidity 0.8.16;

import '../../contracts/library/UtilLib.sol';

import '../../contracts/StaderConfig.sol';
import '../../contracts/VaultProxy.sol';
import '../../contracts/ValidatorWithdrawalVault.sol';
import '../../contracts/OperatorRewardsCollector.sol';

import './mocks/PoolUtilsMock.sol';
import './mocks/PenaltyMockForVault.sol';
import './mocks/SDCollateralMock.sol';
import './mocks/StakePoolManagerMock.sol';

import 'forge-std/Test.sol';
import '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';
import '@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol';

contract ValidatorWithdrawalVaultTest is Test {
    address staderAdmin;
    address staderManager;
    address staderTreasury;
    PoolUtilsMock poolUtils;

    uint8 poolId;
    uint256 validatorId;

    StaderConfig staderConfig;
    VaultProxy withdrawVault;
    OperatorRewardsCollector operatorRC;

    function setUp() public {
        poolId = 1;
        validatorId = 1;

        staderAdmin = vm.addr(100);
        staderManager = vm.addr(101);
        address ethDepositAddr = vm.addr(102);

        ProxyAdmin proxyAdmin = new ProxyAdmin();

        StaderConfig configImpl = new StaderConfig();
        TransparentUpgradeableProxy configProxy = new TransparentUpgradeableProxy(
            address(configImpl),
            address(proxyAdmin),
            ''
        );
        staderConfig = StaderConfig(address(configProxy));
        staderConfig.initialize(staderAdmin, ethDepositAddr);

        OperatorRewardsCollector operatorRCImpl = new OperatorRewardsCollector();
        TransparentUpgradeableProxy operatorRCProxy = new TransparentUpgradeableProxy(
            address(operatorRCImpl),
            address(proxyAdmin),
            ''
        );
        operatorRC = OperatorRewardsCollector(address(operatorRCProxy));
        operatorRC.initialize(staderAdmin, address(staderConfig));

        poolUtils = new PoolUtilsMock(address(staderConfig));
        PenaltyMockForVault penaltyContract = new PenaltyMockForVault();
        SDCollateralMock sdCollateral = new SDCollateralMock();
        ValidatorWithdrawalVault withdrawVaultImpl = new ValidatorWithdrawalVault();

        vm.startPrank(staderAdmin);
        staderConfig.updatePoolUtils(address(poolUtils));
        staderConfig.updatePenaltyContract(address(penaltyContract));
        staderConfig.updateSDCollateral(address(sdCollateral));
        staderConfig.updateOperatorRewardsCollector(address(operatorRC));
        staderConfig.updateValidatorWithdrawalVaultImplementation(address(withdrawVaultImpl));

        vm.stopPrank();

        withdrawVault = new VaultProxy();
        withdrawVault.initialise(true, poolId, validatorId, address(staderConfig));
    }

    function testAdminRights() public {
       vm.startPrank(staderAdmin);
      staderConfig.grantRole(staderConfig.MANAGER(), staderManager); 
      vm.stopPrank();
    }
    function testFetchAdminAndVaultOwner() public{
        address _admin = staderConfig.getAdmin();
        assertEq(_admin, address(0));
        address _owner = withdrawVault.owner();
        assertEq(_owner, address(0));
    }
    
}

Tools Used

Foundry, manual review

StaderConfig.initialize should add setAccount(ADMIN, _admin); a last line

Assessed type

Access Control

#0 - c4-judge

2023-06-10T13:52:26Z

Picodes marked the issue as duplicate of #171

#1 - c4-judge

2023-07-02T12:45:27Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2023-07-02T12:46:34Z

Picodes marked the issue as selected for report

#3 - c4-judge

2023-07-03T12:11:08Z

Picodes changed the severity to 2 (Med Risk)

#4 - sanjay-staderlabs

2023-07-04T05:32:03Z

We are handling it as a part of deployment process, updateAdmin(address _admin) is called during deployment process.

#5 - c4-sponsor

2023-07-04T05:32:10Z

sanjay-staderlabs marked the issue as disagree with severity

#6 - c4-sponsor

2023-07-04T05:56:27Z

sanjay-staderlabs marked the issue as sponsor acknowledged

#7 - sanjay-staderlabs

2023-07-06T03:36:10Z

@Picodes can you please check this, It think it should be part for QA Thanks

#8 - Picodes

2023-07-06T14:32:28Z

Considering the audited version of the code, especially in combination with https://github.com/code-423n4/2022-06-stader-findings/issues/390 I still think this qualifies for Medium severity

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