diff --git a/packages/nfts/contracts/trailblazers-badges/ECDSAWhitelist.sol b/packages/nfts/contracts/trailblazers-badges/ECDSAWhitelist.sol new file mode 100644 index 00000000000..cee54194521 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-badges/ECDSAWhitelist.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { UUPSUpgradeable } from + "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { Ownable2StepUpgradeable } from + "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { ContextUpgradeable } from + "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @title ECDSAWhitelist +/// @dev Signature-driven mint whitelist +/// @custom:security-contact security@taiko.xyz +contract ECDSAWhitelist is ContextUpgradeable, UUPSUpgradeable, Ownable2StepUpgradeable { + event MintSignerUpdated(address _mintSigner); + event MintConsumed(address _minter, uint256 _tokenId); + event BlacklistUpdated(address _blacklist); + + error MINTS_EXCEEDED(); + error ADDRESS_BLACKLISTED(); + error ONLY_MINT_SIGNER(); + + /// @notice Mint signer address + address public mintSigner; + /// @notice Tracker for minted signatures + mapping(bytes32 signatureHash => bool hasMinted) public minted; + /// @notice Blackist address + IMinimalBlacklist public blacklist; + /// @notice Gap for upgrade safety + uint256[47] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Modifier to restrict access to the mint signer + modifier onlyMintSigner() { + if (msg.sender != mintSigner) revert ONLY_MINT_SIGNER(); + _; + } + + /// @notice Update the blacklist address + /// @param _blacklist The new blacklist address + function updateBlacklist(IMinimalBlacklist _blacklist) external onlyOwner { + blacklist = _blacklist; + emit BlacklistUpdated(address(_blacklist)); + } + + /// @notice Update the mint signer address + /// @param _mintSigner The new mint signer address + function updateMintSigner(address _mintSigner) public onlyOwner { + mintSigner = _mintSigner; + emit MintSignerUpdated(_mintSigner); + } + + /// @notice Contract initializer + /// @param _owner Contract owner + /// @param _mintSigner Mint signer address + /// @param _blacklist Blacklist address + function initialize( + address _owner, + address _mintSigner, + IMinimalBlacklist _blacklist + ) + external + initializer + { + __ECDSAWhitelist_init(_owner, _mintSigner, _blacklist); + } + + /// @notice Generate a standardized hash for externally signing + /// @param _minter Address of the minter + /// @param _tokenId ID for the token to mint + function getHash(address _minter, uint256 _tokenId) public pure returns (bytes32) { + return keccak256(bytes.concat(keccak256(abi.encode(_minter, _tokenId)))); + } + + /// @notice Internal method to verify valid signatures + /// @param _signature Signature to verify + /// @param _minter Address of the minter + /// @param _tokenId ID for the token to mint + /// @return Whether the signature is valid + function _isSignatureValid( + bytes memory _signature, + address _minter, + uint256 _tokenId + ) + internal + view + returns (bool) + { + bytes32 _hash = getHash(_minter, _tokenId); + (address _recovered,,) = ECDSA.tryRecover(_hash, _signature); + + return _recovered == mintSigner; + } + + /// @notice Check if a wallet can mint + /// @param _signature Signature to verify + /// @param _minter Address of the minter + /// @param _tokenId ID for the token to mint + /// @return Whether the wallet can mint + function canMint( + bytes memory _signature, + address _minter, + uint256 _tokenId + ) + public + view + returns (bool) + { + if (blacklist.isBlacklisted(_minter)) revert ADDRESS_BLACKLISTED(); + if (minted[keccak256(_signature)]) return false; + return _isSignatureValid(_signature, _minter, _tokenId); + } + + /// @notice Internal initializer + /// @param _owner Contract owner + /// @param _mintSigner Mint signer address + /// @param _blacklist Blacklist address + function __ECDSAWhitelist_init( + address _owner, + address _mintSigner, + IMinimalBlacklist _blacklist + ) + internal + { + _transferOwnership(_owner == address(0) ? msg.sender : _owner); + __Context_init(); + mintSigner = _mintSigner; + blacklist = _blacklist; + } + + /// @notice Internal method to consume a mint + /// @param _signature Signature to verify + /// @param _minter Address of the minter + /// @param _tokenId ID for the token to mint + function _consumeMint(bytes memory _signature, address _minter, uint256 _tokenId) internal { + if (!canMint(_signature, _minter, _tokenId)) revert MINTS_EXCEEDED(); + minted[keccak256(_signature)] = true; + emit MintConsumed(_minter, _tokenId); + } + + /// @notice Internal method to authorize an upgrade + function _authorizeUpgrade(address) internal virtual override onlyOwner { } +} diff --git a/packages/nfts/contracts/trailblazers-badges/TrailblazersBadges.sol b/packages/nfts/contracts/trailblazers-badges/TrailblazersBadges.sol new file mode 100644 index 00000000000..78396e3dc18 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-badges/TrailblazersBadges.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { ERC721EnumerableUpgradeable } from + "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import { ECDSAWhitelist } from "./ECDSAWhitelist.sol"; +import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +contract TrailblazersBadges is ERC721EnumerableUpgradeable, ECDSAWhitelist { + /// @notice Movement IDs + uint256 public constant MOVEMENT_NEUTRAL = 0; + uint256 public constant MOVEMENT_BASED = 1; + uint256 public constant MOVEMENT_BOOSTED = 2; + /// @notice Badge IDs + uint256 public constant BADGE_RAVERS = 0; + uint256 public constant BADGE_ROBOTS = 1; + uint256 public constant BADGE_BOUNCERS = 2; + uint256 public constant BADGE_MASTERS = 3; + uint256 public constant BADGE_MONKS = 4; + uint256 public constant BADGE_DRUMMERS = 5; + uint256 public constant BADGE_ANDROIDS = 6; + uint256 public constant BADGE_SHINTO = 7; + + /// @notice Base URI required to interact with IPFS + string private _baseURIExtended; + /// @notice Token ID to badge ID mapping + mapping(uint256 _tokenId => uint256 _badgeId) public badges; + /// @notice Wallet-to-Movement mapping + mapping(address _user => uint256 _movement) public movements; + /// @notice Wallet to badge ID, token ID mapping + mapping(address _user => mapping(uint256 _badgeId => uint256 _tokenId)) public userBadges; + /// @notice Movement to badge ID, token ID mapping + mapping(bytes32 movementBadgeHash => uint256[2] movementBadge) public movementBadges; + /// @notice Gap for upgrade safety + uint256[43] private __gap; + + error MINTER_NOT_WHITELISTED(); + error INVALID_INPUT(); + error INVALID_BADGE_ID(); + error INVALID_MOVEMENT_ID(); + + event BadgeCreated(uint256 _tokenId, address _minter, uint256 _badgeId); + event MovementSet(address _user, uint256 _movementId); + event UriSet(string _uri); + + /// @notice Contract initializer + /// @param _owner Contract owner + /// @param _rootURI Base URI for the token metadata + /// @param _mintSigner The address that can authorize minting badges + /// @param _blacklistAddress The address of the blacklist contract + function initialize( + address _owner, + string memory _rootURI, + address _mintSigner, + IMinimalBlacklist _blacklistAddress + ) + external + initializer + { + __ERC721_init("Trailblazers Badges", "TBB"); + _baseURIExtended = _rootURI; + __ECDSAWhitelist_init(_owner, _mintSigner, _blacklistAddress); + } + + /// @notice Ensure update of userBadges on transfers + /// @param to The address to transfer to + /// @param tokenId The token id to transfer + /// @param auth The authorizer of the transfer + function _update( + address to, + uint256 tokenId, + address auth + ) + internal + virtual + override + returns (address) + { + userBadges[_ownerOf(tokenId)][badges[tokenId]] = 0; + userBadges[to][badges[tokenId]] = tokenId; + return super._update(to, tokenId, auth); + } + + /// @notice Update the base URI + /// @param _uri The new base URI + function setUri(string memory _uri) public onlyOwner { + _baseURIExtended = _uri; + emit UriSet(_uri); + } + + /// @notice Get the URI for a tokenId + /// @param _tokenId The badge ID + /// @return URI The URI for the badge + function tokenURI(uint256 _tokenId) public view override returns (string memory) { + uint256 movementId = movements[ownerOf(_tokenId)]; + uint256 badgeId = badges[_tokenId]; + return string( + abi.encodePacked( + _baseURIExtended, "/", Strings.toString(movementId), "/", Strings.toString(badgeId) + ) + ); + } + + /// @notice Mint a badge from the calling wallet + /// @param _signature The signature authorizing the mint + /// @param _badgeId The badge ID to mint + function mint(bytes memory _signature, uint256 _badgeId) public { + _mintBadgeTo(_signature, _msgSender(), _badgeId); + } + + /// @notice Mint a badge to a specific address + /// @param _signature The signature authorizing the mint + /// @param _minter The address to mint the badge to + /// @param _badgeId The badge ID to mint + /// @dev Admin only method + function mint(bytes memory _signature, address _minter, uint256 _badgeId) public onlyOwner { + _mintBadgeTo(_signature, _minter, _badgeId); + } + + /// @notice Internal method for badge minting + /// @param _signature The signature authorizing the mint + /// @param _minter The address to mint the badge to + /// @param _badgeId The badge ID to mint + function _mintBadgeTo(bytes memory _signature, address _minter, uint256 _badgeId) internal { + if (_badgeId > BADGE_SHINTO) revert INVALID_BADGE_ID(); + + _consumeMint(_signature, _minter, _badgeId); + + uint256 tokenId = totalSupply() + 1; + badges[tokenId] = _badgeId; + + _mint(_minter, tokenId); + + emit BadgeCreated(tokenId, _minter, _badgeId); + } + + /// @notice Sets movement for the calling wallet + /// @param _movementId The movement ID to set + function setMovement(uint256 _movementId) public { + _setMovement(_msgSender(), _movementId); + } + + /// @notice Sets movement for a specific address + /// @param _user The address to set the movement for + /// @param _movementId The movement ID to set + /// @dev Owner-only method + function setMovement(address _user, uint256 _movementId) public onlyOwner { + _setMovement(_user, _movementId); + } + + /// @notice Internal method for setting movement + /// @param _user The address to set the movement for + /// @param _movementId The movement ID to set + function _setMovement(address _user, uint256 _movementId) internal { + if (_movementId > MOVEMENT_BOOSTED) revert INVALID_MOVEMENT_ID(); + movements[_user] = _movementId; + emit MovementSet(_user, _movementId); + } + + /// @notice Retrieve a token ID given their owner and Badge ID + /// @param _user The address of the badge owner + /// @param _badgeId The badge ID + /// @return tokenId The token ID + function getTokenId(address _user, uint256 _badgeId) public view returns (uint256) { + return userBadges[_user][_badgeId]; + } + + /// @notice Retrieve boolean balance for each badge + /// @param _owner The addresses to check + /// @return balances The badges atomic balances + function badgeBalances(address _owner) public view returns (bool[] memory) { + bool[] memory balances = new bool[](8); + balances[0] = 0 != getTokenId(_owner, BADGE_RAVERS); + balances[1] = 0 != getTokenId(_owner, BADGE_ROBOTS); + balances[2] = 0 != getTokenId(_owner, BADGE_BOUNCERS); + balances[3] = 0 != getTokenId(_owner, BADGE_MASTERS); + balances[4] = 0 != getTokenId(_owner, BADGE_MONKS); + balances[5] = 0 != getTokenId(_owner, BADGE_DRUMMERS); + balances[6] = 0 != getTokenId(_owner, BADGE_ANDROIDS); + balances[7] = 0 != getTokenId(_owner, BADGE_SHINTO); + return balances; + } +} diff --git a/packages/nfts/deployments/trailblazers-badges/hekla.json b/packages/nfts/deployments/trailblazers-badges/hekla.json new file mode 100644 index 00000000000..447f32371cb --- /dev/null +++ b/packages/nfts/deployments/trailblazers-badges/hekla.json @@ -0,0 +1,5 @@ +{ + "MintSigner": "0x3cda4F2EaC3fc2FdE78B3DFFe1A1A1Eff88c68c5", + "Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "TrailblazersBadges": "0x8cCE36573293e5bE12F8530f683caa51719cF57E" +} diff --git a/packages/nfts/deployments/trailblazers-badges/localhost.json b/packages/nfts/deployments/trailblazers-badges/localhost.json new file mode 100644 index 00000000000..45b2da0f36c --- /dev/null +++ b/packages/nfts/deployments/trailblazers-badges/localhost.json @@ -0,0 +1,5 @@ +{ + "MintSigner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "TrailblazersBadges": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" +} diff --git a/packages/nfts/deployments/trailblazers-badges/mainnet.json b/packages/nfts/deployments/trailblazers-badges/mainnet.json new file mode 100644 index 00000000000..45b2da0f36c --- /dev/null +++ b/packages/nfts/deployments/trailblazers-badges/mainnet.json @@ -0,0 +1,5 @@ +{ + "MintSigner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "TrailblazersBadges": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" +} diff --git a/packages/nfts/package.json b/packages/nfts/package.json index a2c77c5295b..a112cc12aa4 100644 --- a/packages/nfts/package.json +++ b/packages/nfts/package.json @@ -20,7 +20,9 @@ "snaefell:deploy:devnet": "forge clean && pnpm compile && forge script script/snaefell/sol/Deploy.s.sol --rpc-url https://rpc.internal.taiko.xyz --broadcast --gas-estimate-multiplier 200", "taikoon:deploy:mainnet": "forge clean && pnpm compile && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 13 ", "snaefell:deploy:mainnet": "forge clean && pnpm compile && forge script script/snaefell/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 13 ", - "taikoon:deploy:holesky": "forge clean && pnpm compile && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://1rpc.io/holesky --broadcast --gas-estimate-multiplier 200" + "taikoon:deploy:holesky": "forge clean && pnpm compile && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://1rpc.io/holesky --broadcast --gas-estimate-multiplier 200", + "tbzb:deploy:localhost": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast", + "tbzb:deploy:hekla": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200" }, "devDependencies": { "@types/node": "^20.11.30", diff --git a/packages/nfts/script/trailblazers-badges/sol/Deploy.s.sol b/packages/nfts/script/trailblazers-badges/sol/Deploy.s.sol new file mode 100644 index 00000000000..4dabb0942e0 --- /dev/null +++ b/packages/nfts/script/trailblazers-badges/sol/Deploy.s.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { UtilsScript } from "./Utils.s.sol"; +import { Script, console } from "forge-std/src/Script.sol"; +import { Merkle } from "murky/Merkle.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { TrailblazersBadges } from "../../../contracts/trailblazers-badges/TrailblazersBadges.sol"; +import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; + +contract DeployScript is Script { + UtilsScript public utils; + string public jsonLocation; + uint256 public deployerPrivateKey; + address public deployerAddress; + + // Taiko Mainnet Values + //address owner = 0xf8ff2AF0DC1D5BA4811f22aCb02936A1529fd2Be; + //bytes32 root = 0xa7e510d5aed347e65609cf6f0e0738cdd752ffdf5980749057c634489fd09fc3; + // string baseURI = "bafybeierqzehlrqeqqeb6fwmil4dj3ij2p6exgoj4lysl53fsxwob6wbdy"; + // IMinimalBlacklist blacklist = IMinimalBlacklist(0xfA5EA6f9A13532cd64e805996a941F101CCaAc9a); + + // Holesky Testnet Values + // address owner = 0xf8ff2AF0DC1D5BA4811f22aCb02936A1529fd2Be; + // bytes32 root = 0xf1359c4c4ba41a72025f2534ea8ad23c6b941b55a715838ebdc71202a78c6c87; + // string baseURI = "bafybeierqzehlrqeqqeb6fwmil4dj3ij2p6exgoj4lysl53fsxwob6wbdy"; + // IMinimalBlacklist blacklist = IMinimalBlacklist(0xe61E9034b5633977eC98E302b33e321e8140F105); + + // Hardhat Testnet Values + address owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address mintSigner = 0x3cda4F2EaC3fc2FdE78B3DFFe1A1A1Eff88c68c5; + string baseURI = "bafybeierqzehlrqeqqeb6fwmil4dj3ij2p6exgoj4lysl53fsxwob6wbdy"; + IMinimalBlacklist blacklist = IMinimalBlacklist(0xe61E9034b5633977eC98E302b33e321e8140F105); + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + + jsonLocation = utils.getContractJsonLocation(); + deployerPrivateKey = utils.getPrivateKey(); + deployerAddress = utils.getAddress(); + } + + function run() public { + string memory jsonRoot = "root"; + + require(owner != address(0), "Owner must be specified"); + + vm.startBroadcast(deployerPrivateKey); + + // deploy token with empty root + address impl = address(new TrailblazersBadges()); + address proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + TrailblazersBadges.initialize, (owner, baseURI, mintSigner, blacklist) + ) + ) + ); + + TrailblazersBadges token = TrailblazersBadges(proxy); + + console.log("Token Base URI:", baseURI); + console.log("Deployed TrailblazersBadges to:", address(token)); + + vm.serializeAddress(jsonRoot, "Owner", token.owner()); + vm.serializeAddress(jsonRoot, "MintSigner", token.mintSigner()); + + string memory finalJson = + vm.serializeAddress(jsonRoot, "TrailblazersBadges", address(token)); + vm.writeJson(finalJson, jsonLocation); + + vm.stopBroadcast(); + } +} diff --git a/packages/nfts/script/trailblazers-badges/sol/Utils.s.sol b/packages/nfts/script/trailblazers-badges/sol/Utils.s.sol new file mode 100644 index 00000000000..6e3a113feea --- /dev/null +++ b/packages/nfts/script/trailblazers-badges/sol/Utils.s.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { Script, console } from "forge-std/src/Script.sol"; +import "forge-std/src/StdJson.sol"; +import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; +import { MockBlacklist } from "../../../test/util/Blacklist.sol"; + +contract UtilsScript is Script { + using stdJson for string; + + address public nounsTokenAddress; + + uint256 public chainId; + + string public lowercaseNetworkKey; + string public uppercaseNetworkKey; + + function setUp() public { + // load all network configs + chainId = block.chainid; + + if (chainId == 31_337) { + lowercaseNetworkKey = "localhost"; + uppercaseNetworkKey = "LOCALHOST"; + } else if (chainId == 17_000) { + lowercaseNetworkKey = "holesky"; + uppercaseNetworkKey = "HOLESKY"; + } else if (chainId == 167_001) { + lowercaseNetworkKey = "devnet"; + uppercaseNetworkKey = "DEVNET"; + } else if (chainId == 11_155_111) { + lowercaseNetworkKey = "sepolia"; + uppercaseNetworkKey = "SEPOLIA"; + } else if (chainId == 167_008) { + lowercaseNetworkKey = "katla"; + uppercaseNetworkKey = "KATLA"; + } else if (chainId == 167_000) { + lowercaseNetworkKey = "mainnet"; + uppercaseNetworkKey = "MAINNET"; + } else if (chainId == 167_009) { + lowercaseNetworkKey = "hekla"; + uppercaseNetworkKey = "HEKLA"; + } else { + revert("Unsupported chainId"); + } + } + + function getPrivateKey() public view returns (uint256) { + string memory lookupKey = string.concat(uppercaseNetworkKey, "_PRIVATE_KEY"); + return vm.envUint(lookupKey); + } + + function getAddress() public view returns (address) { + string memory lookupKey = string.concat(uppercaseNetworkKey, "_ADDRESS"); + return vm.envAddress(lookupKey); + } + + function getContractJsonLocation() public view returns (string memory) { + string memory root = vm.projectRoot(); + return + string.concat(root, "/deployments/trailblazers-badges/", lowercaseNetworkKey, ".json"); + } + + function getBlacklist() public view returns (IMinimalBlacklist blacklistAddress) { + if (block.chainid == 167_000) { + // mainnet blacklist address + blacklistAddress = IMinimalBlacklist(vm.envAddress("BLACKLIST_ADDRESS")); + } else { + // deploy a mock blacklist otherwise + blacklistAddress = IMinimalBlacklist(0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6); + } + + return blacklistAddress; + } + + function run() public { } +} diff --git a/packages/nfts/test/trailblazers-badges/TrailblazersBadges.t.sol b/packages/nfts/test/trailblazers-badges/TrailblazersBadges.t.sol new file mode 100644 index 00000000000..52498a0ff38 --- /dev/null +++ b/packages/nfts/test/trailblazers-badges/TrailblazersBadges.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { Test } from "forge-std/src/Test.sol"; + +import { TrailblazersBadges } from "../../contracts/trailblazers-badges/TrailblazersBadges.sol"; +import { Merkle } from "murky/Merkle.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { UtilsScript } from "../../script/taikoon/sol/Utils.s.sol"; +import { MockBlacklist } from "../util/Blacklist.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +contract TrailblazersBadgesTest is Test { + UtilsScript public utils; + + TrailblazersBadges public token; + + address public owner = vm.addr(0x5); + + address[3] public minters = [vm.addr(0x1), vm.addr(0x2), vm.addr(0x3)]; + + uint256 constant BADGE_ID = 5; + + MockBlacklist public blacklist; + + Merkle tree = new Merkle(); + + address mintSigner; + uint256 mintSignerPk; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + blacklist = new MockBlacklist(); + // create whitelist merkle tree + vm.startBroadcast(owner); + + (mintSigner, mintSignerPk) = makeAddrAndKey("mintSigner"); + + // deploy token with empty root + address impl = address(new TrailblazersBadges()); + address proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + TrailblazersBadges.initialize, (owner, "ipfs://", mintSigner, blacklist) + ) + ) + ); + + token = TrailblazersBadges(proxy); + + vm.stopBroadcast(); + } + + function test_metadata_badges() public view { + assertEq(token.BADGE_RAVERS(), 0); + assertEq(token.BADGE_ROBOTS(), 1); + assertEq(token.BADGE_BOUNCERS(), 2); + assertEq(token.BADGE_MASTERS(), 3); + assertEq(token.BADGE_MONKS(), 4); + assertEq(token.BADGE_DRUMMERS(), 5); + assertEq(token.BADGE_ANDROIDS(), 6); + assertEq(token.BADGE_SHINTO(), 7); + } + + function test_canMint_true() public view { + bytes32 _hash = token.getHash(minters[0], BADGE_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], BADGE_ID); + assertTrue(canMint); + } + + // send the signature for minters[0] but check for minters[1] + function test_canMint_false() public view { + bytes32 _hash = token.getHash(minters[0], BADGE_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[1], BADGE_ID); + assertFalse(canMint); + } + + function test_mint() public { + bytes32 _hash = token.getHash(minters[0], BADGE_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], BADGE_ID); + assertTrue(canMint); + + vm.startPrank(minters[0]); + token.mint(abi.encodePacked(r, s, v), BADGE_ID); + vm.stopPrank(); + + assertEq(token.balanceOf(minters[0]), 1); + } + + function test_mint_revert_notAuthorized() public { + bytes32 _hash = token.getHash(minters[0], BADGE_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], BADGE_ID); + assertTrue(canMint); + + vm.expectRevert(); + token.mint(abi.encodePacked(r, s, v), minters[1], BADGE_ID); + } + + function test_mint_revert_invalidBadgeId() public { + bytes32 _hash = token.getHash(minters[0], BADGE_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], BADGE_ID); + assertTrue(canMint); + + vm.expectRevert(); + token.mint(abi.encodePacked(r, s, v), minters[0], 8); + } + + function test_mint_owner() public { + bytes32 _hash = token.getHash(minters[0], BADGE_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], BADGE_ID); + assertTrue(canMint); + + vm.startPrank(owner); + token.mint(abi.encodePacked(r, s, v), minters[0], BADGE_ID); + vm.stopPrank(); + + assertEq(token.balanceOf(minters[0]), 1); + } + + function test_mint_revert_remintSameSignature() public { + bytes32 _hash = token.getHash(minters[0], BADGE_ID); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], BADGE_ID); + assertTrue(canMint); + + vm.startBroadcast(minters[0]); + token.mint(abi.encodePacked(r, s, v), BADGE_ID); + assertEq(token.balanceOf(minters[0]), 1); + + // fail re-minting + vm.expectRevert(); + token.mint(abi.encodePacked(r, s, v), BADGE_ID); + vm.stopBroadcast(); + } + + function test_setMovement_selfWallet() public { + vm.startBroadcast(minters[0]); + + token.setMovement(token.MOVEMENT_BASED()); + assertEq(token.movements(minters[0]), token.MOVEMENT_BASED()); + vm.stopBroadcast(); + } + + function test_setMovement_owner() public { + vm.startBroadcast(owner); + + token.setMovement(minters[0], token.MOVEMENT_BASED()); + assertEq(token.movements(minters[0]), token.MOVEMENT_BASED()); + vm.stopBroadcast(); + } + + function test_revert_setMovement_notOwner() public { + uint256 movement = token.MOVEMENT_BASED(); + vm.startBroadcast(minters[0]); + vm.expectRevert(); + token.setMovement(minters[0], movement); + vm.stopBroadcast(); + } + + function test_uri() public { + uint256 badgeId = token.BADGE_DRUMMERS(); + uint256 movementId = token.MOVEMENT_BASED(); + + // mint the badge + + vm.startBroadcast(owner); + bytes32 _hash = token.getHash(minters[0], badgeId); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], badgeId); + assertTrue(canMint); + + token.mint(abi.encodePacked(r, s, v), minters[0], badgeId); + + // set the user state to based + token.setMovement(minters[0], movementId); + + vm.stopBroadcast(); + + // check the token URI + + uint256 tokenId = token.getTokenId(minters[0], badgeId); + vm.assertEq(tokenId, 1); + + string memory uri = token.tokenURI(tokenId); + + vm.assertEq(uri, "ipfs:///1/5"); + } + + function test_badgeBalances() public { + // mint a token to minter 0 + uint256 badgeId = token.BADGE_DRUMMERS(); + + // mint the badge + + vm.startBroadcast(owner); + bytes32 _hash = token.getHash(minters[0], badgeId); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], badgeId); + assertTrue(canMint); + + token.mint(abi.encodePacked(r, s, v), minters[0], badgeId); + vm.stopBroadcast(); + + bool[] memory badges = token.badgeBalances(minters[0]); + // ensure only badgeId = 5 (Drummers) is true + vm.assertFalse(badges[token.BADGE_RAVERS()]); + vm.assertFalse(badges[token.BADGE_ROBOTS()]); + vm.assertFalse(badges[token.BADGE_BOUNCERS()]); + vm.assertFalse(badges[token.BADGE_MASTERS()]); + vm.assertFalse(badges[token.BADGE_MONKS()]); + vm.assertTrue(badges[token.BADGE_DRUMMERS()]); + vm.assertFalse(badges[token.BADGE_ANDROIDS()]); + vm.assertFalse(badges[token.BADGE_SHINTO()]); + } + + function test_transfer_dataConsistency() public { + // TODO: ensure the values are properly re-assigned after a transfer + + // mint the token for minters[0] + + // mint a token to minter 0 + uint256 badgeId = token.BADGE_DRUMMERS(); + + // mint the badge + + vm.startBroadcast(owner); + bytes32 _hash = token.getHash(minters[0], badgeId); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = token.canMint(abi.encodePacked(r, s, v), minters[0], badgeId); + assertTrue(canMint); + + token.mint(abi.encodePacked(r, s, v), minters[0], badgeId); + vm.stopBroadcast(); + + // transfer to minters[1] + vm.startBroadcast(minters[0]); + token.safeTransferFrom(minters[0], minters[1], 1); + + // ensure the badge balances are consistent + bool[] memory badges = token.badgeBalances(minters[1]); + + // ensure only badgeId = 5 (Drummers) is true + vm.assertFalse(badges[token.BADGE_RAVERS()]); + vm.assertFalse(badges[token.BADGE_ROBOTS()]); + vm.assertFalse(badges[token.BADGE_BOUNCERS()]); + vm.assertFalse(badges[token.BADGE_MASTERS()]); + vm.assertFalse(badges[token.BADGE_MONKS()]); + vm.assertTrue(badges[token.BADGE_DRUMMERS()]); + vm.assertFalse(badges[token.BADGE_ANDROIDS()]); + vm.assertFalse(badges[token.BADGE_SHINTO()]); + + vm.stopBroadcast(); + + // ensure wallets[0] has no badges + badges = token.badgeBalances(minters[0]); + + vm.assertFalse(badges[token.BADGE_RAVERS()]); + vm.assertFalse(badges[token.BADGE_ROBOTS()]); + vm.assertFalse(badges[token.BADGE_BOUNCERS()]); + vm.assertFalse(badges[token.BADGE_MASTERS()]); + vm.assertFalse(badges[token.BADGE_MONKS()]); + vm.assertFalse(badges[token.BADGE_DRUMMERS()]); + vm.assertFalse(badges[token.BADGE_ANDROIDS()]); + vm.assertFalse(badges[token.BADGE_SHINTO()]); + + // check the token IDs + vm.assertEq(token.getTokenId(minters[0], badgeId), 0); + vm.assertEq(token.getTokenId(minters[1], badgeId), 1); + } +}