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!
pragma solidity ^0.8.0;
contract ReadOnlyReentrancyGame {
mapping(address => uint256) public balances;
bool public prizeClaimed = false;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function getPrizeEligibility() public view returns (bool) {
// Checks if the user has a balance and the prize is not yet claimed
return (balances[msg.sender] >= 1 ether && !prizeClaimed);
}
function claimPrize() public {
require(getPrizeEligibility(), "Not eligible for prize");
prizeClaimed = true;
(bool success, ) = msg.sender.call{value: 1 ether}("");
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?
function claimPrize() public {
require(balances[msg.sender] >= 1 ether, "Insufficient balance");
require(!prizeClaimed, "Prize already claimed");
// Fix: Set prizeClaimed to true immediately to prevent reentrancy
prizeClaimed = true;
(bool success, ) = msg.sender.call{value: 1 ether}("");
require(success, "Transfer failed");
}