TwentyOne

First Flight #29
Beginner FriendlyGameFiFoundrySolidity
100 EXP
View results
Submission Details
Severity: medium
Invalid

Vulnerable Prize Transfer: .transfer() Method Risks Breaking with Complex Recipient Contracts

Summary

transfer()in TwentyOne::EndGame() is not recommend, as its fixed gas allocation might cause failure for recipients with complex logic in their receive or fallback functions.

Vulnerability Details

If the recipient is a smart contract with complex logic in its receive/fallback functions, the transfer will revert. The provided test code demonstrates this by creating a FakeRecipient contract that can cause an out-of-gas error.

contract FakeRecipient {
uint256 sum;
receive() external payable {
for (uint256 i = 0; i < 1000; i++) {
sum += i;
}
}
}
contract TwentyOne is Test {
TwentyOne public twentyOne;
FakeRecipient fakeRecipient;
function setUp() public {
twentyOne = new TwentyOne();
fakeRecipient = new FakeRecipient();
vm.deal(address(twentyOne), 10 ether);
vm.deal(address(fakeRecipient), 10 ether);
}
function test_Call_FakeRecipientWins() public {
vm.startPrank(address(fakeRecipient)); // Start acting as fakeRecipient
twentyOne.startGame{value: 1 ether}();
// Mock the dealer's behavior to ensure fakeRecipient wins
// Simulate dealer cards by manipulating state
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("dealersHand(address)", address(fakeRecipient)),
abi.encode(18) // Dealer's hand total is 18
);
uint256 initialPlayerBalance = address(fakeRecipient).balance;
// Player calls to compare hands
twentyOne.call();
// Check if the player's balance increased (prize payout)
uint256 finalPlayerBalance = address(fakeRecipient).balance;
assertGt(finalPlayerBalance, initialPlayerBalance);
vm.stopPrank();
}
}

Impact

Potential failure of prize transfers to smart contract recipients with complex receive or fallback functions.

Tools Used

Manual Review

Recommendations

Consider using .call{value: amount} instead for greater flexibility:

if (playerWon) {
+ bool success,) = payable(player).call{value: 2 ether}("");
+ require(success, "fail to call");
- payable(player).transfer(2 ether); // Transfer the prize to the player
emit FeeWithdrawn(player, 2 ether); // Emit the prize withdrawal event
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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