diff --git a/src/Main.sol b/src/Main.sol index 58aab09..004bc02 100644 --- a/src/Main.sol +++ b/src/Main.sol @@ -75,7 +75,7 @@ contract Main is Owned { /// Point to the latest leaderboard update uint256 public leaderboardPointer; /// Price of the Unaboomer ERC-721 token - uint256 public unaboomerPrice = 0.01 ether; + uint256 public unaboomerPrice = 0; /// Price of the Mailbomb ERC-1155 token uint256 public bombPrice = 0.01 ether; /// Unaboomer contract @@ -176,6 +176,12 @@ contract Main is Owned { return unaboomer.MAX_SURVIVOR_COUNT(); } + /// Get BOOMR token max mint amount per wallet + /// @return mintAmount Maximum amount of BOOMR tokens that can be minted per wallet + function unaboomerMaxMintPerWallet() public view returns (uint256) { + return unaboomer.MAX_MINT_AMOUNT(); + } + /// Get BOMB token balance of wallet /// @param _address Wallet address to query balance of BOMB token /// @return balance Amount of BOMB tokens owned by _address @@ -230,12 +236,14 @@ contract Main is Owned { for (uint256 i; i < _amount; i++) { // Pick a pseudo-random Unaboomer token - imperfectly derives token IDs so that repeats are probable uint256 randomBoomer = (uint256(keccak256(abi.encodePacked(i, _amount, killed, block.timestamp, msg.sender))) % supply) + 1; + // Capture owner + address _owner = unaboomer.ownerOf(randomBoomer); // Check if it was already killed - bool dud = unaboomer.tokenDead(randomBoomer); + bool dud = _owner == address(0); // Kill it (does nothing if already toggled as dead) unaboomer.die(randomBoomer); // Check if the sender owns it (misfired, kills own pfp) - bool senderOwned = msg.sender == unaboomer.ownerOf(randomBoomer); + bool senderOwned = msg.sender == _owner; // Emit event for displaying in web app emit SentBomb(msg.sender, randomBoomer, !dud, senderOwned); // Increment kill count if successfully killed another player's Unaboomer diff --git a/src/Unaboomer.sol b/src/Unaboomer.sol index f19ffca..ca76164 100644 --- a/src/Unaboomer.sol +++ b/src/Unaboomer.sol @@ -24,12 +24,14 @@ the Main interface where the logic and validation resides. contract Unaboomer is ERC721, Owned { using LibString for uint256; - /// Track if a BOOMR token is toggled as alive or dead - mapping(uint256 => bool) public tokenDead; + /// Track mints per wallet to enforce maximum + mapping(address => uint256) public tokensMintedByWallet; /// Maximum supply of BOOMR tokens uint256 public constant MAX_SUPPLY = 35000; /// Maximum amount of survivors remaining to advance to the next round uint256 public constant MAX_SURVIVOR_COUNT = 10000; + /// Maximum amount of mints per wallet - cut down on botters + uint256 public constant MAX_MINT_AMOUNT = 35; /// Amount of Unaboomers killed (tokens burned) uint256 public burned; /// Amount of Unaboomers radicalized (tokens minted) @@ -90,10 +92,12 @@ contract Unaboomer is ERC721, Owned { /// @param _amount Amount of BOOMR tokens to mint function radicalize(address _to, uint256 _amount) external onlyMain { require(minted + _amount <= MAX_SUPPLY, "supply reached"); + require(tokensMintedByWallet[_to] + _amount <= MAX_MINT_AMOUNT, "cannot exceed maximum per wallet"); for (uint256 i; i < _amount; i++) { minted++; _safeMint(_to, minted); } + tokensMintedByWallet[_to] += _amount; } /// Toggle token state from living to dead diff --git a/test/Unaboomer.t.sol b/test/Unaboomer.t.sol index a35681e..bc9ac71 100644 --- a/test/Unaboomer.t.sol +++ b/test/Unaboomer.t.sol @@ -85,6 +85,15 @@ contract UnaboomerTest is Test { assertEq(main.bombsExploded(), 5); } + // ensure wallet limits enforced + function testWalletMintLimit() public { + uint256 max = main.unaboomerMaxMintPerWallet(); + startHoax(victim); + main.radicalizeBoomers{value: max * unaboomerPrice}(max); + vm.expectRevert("cannot exceed maximum per wallet"); + main.radicalizeBoomers{value: unaboomerPrice}(1); + } + // ensure supply limits enforced function testMaximumSupply() public { uint256 maxSupply = main.unaboomerMaxSupply();