Skip to content

Commit

Permalink
feat(nfts): trailblazer-badges contract implementation (#17448)
Browse files Browse the repository at this point in the history
Co-authored-by: Kenk <[email protected]>
Co-authored-by: bearni95 <[email protected]>
Co-authored-by: Daniel Wang <[email protected]>
  • Loading branch information
4 people authored Jun 11, 2024
1 parent 1b3eb90 commit 7173639
Show file tree
Hide file tree
Showing 9 changed files with 803 additions and 1 deletion.
149 changes: 149 additions & 0 deletions packages/nfts/contracts/trailblazers-badges/ECDSAWhitelist.sol
Original file line number Diff line number Diff line change
@@ -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 [email protected]
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 { }
}
184 changes: 184 additions & 0 deletions packages/nfts/contracts/trailblazers-badges/TrailblazersBadges.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions packages/nfts/deployments/trailblazers-badges/hekla.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"MintSigner": "0x3cda4F2EaC3fc2FdE78B3DFFe1A1A1Eff88c68c5",
"Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"TrailblazersBadges": "0x8cCE36573293e5bE12F8530f683caa51719cF57E"
}
5 changes: 5 additions & 0 deletions packages/nfts/deployments/trailblazers-badges/localhost.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"MintSigner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"TrailblazersBadges": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
}
5 changes: 5 additions & 0 deletions packages/nfts/deployments/trailblazers-badges/mainnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"MintSigner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"TrailblazersBadges": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
}
4 changes: 3 additions & 1 deletion packages/nfts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 7173639

Please sign in to comment.