A malicious player who wins a game created with Ether, can cause another player to pay a high gas cost by using a return bomb when being transferred winnngs.
Gas griefing is an attack whereby an attacker causes a unsuspecting user to pay a higher gas fee.
In the RockPaperScissors
contract, the winner of turn and/or game is decided by calling RockPaperScissors::revealMove
. For games created by createGameWithEth
, the revealMove
function follows this execution flow:
Runs checks to make sure the commited move matches the declared move
After checks pass, calls RockPaperScissors::_determineWinner
_determineWinner
decidies who won the turn.
If this is the last turn and there is a winner then RockPaperScissors::_finishGame
is called.
_finishGame
calculates the prize and fees and then sends the prize winnings to the Winner by a low-level call.
The Code Issue
By default, the low-level '.call' automatically loads any returned data into the contracts memory regardless of it being used in the contract or not. The operation of loading data into memory is expensive. The more data that is returned, the higher the associated gas cost.
The low-level call to send funds is in RockPaperScissors::_finishGame#491
:
As per the execution steps, the last player who calls the revealMove
function - on the last turn - is responsible for the final gas cost associated with transferring winnings.
A malicious player can craft a contract that contains a malicious fallback function that returns a large amount of data when triggered by receiving the payment. Even though the data is not used, it is temporarily loaded into memory.
Requirements for Successful exploitation
In order to successfully be executed
The target player must be the last one to call RockPaperScissors::revealMove
The malicious player needs to be the winner of the entire game. (not turn)
There are no funds lost therefore the severity of this vulnerability is Medium. The impact of successful exploitation to gas grief an unsuspecting player:
Target player has to pay a much higher gas fee
A much higher financial investment that anticipated. A player goes into play the game with limited funds, and now has a larger % drop in funds
User Frustration resulting in lost trust to the protocol: a user who has to pay higher fees than expected without understanding why, will be very confused and frustrated; which will be directed towards the protocol and result in loss of trust and a bad reputation.
Manual Review
Foundry
Description
In order to get gas consumption, 2 contracts are used to simulate the 2 players - "legitPlayer" and "maliciousPlayer". The gas consumption is measured starting from when the vulnerable player calls revealMove
"legitPlayer" executes createGameWithEth
"maliciousPlayer" joins the game and plays.
On the last turn, after "maliciousPlayer" commits move and immediately calls reveal - ensuring that "legitPlayer" will be the last to call revealMove
and be griefed
"legitPlayer" calls revealMove
and has lost and triggers the _finishGame
functionality
Once the funds are sent, the "maliciousPlayer" fallback function is hit and it returns 1MB of data.
The data is automatically loaded into the contract but not used. Regardless, the operation consumes gas which "legitPlayer" is now forced to pay.
Execution
To prove the vulnerability I have created a runnable PoC. To execute run forge test --mt testGasGriefPlayer -vvv
Code
In order to mitigate against this:
Recommended: The contract should use the "pull" method, rather than "push" method for funds. This means that instead of the contract pushing funds to the winner, the winner instead needs to withdraw their winnings (i.e pull)
The contract can implement a transaction gas limit to protect legit players against malicious players
The ExcessivelySafeCall
library can be implemented to protect against return bombs.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.