diff --git a/src/Mailbomb.sol b/src/Mailbomb.sol index 6deebc1..b0d9565 100644 --- a/src/Mailbomb.sol +++ b/src/Mailbomb.sol @@ -32,12 +32,6 @@ contract Mailbomb is ERC1155, Owned { // Admin // ========================================================================= - /// Withdraw funds to contract owner - function withdraw() external onlyOwner { - uint256 balance = address(this).balance; - payable(msg.sender).transfer(balance); - } - /// Set metadata URI for all BOMB (token 1) /// @param _baseURI IPFS hash or URL to retrieve JSON metadata function setBaseURI(string calldata _baseURI) external onlyOwner { diff --git a/src/Main.sol b/src/Main.sol index 980ba0a..5e79859 100644 --- a/src/Main.sol +++ b/src/Main.sol @@ -99,7 +99,8 @@ contract Main is Owned { /// Withdraw funds to contract owner function withdraw() external onlyOwner { uint256 balance = address(this).balance; - payable(msg.sender).transfer(balance); + (bool success, ) = payable(msg.sender).call{value: balance}(""); + require(success, "failed to withdraw"); } /// Set price per BOOMR @@ -134,7 +135,7 @@ contract Main is Owned { /// The game stops; no more bombing/killing. Survivors make it to the next round. modifier missionNotCompleted { require( - unaboomer.totalKillCount() <= (unaboomer.MAX_SUPPLY() - unaboomer.SURVIVOR_COUNT()), + unaboomer.totalKillCount() < (unaboomer.MAX_SUPPLY() - unaboomer.MAX_SURVIVOR_COUNT()), "mission already completed" ); _; @@ -171,8 +172,8 @@ contract Main is Owned { /// Get BOOMR token survivor count /// @return survivorCount Maximum amount of BOOMR survivor tokens that can ever exist - function unaboomerSurvivorCount() public view returns (uint256) { - return unaboomer.SURVIVOR_COUNT(); + function unaboomerMaxSurvivorCount() public view returns (uint256) { + return unaboomer.MAX_SURVIVOR_COUNT(); } /// Get BOMB token balance of wallet diff --git a/src/Unaboomer.sol b/src/Unaboomer.sol index d71bf3e..3e443b5 100644 --- a/src/Unaboomer.sol +++ b/src/Unaboomer.sol @@ -15,7 +15,7 @@ Each Unaboomer is a unique, dynamically generated pixel avatar in the likeness of the real-life Unabomber, Theodore Kaczynski. Unaboomers can be "killed" by other players by "sending" (burning) mailbombs. When Unaboomers are killed their corresponding image is replaced with an explosion, rendering it worthless as any -rarity associated with it ceases to exist. The game stops when SURVIVOR_COUNT +rarity associated with it ceases to exist. The game stops when MAX_SURVIVOR_COUNT threshold is breached. The surviving players (any address which holds an "alive" Unaboomer) will advance to the next round of gameplay. @dev All contract functions regarding token burning and minting are limited to @@ -29,7 +29,7 @@ contract Unaboomer is ERC721, Owned { /// Maximum supply of BOOMR tokens uint256 public constant MAX_SUPPLY = 10000; /// Maximum amount of survivors remaining to advance to the next round - uint256 public constant SURVIVOR_COUNT = 1000; + uint256 public constant MAX_SURVIVOR_COUNT = 1000; /// The total amount of Unaboomers who have been killed during the game uint256 public totalKillCount; /// Number of tokens minted (total supply) @@ -47,12 +47,6 @@ contract Unaboomer is ERC721, Owned { // Admin // ========================================================================= - /// Withdraw funds to contract owner - function withdraw() external onlyOwner { - uint256 balance = address(this).balance; - payable(msg.sender).transfer(balance); - } - /// Set metadata URI for living Unaboomer tokens /// @param _baseURI IPFS hash or URL to retrieve JSON metadata for living Unaboomer tokens function setAliveURI(string calldata _baseURI) external onlyOwner { diff --git a/test/Unaboomer.t.sol b/test/Unaboomer.t.sol index b8e2bc3..c7f91f3 100644 --- a/test/Unaboomer.t.sol +++ b/test/Unaboomer.t.sol @@ -7,11 +7,15 @@ import {Unaboomer} from "../src/Unaboomer.sol"; import {Mailbomb} from "../src/Mailbomb.sol"; contract UnaboomerTest is Test { + using stdStorage for StdStorage; + Main public main; Unaboomer public boomr; Mailbomb public bomb; uint256 unaboomerPrice; uint256 bombPrice; + address victim = address(1); + address killer = address(2); function setUp() public { main = new Main(); @@ -42,8 +46,6 @@ contract UnaboomerTest is Test { // ensure killing increments leaderboard function testLeaderboard() public { - address victim = address(1); - address killer = address(2); uint256 amt = 20; hoax(victim); main.radicalizeBoomers{value: unaboomerPrice * amt}(amt); @@ -60,21 +62,112 @@ contract UnaboomerTest is Test { } // ensure killing toggles URI + function testURIToggling() public { + boomr.setAliveURI('ipfs://alive/'); + boomr.setDeadURI('ipfs://dead/'); + startHoax(victim); + main.radicalizeBoomers{value: unaboomerPrice * 1}(1); + assertEq(boomr.tokenURI(1), 'ipfs://alive/1.json'); + main.sendBombs(1); + assertEq(boomr.tokenURI(1), 'ipfs://dead/1.json'); + } + // ensure sending bombs burns bombs + function testBombBurning() public { + hoax(victim); + main.radicalizeBoomers{value: unaboomerPrice * 20}(20); + startHoax(killer); + main.assembleBombs{value: bombPrice * 20}(20); + assertEq(main.bombBalance(killer), 20); + assertEq(main.bombsExploded(), 0); + main.sendBombs(5); + assertEq(main.bombBalance(killer), 15); + assertEq(main.bombsExploded(), 5); + } + // ensure supply limits enforced - // ensure survivor limit enforced - // ensure only owners can withdraw funds - // ensure withdraw function actuall works - + function testMaximumSupply() public { + uint256 maxSupply = main.unaboomerMaxSupply(); + uint256 slot = stdstore + .target(address(boomr)) + .sig("minted()") + .find(); + bytes32 loc = bytes32(slot); + bytes32 mockedCurrentTokenId = bytes32(abi.encode(maxSupply - 1)); + vm.store(address(boomr), loc, mockedCurrentTokenId); + assertEq(main.unaboomerSupply(), (maxSupply - 1)); + startHoax(victim); + main.radicalizeBoomers{value: unaboomerPrice}(1); + vm.expectRevert(bytes("supply reached")); + main.radicalizeBoomers{value: unaboomerPrice}(1); + } + + // ensure survivor limit enforced and actions halted + function testSurvivors() public { + uint256 maxSupply = main.unaboomerMaxSupply(); + uint256 maxSurvivorCount = main.unaboomerMaxSurvivorCount(); + uint256 slot = stdstore + .target(address(boomr)) + .sig("minted()") + .find(); + bytes32 loc = bytes32(slot); + bytes32 a = bytes32(abi.encode(maxSupply)); // 10k + vm.store(address(boomr), loc, a); + slot = stdstore + .target(address(boomr)) + .sig("totalKillCount()") + .find(); + loc = bytes32(slot); + a = bytes32(abi.encode((maxSupply - maxSurvivorCount))); // 1k + vm.store(address(boomr), loc, a); + startHoax(victim); + vm.expectRevert(bytes("mission already completed")); + main.radicalizeBoomers{value: unaboomerPrice}(1); + vm.expectRevert(bytes("mission already completed")); + main.assembleBombs{value: bombPrice}(1); + vm.expectRevert(bytes("mission already completed")); + main.sendBombs(1); + } + + // ensure withdraw function works as expected + function testWithdrawalWorksAsOwner() public { + address owner = main.owner(); + uint256 ownerStartBalance = owner.balance; + assertEq(address(main).balance, 0); + hoax(victim); + main.radicalizeBoomers{value: unaboomerPrice * 10}(10); + uint256 contractBalance = address(main).balance; + assertEq(contractBalance, unaboomerPrice * 10); + main.withdraw(); + assertEq(owner.balance, ownerStartBalance + contractBalance); + } + + // ensure only owner can withdraw + function testWithdrawalFailsAsNotOwner() public { + hoax(victim); + main.radicalizeBoomers{value: unaboomerPrice * 10}(10); + uint256 contractBalance = address(main).balance; + assertEq(contractBalance, unaboomerPrice * 10); + vm.expectRevert("UNAUTHORIZED"); + hoax(address(0xd3ad)); + main.withdraw(); + vm.expectRevert("UNAUTHORIZED"); + hoax(address(123)); + main.withdraw(); + } + + // ensure NFT contracts do not accept Ether + function testNoAcceptEther() public { + (bool tbb, ) = payable(address(bomb)).call{value: .5 ether}(""); + assertEq(tbb, false); + (bool tbr, ) = payable(address(boomr)).call{value: .5 ether}(""); + assertEq(tbr, false); + } - // function testURILogic() public { - // address t = address(1); - // boomr.setAliveURI('ipfs://alive/'); - // boomr.setDeadURI('ipfs://dead/'); - // startHoax(t); - // main.radicalizeBoomers{value: .01 ether}(1); - // assertEq(boomr.tokenURI(0), 'ipfs://alive/0.json'); - // main.sendBombs(1); - // assertEq(boomr.tokenURI(0), 'ipfs://dead/0.json'); - // } + // Function to receive Ether. msg.data must be empty + receive() external payable {} + + // Fallback function is called when msg.data is not empty + fallback() external payable {} + }