-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nfts): trailblazer-badges contract implementation (#17448)
Co-authored-by: Kenk <[email protected]> Co-authored-by: bearni95 <[email protected]> Co-authored-by: Daniel Wang <[email protected]>
- Loading branch information
1 parent
1b3eb90
commit 7173639
Showing
9 changed files
with
803 additions
and
1 deletion.
There are no files selected for viewing
149 changes: 149 additions & 0 deletions
149
packages/nfts/contracts/trailblazers-badges/ECDSAWhitelist.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
184
packages/nfts/contracts/trailblazers-badges/TrailblazersBadges.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"MintSigner": "0x3cda4F2EaC3fc2FdE78B3DFFe1A1A1Eff88c68c5", | ||
"Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", | ||
"TrailblazersBadges": "0x8cCE36573293e5bE12F8530f683caa51719cF57E" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"MintSigner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", | ||
"Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", | ||
"TrailblazersBadges": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"MintSigner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", | ||
"Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", | ||
"TrailblazersBadges": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.