From 06efb7d2404477d8cae4d17a313142bdf381edd6 Mon Sep 17 00:00:00 2001 From: lza_menace Date: Thu, 5 Jan 2023 14:45:20 -0800 Subject: [PATCH] documentation --- src/Mailbomb.sol | 40 ++++++++++--- src/Main.sol | 142 +++++++++++++++++++++++++++------------------- src/Unaboomer.sol | 45 +++++++++++++-- testing.sh | 3 + 4 files changed, 159 insertions(+), 71 deletions(-) diff --git a/src/Mailbomb.sol b/src/Mailbomb.sol index 4429bf3..6deebc1 100644 --- a/src/Mailbomb.sol +++ b/src/Mailbomb.sol @@ -3,15 +3,27 @@ pragma solidity ^0.8.13; import {ERC1155} from "solmate/tokens/ERC1155.sol"; import {Owned} from "solmate/auth/Owned.sol"; -import {LibString} from "solmate/utils/LibString.sol"; import {Main} from "./Main.sol"; +/** +@title Mailbomb +@author 0xgrimey.eth +@notice This contract contains ERC-1155 Mailbomb tokens (BOMB) which are used as +utility tokens for the Unaboomer NFT project and chain based game. +Mailbombs can be delivered to other players to "kill" tokens they hold, which +toggles the image to a dead / exploded image, and burns the underlying BOMB token. +@dev All contract functions regarding token burning and minting are limited to +the Main interface where the logic and validation resides. +*/ contract Mailbomb is ERC1155, Owned { - using LibString for uint256; + /// Track the total number of bombs assembled (tokens minted) uint256 public bombsAssembled; + /// Track the number of bombs that have exploded (been burned) uint256 public bombsExploded; + /// Base URI for the bomb image - all bombs use the same image string public baseURI; + /// Contract address of the deployed Main contract interface to the game Main public main; constructor() ERC1155() Owned(msg.sender) {} @@ -27,11 +39,13 @@ contract Mailbomb is ERC1155, Owned { } /// 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 { baseURI = _baseURI; } /// Set main contract address for executing functions + /// @param _address Contract address of the deployed Main contract function setMainContract(address _address) external onlyOwner { main = Main(_address); } @@ -40,9 +54,9 @@ contract Mailbomb is ERC1155, Owned { // Modifiers // ========================================================================= - /// Only main address can mint + /// Limit function execution to deployed Main contract modifier onlyMain { - require(msg.sender == address(main), "invalid minter"); + require(msg.sender == address(main), "invalid msg sender"); _; } @@ -51,29 +65,37 @@ contract Mailbomb is ERC1155, Owned { // ========================================================================= /// Mint tokens from main contract - function create(address _to, uint256 _amount) external payable onlyMain { + /// @param _to Address to mint BOMB tokens to + /// @param _amount Amount of BOMB tokens to mint + function create(address _to, uint256 _amount) external onlyMain { bombsAssembled += _amount; super._mint(_to, 1, _amount, ""); } /// Burn spent tokens from main contract + /// @param _from Address to burn BOMB tokens from + /// @param _amount Amount of BOMB tokens to burn function explode(address _from, uint256 _amount) external onlyMain { bombsExploded += _amount; super._burn(_from, 1, _amount); } + /// Get the total amount of bombs that have been assembled (minted) + /// @return supply Number of bombs assembled in totality (minted) function totalSupply() public view returns (uint256 supply) { return bombsAssembled; } + /// Return URI to retrieve JSON metadata from - points to images and descriptions + /// @param _tokenId Unused as all bombs point to same metadata URI + /// @return string IPFS or HTTP URI to retrieve JSON metadata from function uri(uint256 _tokenId) public view override returns (string memory) { return baseURI; } - function tokenURI(uint256 _tokenId) public view returns (string memory) { - return uri(_tokenId); - } - + /// Checks if contract supports a given interface + /// @param interfaceId The interface ID to check if contract supports + /// @return bool Boolean value if contract supports interface ID or not function supportsInterface(bytes4 interfaceId) public view virtual override (ERC1155) returns (bool) { return super.supportsInterface(interfaceId); } diff --git a/src/Main.sol b/src/Main.sol index 8af9f34..ba87c92 100644 --- a/src/Main.sol +++ b/src/Main.sol @@ -1,61 +1,71 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -// xxxx.xxxxxx -// xxxx xx xxx -// xxxxx xxx xxx -// x.xxxx xxx xx -// xx x xx xx -// xxx . xxx xx -// xxx xxxxxx xx -// xx x xx xx -// x xxxxxxxx.xxx xx x.x -// . x xxx..xxxx xxx .x -// xx xxx...xxx xxxxxxxxxx xx...x -// x x xx......x.....xx xxxxx x..x -// xx xx..xxxxxx..xx.xx....xxxx xxx x.x -// xx x.xxxxxx xxxx. xxxxxxxxxx...xxx .x -// .x x...xx x. .x x.xxxxxxxx ..x..x.x xx xx -// xxxx....xxx xx x.x xx x xxx x.xxxx.xxxx x.x xx -// xx x....xx xx .xx xx.xxxx x.xx xx xxx xxx...x x. xx -// x x...... xx xxxxx xxxxxxxxxxxxxxxxxxx xxx.x x.x x -// xx ...... xxxxxxxxxxxxxxxx xx...........xxxxx xx x.x x -// x ......xxx................x xx..xxxx.............xxxxx xx -// x x.....x..x xx.xx..........x x.. xxxxx.........xx.x xx -// x x........xx x..........x xxxx..x xxx............x..x . -// x x..........xx...........xxx .x...................xx..x x -// xxx.xx...................x x x. x................x x..x xx -// xx .x x..xx.............xxx x x...xx..............x x...x x -// xx.x .. x........xxxx xx xxxxxxxx...........xxx xxx.x x x -// xx.x x. xxxx..xxxxx xx.x xxx xxxxx..xxxxxx x ... . x -// xx..x .x xxxx x. xxx xx xx xxxxxx ...... xx -// x..x .x xx......x...xx.x x. ...... .x -// xxx. .x x xx x..xx.....xx ... ...... x xx -// xxxxx.x xx xx xxxxxxxxxxxxxx x... ...xx x. xx -// x x... xx x xxxxxxx xxxxxxx ... ..x x.. xx -// ...x .xxxxxxxxxxxxxxxx.xx . ..x .. x...x x -// xx..x xxxxxxxx x..xxx.x x..... x -// xxxx..x xxxx.xxxx x.xxx..x x.....x x -// xx x...x x.xxx...x x..xx xxxx -// . x...x x.xxx.... xx x...x -// x x..xxx xxxxx...xx xx x....x -// xx x..xxxx xxxxxxxxxxxxxx..xx xxx x......xx -// .xxx..x x..xxxxxxxxxxx...xx xxx x....x xx.xx -// xxxxx.xx.x...x .xxxxx.......xxxx..xxxxxxx.....xxxxxx xxxx -// xxx xxxx x xx.x ....x xx .......xxxxxxxx xxx xxx -// xxxxx xxxx x x x.x x xx.xxxx..x xxxx xx xxx -// xx xxx x x xxx.....xx...xxx x xx xx -// x x x .xx.xxxxx xxxx -// x x xxxxx xxxx -// x x x.xxx x x - -/// This contract is the main logic interface for the Unaboomer NFT project. - +// ___ooo_..._. +// .___. o_ __. +// ._.._ ._o. ..o_. +// _oo_...._._. .._. +// __.. o. ._ __ +// ._.. .o..... .o. +// .o. ....___. __. +// __. .... _o __. +// __ ..._.______.. .__ _x_. +// __ ..... ..._ooxo__.. .__. oo. +// o. . .__ooo_. ..__oxo__. ..oo_xo +// ._... ._oxxxxxoxxxx__. ______._.. .oxx_ +// __. .oxx_ooo__oxo.xooxoo___. .___ .oo +// __ _o__o_._.____o ____.oo_.oxx___. . .oo. +// ._. _oxoo_.o_ .x o_....____. .xxoxx__. ._ _o +// ._o._oxx_.o..o_ _xo oo.._. o. .x..o_xo__ .x_ __ +// __._oxxo__.o..o. __o_. .o. o_o o_o .._o_ox__ .oo. __ +// .o _xxxxo_ _o.oo_..._o___.._x__xoox_.___. ...ooxx_ oo_ _. +// o _xxxxxo.......__......... .._oooooo____.....___ .oo_ .. +// _. _xxxxxo.._ooxxxxxxxxxxxxx_..__xxxxxxxxxxxxxxxxxx__.__ .o_ +// _..oxxxxo._xooxxxxxxxxxxxxxxx. _xo ..oxxxxxxxxxxxxx._x_ .x. +// _..xxxxxxox.. ._.oxxxxxxxxxo...._xo. ...oxxxxxxxxxx.xxo .o. +// _..xxxxxxxxxx_.._xxxxxxxxxxo._..___xxo__oxxxxxxxxxxxxxx.xxx _. +// _..xxoxxxxxxxxxxxxxxxxxxxxx o. o_.xxxxxxxxxxxxxxxxxx_ xxx _. +// _o.ox .xxoxxxxxxxxxxxxxxxx.__. ox_.oxxxxxxxxxxxxxxx_ xxx_ .o. +// .._xo ox_ _xxxxxxxxxxxx_..._. _ooo_._oxxxxxxxxxxxx_...xoxo ._ .. +// .._o.._o. ._oxxxxxx___...o_ . ._..._ooxxxxxxo_..o o..ox. _. .o +// _.xxo.o_ ...._oo_... .o._ __ _..._oo_..__. ._.oxxo .o __ +// xxx._x ..... _oo.oo_..._o_.__ ..... ._ _xxxxxo .o. +// xxx._x _..ox_oxxxxxxxxxx_ .ox. _xxxxxo .x. +// o.o_.x_ ... .. . _oo_ _oxxo_.. oxx. _xxxxo. oo _. +// .o.oooo ._ .__ ..__o._..___.___. .xxx. _xxo. ox_._ +// .xxxo .x__._.______...____.__ _xx_ .ox. .oox_ o. +// .xxx_ _.. .......____._ _ .oxo..oo .oxxxo o +// _._xx_. .._o_. _xo_.oxo _xxxxx_ _. +// .o_.oxx_. ._________ .oo_.oxx_ oxxxo__ ._ +// __ _xxx_ .oo_.oxxx_ _xo_ ._oo_ +// ._ .oxxx_. .oo__oxxxo. ._ _oxxo. +// __ _xx___ .____.oxxo_. __ .xxxxo +// __ oxx _o_. ........__..ooxo_. ___. ._xxxooxo_.. +// ._xoo_xx_ _ox__________oxxx_. .__. ..oxxxx_. ._oo_. +// ......_o__o_xox_ .o__...oxxxxxo__._oo______oxxxxxo_.__.. ..___. +// ...... .___ .o_.ox_ oxxo. .o oxxxxxxooooooo_. ___. ..__. +// ._.... ._._ o_. .o_ _. .oxo__oxx_.oo__ .__. ._.. +// ... .._. o_. __.xxxoo__xxo__o _. ._. .._. +// .. o_. .o._ooo__. o__. +// o_. .._.o. _.__ +// o_. ..x_o. o o import {Owned} from "solmate/auth/Owned.sol"; import {Unaboomer} from "./Unaboomer.sol"; import {Mailbomb} from "./Mailbomb.sol"; +/** +@title UnaboomerNFT +@author 0xgrimey.eth +@notice This is the main contract interface for the Unaboomer NFT project drop and chain based game. +It contains the logic between an ERC-721 contract containing Unaboomer tokens (pixelated Unabomber +inspired profile pictures) and an ERC-1155 contract containing Mailbomb tokens (utility tokens). +Unaboomer is a chain based game with some mechanics based around "killing" other players by sending +them mailbombs until a certain amount of players or "survivors" remain. The motif was inspired by +the real life story of Theodore Kaczynski, known as the Unabomber, who conducted a nationwide +mail bombing campaign against people he believed to be advancing modern technology and the +destruction of the environment. Ironic, isn't it? +*/ contract Main is Owned { /// Track the number of kills for each address @@ -73,7 +83,7 @@ contract Main is Owned { /// Mailbomb contract Mailbomb public mailbomb; - /// SentBomb event is for recording the results of sendBombs + /// SentBomb event is for recording the results of sendBombs for real-time feedback to a frontend interface /// @param from Sender of the bombs /// @param tokenId Unaboomer token which was targeted /// @param hit Whether or not the bomb killed the token or not (was a dud / already killed) @@ -92,13 +102,13 @@ contract Main is Owned { payable(msg.sender).transfer(balance); } - /// Set price for 1 BOOMR + /// Set price per BOOMR /// @param _price Price in wei to mint BOOMR token function setBoomerPrice(uint256 _price) external onlyOwner { unaboomerPrice = _price; } - /// Set price for 1 BOMB + /// Set price per BOMB /// @param _price Price in wei to mint BOMB token function setBombPrice(uint256 _price) external onlyOwner { bombPrice = _price; @@ -121,7 +131,7 @@ contract Main is Owned { // ========================================================================= /// This modifier prevents actions once the Unaboomer survivor count is breached. - /// The game stops. + /// 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()), @@ -136,42 +146,50 @@ contract Main is Owned { /// Get BOOMR token balance of wallet /// @param _address Wallet address to query balance of BOOMR token + /// @return balance Amount of BOOMR tokens owned by _address function unaboomerBalance(address _address) public view returns (uint256) { return unaboomer.balanceOf(_address); } /// Get BOOMR token total supply + /// @return supply Amount of BOOMR tokens minted in total function unaboomerSupply() public view returns (uint256) { return unaboomer.totalSupply(); } /// Get BOOMR kill count (unaboomers killed) + /// @return killCount Amount of BOOMR tokens "killed" (dead pfp) function unaboomersKilled() public view returns (uint256) { return unaboomer.totalKillCount(); } /// Get BOOMR token max supply + /// @return maxSupply Maximum amount of BOOMR tokens that can ever exist function unaboomerMaxSupply() public view returns (uint256) { return unaboomer.MAX_SUPPLY(); } /// 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(); } /// 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 function bombBalance(address _address) public view returns (uint256) { return mailbomb.balanceOf(_address, 1); } /// Get BOMB token supply + /// @return supply Amount of BOMB tokens ever minted / "assembled" function bombSupply() public view returns (uint256) { return mailbomb.bombsAssembled(); } /// Get BOMB exploded amount + /// @return exploded Amount of BOMB tokens that have burned / "exploded" function bombsExploded() public view returns (uint256) { return mailbomb.bombsExploded(); } @@ -181,7 +199,7 @@ contract Main is Owned { // ========================================================================= /// Radicalize a boomer to become a Unaboomer - start with 2 bombs - /// @param _amount Amount of Unaboomers to radicalize (mint) + /// @param _amount Amount of Unaboomers to mint / "radicalize" function radicalizeBoomers(uint256 _amount) external payable missionNotCompleted { require(msg.value >= _amount * unaboomerPrice, "not enough ether"); unaboomer.radicalize(msg.sender, _amount); @@ -189,7 +207,7 @@ contract Main is Owned { } /// Assemble additional mailbombs to kill targets - /// @param _amount Amount of bombs to assemble (mint) + /// @param _amount Amount of bombs mint / "assemble" function assembleBombs(uint256 _amount) external payable missionNotCompleted { require(msg.value >= _amount * bombPrice, "not enough ether"); mailbomb.create(msg.sender, _amount); @@ -200,22 +218,30 @@ contract Main is Owned { /// Update a leaderboard with updated kill counts. /// @dev Pick a pseudo-random tokenID from Unaboomer contract and toggle a mapping value /// @dev The likelihood of killing a boomer decreases as time goes on - i.e. more duds - /// @param _amount Amount of bombs to send to kill Unaboomers + /// @param _amount Amount of bombs to send to kill Unaboomers (dead pfps) function sendBombs(uint256 _amount) external missionNotCompleted { + // Ensure _amount will not exceed wallet balance of bombs, Unaboomer supply, and active Unaboomers uint256 supply = unaboomer.totalSupply(); require(_amount <= bombBalance(msg.sender), "not enough bombs"); require(_amount <= supply, "not enough supply"); require(_amount <= supply - unaboomersKilled(), "not enough active boomers"); for (uint256 i; i < _amount; i++) { + // Pick a pseudo-random Unaboomer token uint256 randomBoomer = uint256(keccak256(abi.encodePacked(i, _amount, block.timestamp, msg.sender))) % supply; + // Check if it was already killed bool dud = unaboomer.tokenDead(randomBoomer); + // 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); + // Emit event for displaying in web app emit SentBomb(msg.sender, randomBoomer, !dud, senderOwned); + // Increment kill count if successfully killed another player's Unaboomer if(!dud && !senderOwned) { killCount[msg.sender]++; } } + // Update the leaderboard and pointer for tracking the highest amount of kills for wallets uint256 kills = killCount[msg.sender]; address leader = leaderboard[leaderboardPointer]; if (kills > killCount[leader]) { @@ -224,6 +250,8 @@ contract Main is Owned { leaderboard[leaderboardPointer] = msg.sender; } } + // Burn ERC-1155 BOMB tokens (bombs go away after sending / exploding) mailbomb.explode(msg.sender, _amount); } + } \ No newline at end of file diff --git a/src/Unaboomer.sol b/src/Unaboomer.sol index 00d63ed..197c7ad 100644 --- a/src/Unaboomer.sol +++ b/src/Unaboomer.sol @@ -6,17 +6,39 @@ import {Owned} from "solmate/auth/Owned.sol"; import {LibString} from "solmate/utils/LibString.sol"; import {Main} from "./Main.sol"; +/** +@title Unaboomer +@author 0xgrimey.eth +@notice This contract contains ERC-721 Unaboomer tokens (BOOMR) which are the profile +picture and membership tokens for the Unaboomer NFT project and chain based game. +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 +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 +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; - + /// 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; + /// The total amount of Unaboomers who have been killed during the game uint256 public totalKillCount; + /// Number of tokens minted (total supply) uint256 public minted; + /// Base URI for living Unaboomers - original pixelated avatars string public aliveURI; + /// Base URI for dead Unaboomers - pixelated explosion string public deadURI; + /// Contract address of the deployed Main contract interface to the game Main public main; constructor() ERC721("Unaboomer", "BOOMR") Owned(msg.sender) {} @@ -31,17 +53,20 @@ contract Unaboomer is ERC721, Owned { payable(msg.sender).transfer(balance); } - /// Set metadata URI for alive BOOMR + /// 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 { aliveURI = _baseURI; } - /// Set metadata URI for dead BOOMR + /// Set metadata URI for dead Unaboomer tokens + /// @param _baseURI IPFS hash or URL to retrieve JSON metadata for dead Unaboomer tokens function setDeadURI(string calldata _baseURI) external onlyOwner { deadURI = _baseURI; } /// Set main contract address for executing functions + /// @param _address Contract address of the deployed Main contract function setMainContract(address _address) external onlyOwner { main = Main(_address); } @@ -50,9 +75,9 @@ contract Unaboomer is ERC721, Owned { // Modifiers // ========================================================================= - /// Only main address can mint + /// Limit function execution to deployed Main contract modifier onlyMain { - require(msg.sender == address(main), "invalid minter"); + require(msg.sender == address(main), "invalid msg sender"); _; } @@ -61,11 +86,14 @@ contract Unaboomer is ERC721, Owned { // ========================================================================= /// Helper function to get supply minted + /// @return supply Number of Unaboomers radicalized in totality (minted) function totalSupply() public view returns (uint256) { return minted; } /// Mint tokens from main contract + /// @param _to Address to mint BOOMR tokens to + /// @param _amount Amount of BOOMR tokens to mint function radicalize(address _to, uint256 _amount) external payable onlyMain { require(totalSupply() + _amount <= MAX_SUPPLY, "supply reached"); for (uint256 i; i < _amount; i++) { @@ -75,6 +103,7 @@ contract Unaboomer is ERC721, Owned { } /// Toggle token state from living to dead + /// @param tokenId Token ID of BOOMR to toggle living -> dead and increment kill count function die(uint256 tokenId) external onlyMain { require(tokenId < totalSupply(), "invalid token id"); if (tokenDead[tokenId] == false) { @@ -83,6 +112,9 @@ contract Unaboomer is ERC721, Owned { } } + // Return URI to retrieve JSON metadata from - points to images and descriptions + /// @param _tokenId Token ID of BOOMR to fetch URI for + /// @return string IPFS or HTTP URI to retrieve JSON metadata from function tokenURI(uint256 _tokenId) public view override returns (string memory) { if (tokenDead[_tokenId] == true) { return string(abi.encodePacked(deadURI, _tokenId.toString(), ".json")); @@ -91,6 +123,9 @@ contract Unaboomer is ERC721, Owned { } } + /// Checks if contract supports a given interface + /// @param interfaceId The interface ID to check if contract supports + /// @return bool Boolean value if contract supports interface ID or not function supportsInterface(bytes4 interfaceId) public view virtual override (ERC721) returns (bool) { return super.supportsInterface(interfaceId); } diff --git a/testing.sh b/testing.sh index de85657..56f8bac 100644 --- a/testing.sh +++ b/testing.sh @@ -16,3 +16,6 @@ cast send --private-key=$LOCAL_KEY --rpc-url=$LOCAL_RPC --value "10 ether" 0x653 # cast send --private-key=0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356 --rpc-url=$LOCAL_RPC 0x5FbDB2315678afecb367f032d93F642f64180aa3 "radicalizeBoomers(uint256)" 10 --value "0.1 ether" # cast send --private-key=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 --rpc-url=$LOCAL_RPC 0x5FbDB2315678afecb367f032d93F642f64180aa3 "radicalizeBoomers(uint256)" 10 --value "0.1 ether" # cast send --private-key=0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --rpc-url=$LOCAL_RPC 0x5FbDB2315678afecb367f032d93F642f64180aa3 "radicalizeBoomers(uint256)" 10 --value "0.1 ether" + + +# forge script script/Unaboomer.s.sol:DeployProject --private-key=$TESTNET_KEY --rpc-url=$TESTNET_RPC --broadcast \ No newline at end of file