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: 22/75
Findings: 2
Award: $704.54
π Selected for report: 1
π Solo Findings: 0
102.2712 USDC - $102.27
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
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; }
Manual review
Implement an access-controlled pause and unpause function
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
602.27 USDC - $602.27
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
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)); } }
Foundry, manual review
StaderConfig.initialize
should add setAccount(ADMIN, _admin);
a last line
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