Read-only reentrancy exploits functions that only perform "view" operations (i.e., don’t directly change state) but still affect the contract's behavior based on inconsistent state.
While such functions don’t modify storage, they may still provide inaccurate or exploitable information if they rely on external contract calls that can reenter and manipulate state elsewhere.
Game
Think about what would happen if msg.sender is a contract that re-enters getPrizeEligibility via a fallback function during the call to claimPrize
// SPDX-License-Identifier: MIT// Open me in VSCode and really think before opening the hints!// Add @audit tags wherever suspicious// Go to the solidity docs to complete missing knowledge of what's happening here// Solve by drafting a fix!pragmasolidity ^0.8.0;contract ReadOnlyReentrancyGame {mapping(address=>uint256) public balances;boolpublic prizeClaimed =false;functiondeposit() publicpayable { balances[msg.sender] += msg.value; }functiongetPrizeEligibility() publicviewreturns (bool) {// Checks if the user has a balance and the prize is not yet claimedreturn (balances[msg.sender] >=1ether&&!prizeClaimed); }functionclaimPrize() public {require(getPrizeEligibility(),"Not eligible for prize"); prizeClaimed =true; (bool success, ) = msg.sender.call{value:1ether}("");require(success,"Transfer failed"); }}
Notice that getPrizeEligibility is a view function, meaning it doesn’t directly modify state.
However, read-only reentrancy can happen if msg.sender can re-enter the view function during claimPrize to get outdated state information
Think about whether balances[msg.sender] is updated before the external call. If dynamicPayout is reentered, would the balance deduction prevent repeated calls, or is there a way to exploit this order?
functionclaimPrize() public {require(balances[msg.sender] >=1ether,"Insufficient balance");require(!prizeClaimed,"Prize already claimed");// Fix: Set prizeClaimed to true immediately to prevent reentrancy prizeClaimed =true; (bool success, ) = msg.sender.call{value:1ether}("");require(success,"Transfer failed");}