From 524ac559813315bc13ba3e5f0caf57b80a1c90df Mon Sep 17 00:00:00 2001 From: Bernat Canal Garceran Date: Mon, 2 Dec 2024 12:20:54 +0100 Subject: [PATCH] feat(nfts): trailblazers badges s2 (#18040) Co-authored-by: bearni95 --- .../TrailblazersBadgesV3.sol | 2 +- .../BadgeRecruitment.sol | 748 ++++++++++++++++++ .../TrailblazersBadgesS2.sol | 211 +++++ .../TrailblazersS1BadgesV4.sol | 70 ++ packages/nfts/deployments/gen-layouts.sh | 2 + .../nfts/deployments/profile/mainnet.json | 2 +- packages/nfts/deployments/taikoon/hekla.json | 2 +- .../trailblazers-airdrop/hekla.json | 3 +- .../trailblazers-airdrop/mainnet.json | 3 +- .../trailblazers-badges/hekla.json | 6 +- .../trailblazers-season-2/hekla.json | 6 + .../trailblazers-season-2/mainnet.json | 6 + packages/nfts/package.json | 36 +- .../{sol => }/Deploy.s.sol | 11 +- .../{sol/UpgradeV2.sol => UpgradeS1.sol} | 7 +- .../trailblazers-badges}/UpgradeV3.s.sol | 0 .../{sol => }/UpgradeV3.sol | 9 +- .../trailblazers-badges/UpgradeV4.s.sol | 52 ++ .../trailblazers-badges}/Utils.s.sol | 0 .../script/trailblazers-season-2/Deploy.s.sol | 163 ++++ .../trailblazers-season-2/UpgradeV2.s.sol | 50 ++ .../sol => trailblazers-season-2}/Utils.s.sol | 12 +- .../BadgeRecruitment.t.sol | 493 ++++++++++++ .../TrailblazersBadgesS2.t.sol | 111 +++ .../test/util/TrailblazerBadgesS1MintTo.sol | 24 + 25 files changed, 1991 insertions(+), 38 deletions(-) create mode 100644 packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol create mode 100644 packages/nfts/contracts/trailblazers-season-2/TrailblazersBadgesS2.sol create mode 100644 packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol create mode 100644 packages/nfts/deployments/trailblazers-season-2/hekla.json create mode 100644 packages/nfts/deployments/trailblazers-season-2/mainnet.json rename packages/nfts/script/trailblazer/trailblazers-badges/{sol => }/Deploy.s.sol (90%) rename packages/nfts/script/trailblazer/trailblazers-badges/{sol/UpgradeV2.sol => UpgradeS1.sol} (84%) rename packages/nfts/script/{trailblazers-badges/sol => trailblazer/trailblazers-badges}/UpgradeV3.s.sol (100%) rename packages/nfts/script/trailblazer/trailblazers-badges/{sol => }/UpgradeV3.sol (83%) create mode 100644 packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV4.s.sol rename packages/nfts/script/{trailblazers-badges/sol => trailblazer/trailblazers-badges}/Utils.s.sol (100%) create mode 100644 packages/nfts/script/trailblazers-season-2/Deploy.s.sol create mode 100644 packages/nfts/script/trailblazers-season-2/UpgradeV2.s.sol rename packages/nfts/script/{trailblazer/trailblazers-badges/sol => trailblazers-season-2}/Utils.s.sol (88%) create mode 100644 packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol create mode 100644 packages/nfts/test/trailblazers-season-2/TrailblazersBadgesS2.t.sol create mode 100644 packages/nfts/test/util/TrailblazerBadgesS1MintTo.sol diff --git a/packages/nfts/contracts/trailblazers-badges/TrailblazersBadgesV3.sol b/packages/nfts/contracts/trailblazers-badges/TrailblazersBadgesV3.sol index 3d6c0e511ba..cae60cd6468 100644 --- a/packages/nfts/contracts/trailblazers-badges/TrailblazersBadgesV3.sol +++ b/packages/nfts/contracts/trailblazers-badges/TrailblazersBadgesV3.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import "./TrailblazersBadges.sol"; contract TrailblazersBadgesV3 is TrailblazersBadges { - function version() external pure returns (string memory) { + function version() external pure virtual returns (string memory) { return "V3"; } diff --git a/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol b/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol new file mode 100644 index 00000000000..4b2e1f57a90 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol"; +import "../trailblazers-badges/ECDSAWhitelist.sol"; +import "@taiko/blacklist/IMinimalBlacklist.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "./TrailblazersS1BadgesV4.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "./TrailblazersBadgesS2.sol"; + +contract BadgeRecruitment is + UUPSUpgradeable, + Ownable2StepUpgradeable, + AccessControlUpgradeable, + ERC721HolderUpgradeable +{ + /// @notice Season 1 Badges ERC721 contract + TrailblazersBadgesV4 public s1Badges; + /// @notice badges role key + bytes32 public constant S1_BADGES_ROLE = keccak256("S1_BADGES_ROLE"); + /// @notice Season 2 Badges ERC1155 contract + TrailblazersBadgesS2 public s2Badges; + /// @notice Wallet authorized to sign as a source of randomness + address public randomSigner; + /// @notice Recruitment-enabled badge IDs per cycle + //mapping(uint256 cycle => mapping(uint256 s1BadgeId => bool enabled)) public enabledBadgeIds; + // uint256[] public currentCycleEnabledRecruitmentIds; + /// @notice Current recruitment cycle + uint256 public recruitmentCycleId; + + /// @notice Mapping of unique user-per-mint-per-cycle + mapping( + uint256 recruitmentCycle + => mapping( + address minter + => mapping( + uint256 s1BadgeId + => mapping(RecruitmentType recruitmentType => bool mintEnded) + ) + ) + ) public recruitmentCycleUniqueMints; + /// @notice User experience points + + mapping(address user => uint256 experience) public userExperience; + /// @notice Influence colors available + + enum InfluenceColor { + Undefined, // unused + Whale, // based, pink + Minnow // boosted, purple + + } + + /// @notice Recruitment types + enum RecruitmentType { + Undefined, + Claim, + Migration + } + /// @notice Hash types + enum HashType { + Undefined, + Start, + End, + Influence + } + + /// @notice Configuration struct + struct Config { + uint256 cooldownRecruitment; + uint256 cooldownInfluence; + uint256 influenceWeightPercent; + uint256 baseMaxInfluences; + uint256 maxInfluencesDivider; + uint256 defaultCycleDuration; + } + /// @notice Current config + + Config private config; + /// @notice Recruitment struct + + struct Recruitment { + uint256 recruitmentCycle; + address user; + uint256 s1BadgeId; + uint256 s1TokenId; + uint256 s2TokenId; + uint256 cooldownExpiration; + uint256 influenceExpiration; + uint256 whaleInfluences; + uint256 minnowInfluences; + } + /// @notice Recruitment Cycle struct + + struct RecruitmentCycle { + uint256 cycleId; + uint256 startTime; + uint256 endTime; + uint256[] s1BadgeIds; + } + + /// @notice Recruitment cycles + mapping(uint256 cycleId => RecruitmentCycle recruitmentCycle) public recruitmentCycles; + + /// @notice Recruitments per user + + mapping(address _user => Recruitment[] _recruitment) public recruitments; + /// @notice Gap for upgrade safety + uint256[43] private __gap; + /// @notice Errors + + error MAX_INFLUENCES_REACHED(); + error RECRUITMENT_NOT_STARTED(); + error RECRUITMENT_ALREADY_STARTED(); + error INFLUENCE_IN_PROGRESS(); + error RECRUITMENT_NOT_READY(); + error RECRUITMENT_NOT_ENABLED(); + error TOKEN_NOT_OWNED(); + error NOT_RANDOM_SIGNER(); + error ALREADY_MIGRATED_IN_CYCLE(); + error HASH_MISMATCH(); + error NOT_S1_CONTRACT(); + error EXP_TOO_LOW(); + error INVALID_INFLUENCE_COLOR(); + error CURRENT_CYCLE_NOT_OVER(); + /// @notice Events + + event RecruitmentCycleToggled( + uint256 indexed recruitmentCycleId, + uint256 indexed startTime, + uint256 indexed endTime, + uint256[] s1BadgeIds, + bool enabled + ); + + event RecruitmentUpdated( + uint256 indexed recruitmentCycle, + address indexed user, + uint256 s1BadgeId, + uint256 s1TokenId, + uint256 s2TokenId, + uint256 cooldownExpiration, + uint256 influenceExpiration, + uint256 whaleInfluences, + uint256 minnowInfluences + ); + + event RecruitmentComplete( + uint256 indexed recruitmentCycle, + address indexed user, + uint256 s1TokenId, + uint256 s2TokenId, + uint256 finalColor + ); + + /// @notice Check if the message sender has an active recruitment + modifier isMigrating() { + Recruitment memory recruitment_ = getActiveRecruitmentFor(_msgSender()); + if (recruitment_.cooldownExpiration == 0) { + revert RECRUITMENT_NOT_STARTED(); + } + _; + } + + /// @notice Reverts if sender is already migrating + modifier isNotMigrating(address _user) { + if ( + recruitments[_user].length > 0 + && recruitments[_user][recruitments[_user].length - 1].cooldownExpiration + > block.timestamp + ) { + revert RECRUITMENT_ALREADY_STARTED(); + } + _; + } + + /// @notice Reverts if recruitments aren't enabled for that badge + /// @param _s1BadgeId The badge ID + modifier recruitmentOpen(uint256 _s1BadgeId) { + RecruitmentCycle memory cycle_ = recruitmentCycles[recruitmentCycleId]; + + if (cycle_.startTime > block.timestamp || cycle_.endTime < block.timestamp) { + revert RECRUITMENT_NOT_ENABLED(); + } + + bool found_ = false; + + for (uint256 i = 0; i < cycle_.s1BadgeIds.length; i++) { + if (cycle_.s1BadgeIds[i] == _s1BadgeId) { + found_ = true; + break; + } + } + + if (!found_) { + revert RECRUITMENT_NOT_ENABLED(); + } + _; + } + + /// @notice Limits recruitments to one per user, badge and cycle + /// @param _s1BadgeId The badge ID + /// @param _minter The minter address + /// @param _recruitmentType The recruitment type + modifier hasntMigratedInCycle( + uint256 _s1BadgeId, + address _minter, + RecruitmentType _recruitmentType + ) { + // check that the minter hasn't used the recruitment within this cycle + if (recruitmentCycleUniqueMints[recruitmentCycleId][_minter][_s1BadgeId][_recruitmentType]) + { + revert ALREADY_MIGRATED_IN_CYCLE(); + } + _; + } + + /// @notice Contract initializer + /// @param _s1Badges The Season 1 Badges contract address + /// @param _s2Badges The Season 2 Badges contract address + /// @param _randomSigner The random signer address + /// @param _config The initial configuration + function initialize( + address _s1Badges, + address _s2Badges, + address _randomSigner, + Config memory _config + ) + external + initializer + { + _transferOwnership(_msgSender()); + __Context_init(); + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + s1Badges = TrailblazersBadgesV4(_s1Badges); + _grantRole(S1_BADGES_ROLE, _s1Badges); + s2Badges = TrailblazersBadgesS2(_s2Badges); + randomSigner = _randomSigner; + config = _config; + } + + /// @notice Upgrade configuration + /// @param _config The new configuration + function setConfig(Config memory _config) external onlyRole(DEFAULT_ADMIN_ROLE) { + config = _config; + } + + /// @notice Get the current configuration + /// @return The current configuration + function getConfig() external view returns (Config memory) { + return config; + } + + /// @notice Disable all current recruitments + /// @dev Bypasses the default date checks + function forceDisableRecruitments() internal onlyRole(DEFAULT_ADMIN_ROLE) { + recruitmentCycles[recruitmentCycleId].endTime = block.timestamp; + } + + /// @notice Enable recruitments for a set of badges + /// @param _startTime The start time of the recruitment cycle + /// @param _endTime The end time of the recruitment cycle + /// @param _s1BadgeIds The badge IDs to enable + function _enableRecruitments( + uint256 _startTime, + uint256 _endTime, + uint256[] calldata _s1BadgeIds + ) + internal + { + if ( + recruitmentCycleId > 0 + && recruitmentCycles[recruitmentCycleId].endTime > block.timestamp + ) { + revert CURRENT_CYCLE_NOT_OVER(); + } + // emit disabled badges + emit RecruitmentCycleToggled( + recruitmentCycleId, + recruitmentCycles[recruitmentCycleId].startTime, + recruitmentCycles[recruitmentCycleId].endTime, + recruitmentCycles[recruitmentCycleId].s1BadgeIds, + false + ); + + recruitmentCycleId++; + recruitmentCycles[recruitmentCycleId] = + RecruitmentCycle(recruitmentCycleId, _startTime, _endTime, _s1BadgeIds); + + // emit enabled badges + emit RecruitmentCycleToggled(recruitmentCycleId, _startTime, _endTime, _s1BadgeIds, true); + } + + /// @notice Enable recruitments for a set of badges + /// @param _s1BadgeIds The badge IDs to enable + /// @dev Can be called only by the contract owner/admin + function enableRecruitments(uint256[] calldata _s1BadgeIds) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + _enableRecruitments( + block.timestamp, block.timestamp + config.defaultCycleDuration, _s1BadgeIds + ); + } + + /// @notice Enable recruitments for a set of badges + /// @param _startTime The start time of the recruitment cycle + /// @param _endTime The end time of the recruitment cycle + /// @param _s1BadgeIds The badge IDs to enable + /// @dev Can be called only by the contract owner/admin + function enableRecruitments( + uint256 _startTime, + uint256 _endTime, + uint256[] calldata _s1BadgeIds + ) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + _enableRecruitments(_startTime, _endTime, _s1BadgeIds); + } + + /// @notice Get the current recruitment cycle + /// @return The current recruitment cycle + function getRecruitmentCycle(uint256 _cycleId) + external + view + returns (RecruitmentCycle memory) + { + return recruitmentCycles[_cycleId]; + } + + /// @notice Internal logic to start a recruitment + /// @param _user The user address + /// @param _s1BadgeId The badge ID + /// @param _s1TokenId The badge token ID + /// @param _recruitmentType The recruitment type + function _startRecruitment( + address _user, + uint256 _s1BadgeId, + uint256 _s1TokenId, + RecruitmentType _recruitmentType + ) + internal + virtual + { + Recruitment memory _recruitment = Recruitment( + recruitmentCycleId, // recruitmentCycle + _user, // user + _s1BadgeId, + _s1TokenId, + 0, // s2TokenId, unset + block.timestamp + config.cooldownRecruitment, // cooldownExpiration + 0, // influenceExpiration, unset + 0, // whaleInfluences + 0 // minnowInfluences + ); + + recruitments[_user].push(_recruitment); + recruitmentCycleUniqueMints[recruitmentCycleId][_user][_s1BadgeId][_recruitmentType] = true; + + emit RecruitmentUpdated( + _recruitment.recruitmentCycle, + _recruitment.user, + _recruitment.s1BadgeId, + _recruitment.s1TokenId, + _recruitment.s2TokenId, + _recruitment.cooldownExpiration, + _recruitment.influenceExpiration, + _recruitment.whaleInfluences, + _recruitment.minnowInfluences + ); + } + + /// @notice Start a recruitment for a badge using the user's experience points + /// @param _hash The hash to sign of the signature + /// @param _v The signature V field + /// @param _r The signature R field + /// @param _s The signature S field + /// @param _exp The user's experience points + function startRecruitment( + bytes32 _hash, + uint8 _v, + bytes32 _r, + bytes32 _s, + uint256 _exp + ) + external + virtual + isNotMigrating(_msgSender()) + { + bytes32 calculatedHash_ = generateClaimHash(HashType.Start, _msgSender(), _exp); + + if (calculatedHash_ != _hash) { + revert HASH_MISMATCH(); + } + + (address recovered_,,) = ECDSA.tryRecover(_hash, _v, _r, _s); + if (recovered_ != randomSigner) { + revert NOT_RANDOM_SIGNER(); + } + + if (_exp < userExperience[_msgSender()]) { + revert EXP_TOO_LOW(); + } + + userExperience[_msgSender()] = _exp; + + RecruitmentCycle memory cycle_ = recruitmentCycles[recruitmentCycleId]; + if (cycle_.startTime > block.timestamp || cycle_.endTime < block.timestamp) { + revert RECRUITMENT_NOT_ENABLED(); + } + uint256 randomSeed_ = randomFromSignature(_hash, _v, _r, _s); + uint256 s1BadgeId_ = cycle_.s1BadgeIds[randomSeed_ % cycle_.s1BadgeIds.length]; + + if ( + recruitmentCycleUniqueMints[recruitmentCycleId][_msgSender()][s1BadgeId_][RecruitmentType + .Claim] + ) { + revert ALREADY_MIGRATED_IN_CYCLE(); + } + + _startRecruitment(_msgSender(), s1BadgeId_, 0, RecruitmentType.Claim); + } + + /// @notice Start a recruitment for a badge using the user's experience points + /// @param _hash The hash to sign of the signature + /// @param _v The signature V field + /// @param _r The signature R field + /// @param _s The signature S field + /// @param _exp The user's experience points + /// @param _s1BadgeId The badge ID (s1) + function startRecruitment( + bytes32 _hash, + uint8 _v, + bytes32 _r, + bytes32 _s, + uint256 _exp, + uint256 _s1BadgeId + ) + external + virtual + isNotMigrating(_msgSender()) + recruitmentOpen(_s1BadgeId) + hasntMigratedInCycle(_s1BadgeId, _msgSender(), RecruitmentType.Claim) + { + bytes32 calculatedHash_ = generateClaimHash(HashType.Start, _msgSender(), _s1BadgeId); + + if (calculatedHash_ != _hash) { + revert HASH_MISMATCH(); + } + + (address recovered_,,) = ECDSA.tryRecover(_hash, _v, _r, _s); + if (recovered_ != randomSigner) { + revert NOT_RANDOM_SIGNER(); + } + + if (_exp < userExperience[_msgSender()]) { + revert EXP_TOO_LOW(); + } + + userExperience[_msgSender()] = _exp; + + _startRecruitment(_msgSender(), _s1BadgeId, 0, RecruitmentType.Claim); + } + + /// @notice Start a recruitment for a badge + /// @param _s1BadgeId The badge ID (s1) + /// @dev Not all badges are eligible for recruitment at the same time + /// @dev Defines a cooldown for the recruitment to be complete + /// @dev the cooldown is lesser the higher the Pass Tier + /// @dev Must be called from the s1 badges contract + function startRecruitment( + address _user, + uint256 _s1BadgeId + ) + external + virtual + onlyRole(S1_BADGES_ROLE) + recruitmentOpen(_s1BadgeId) + isNotMigrating(_user) + hasntMigratedInCycle(_s1BadgeId, _user, RecruitmentType.Migration) + { + uint256 s1TokenId_ = s1Badges.getTokenId(_user, _s1BadgeId); + + if (s1Badges.ownerOf(s1TokenId_) != _user) { + revert TOKEN_NOT_OWNED(); + } + _startRecruitment(_user, _s1BadgeId, s1TokenId_, RecruitmentType.Migration); + } + + /// @notice Get the active recruitment for a user + /// @param _user The user address + /// @return The active recruitment + function getActiveRecruitmentFor(address _user) public view returns (Recruitment memory) { + if (recruitments[_user].length == 0) { + revert RECRUITMENT_NOT_STARTED(); + } + return recruitments[_user][recruitments[_user].length - 1]; + } + + /// @notice Update a recruitment + /// @param _recruitment The updated recruitment + function _updateRecruitment(Recruitment memory _recruitment) internal virtual { + recruitments[_recruitment.user][recruitments[_recruitment.user].length - 1] = _recruitment; + + emit RecruitmentUpdated( + _recruitment.recruitmentCycle, + _recruitment.user, + _recruitment.s1BadgeId, + _recruitment.s1TokenId, + _recruitment.s2TokenId, + _recruitment.cooldownExpiration, + _recruitment.influenceExpiration, + _recruitment.whaleInfluences, + _recruitment.minnowInfluences + ); + } + + /// @notice Get the maximum number of influences for a given experience + /// @param _exp The user's experience points + function maxInfluences(uint256 _exp) public view virtual returns (uint256 value) { + value = 0; // _exp / config.maxInfluencesDivider; + value += config.baseMaxInfluences; + return value; + } + + /// @notice Influence (alter) the chances during a recruitment + /// @param _hash The hash to sign + /// @param _v signature V field + /// @param _r signature R field + /// @param _s signature S field + /// @param _influenceColor the influence's color + /// @dev Can be called only during an active recruitment + /// @dev Implements a cooldown before allowing to re-influence + /// @dev The max influence amount is determined by Pass Tier + function influenceRecruitment( + bytes32 _hash, + uint8 _v, + bytes32 _r, + bytes32 _s, + uint256 _exp, + InfluenceColor _influenceColor + ) + external + isMigrating + { + bytes32 calculatedHash_ = generateClaimHash(HashType.Influence, _msgSender(), _exp); + + if (calculatedHash_ != _hash) { + revert HASH_MISMATCH(); + } + + (address recovered_,,) = ECDSA.tryRecover(_hash, _v, _r, _s); + if (recovered_ != randomSigner) revert NOT_RANDOM_SIGNER(); + Recruitment memory recruitment_ = getActiveRecruitmentFor(_msgSender()); + + if ((recruitment_.whaleInfluences + recruitment_.minnowInfluences) >= maxInfluences(_exp)) { + revert MAX_INFLUENCES_REACHED(); + } + + if (recruitment_.influenceExpiration > block.timestamp) { + revert INFLUENCE_IN_PROGRESS(); + } + + // apply the influence, and reset the other + if (_influenceColor == InfluenceColor.Whale) { + recruitment_.whaleInfluences++; + recruitment_.minnowInfluences = 0; + } else if (_influenceColor == InfluenceColor.Minnow) { + recruitment_.minnowInfluences++; + recruitment_.whaleInfluences = 0; + } else { + revert INVALID_INFLUENCE_COLOR(); + } + + recruitment_.influenceExpiration = block.timestamp + config.cooldownInfluence; + + _updateRecruitment(recruitment_); + } + + /// @notice End a recruitment + /// @param _hash The hash to sign + /// @param _v signature V field + /// @param _r signature R field + /// @param _s signature S field + /// @param _exp The user's experience points + /// @dev Can be called only during an active recruitment, after the cooldown is over + /// @dev The final color is determined randomly, and affected by the influence amounts + function endRecruitment( + bytes32 _hash, + uint8 _v, + bytes32 _r, + bytes32 _s, + uint256 _exp + ) + external + isMigrating + { + Recruitment memory recruitment_ = getActiveRecruitmentFor(_msgSender()); + + if (recruitment_.influenceExpiration > block.timestamp) { + revert INFLUENCE_IN_PROGRESS(); + } + // check if the cooldown is over + if (recruitment_.cooldownExpiration > block.timestamp) { + revert RECRUITMENT_NOT_READY(); + } + // ensure the hash corresponds to the start time + bytes32 calculatedHash_ = generateClaimHash(HashType.End, _msgSender(), _exp); + + if (calculatedHash_ != _hash) { + revert HASH_MISMATCH(); + } + + uint256 randomSeed_ = randomFromSignature(_hash, _v, _r, _s); + + uint256 whaleWeight_ = 50 + recruitment_.whaleInfluences * config.influenceWeightPercent; + uint256 minnowWeight_ = 50 + recruitment_.minnowInfluences * config.influenceWeightPercent; + + uint256 totalWeight_ = whaleWeight_ + minnowWeight_; + + uint256 randomValue = randomSeed_ % totalWeight_; + + TrailblazersBadgesS2.MovementType finalColor_; + if (randomValue < minnowWeight_) { + finalColor_ = TrailblazersBadgesS2.MovementType.Minnow; + } else { + finalColor_ = TrailblazersBadgesS2.MovementType.Whale; + } + + uint256 s1BadgeId_ = recruitment_.s1BadgeId; + + // mint the badge + s2Badges.mint(_msgSender(), TrailblazersBadgesS2.BadgeType(s1BadgeId_), finalColor_); + uint256 s2TokenId_ = s2Badges.totalSupply(); + + recruitment_.s2TokenId = s2TokenId_; + recruitment_.cooldownExpiration = 0; + recruitment_.influenceExpiration = 0; + + _updateRecruitment(recruitment_); + + emit RecruitmentComplete( + recruitment_.recruitmentCycle, + recruitment_.user, + recruitment_.s1TokenId, + recruitment_.s2TokenId, + uint256(finalColor_) + ); + } + + /// @notice Generate a unique hash for each recruitment uniquely + /// @param _user The user address + /// @param _exp The users experience points + /// @return _hash The unique hash + function generateClaimHash( + HashType _hashType, + address _user, + uint256 _exp + ) + public + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(_hashType, _user, _exp)); + } + + /// @notice Check if a recruitment is active for a user + /// @param _user The user address + /// @return Whether the user has an active recruitment + function isRecruitmentActive(address _user) public view returns (bool) { + if (recruitments[_user].length == 0) { + return false; + } + Recruitment memory recruitment_ = getActiveRecruitmentFor(_user); + return recruitment_.cooldownExpiration != 0; + } + + /// @notice Generates a random number from a signature + /// @param _hash The hash to sign + /// @param _v signature V field + /// @param _r signature R field + /// @param _s signature S field + /// @return _random The pseudo-random number + function randomFromSignature( + bytes32 _hash, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + public + view + returns (uint256 _random) + { + (address recovered_,,) = ECDSA.tryRecover(_hash, _v, _r, _s); + if (recovered_ != randomSigner) revert NOT_RANDOM_SIGNER(); + return uint256(keccak256(abi.encodePacked(_r, _s, _v))); + } + + /// @notice Check if a influence is active for a user + /// @param _user The user address + /// @return Whether the user has an active influence + function isInfluenceActive(address _user) public view returns (bool) { + Recruitment memory recruitment_ = getActiveRecruitmentFor(_user); + return recruitment_.influenceExpiration > block.timestamp; + } + + /// @notice Get the recruitment influence counts for a user + /// @param _user The user address + /// @return _whaleInfluences The Whale influence count + /// @return _minnowInfluences The Minnow influence count + function getRecruitmentInfluences(address _user) + public + view + returns (uint256 _whaleInfluences, uint256 _minnowInfluences) + { + if (!isRecruitmentActive(_user)) { + revert RECRUITMENT_NOT_STARTED(); + } + Recruitment memory recruitment_ = getActiveRecruitmentFor(_user); + return (recruitment_.whaleInfluences, recruitment_.minnowInfluences); + } + + /// @notice supportsInterface implementation + /// @param _interfaceId The interface ID + /// @return Whether the interface is supported + function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { + return super.supportsInterface(_interfaceId); + } + + /// @notice Internal method to authorize an upgrade + function _authorizeUpgrade(address) internal virtual override onlyOwner { } +} diff --git a/packages/nfts/contracts/trailblazers-season-2/TrailblazersBadgesS2.sol b/packages/nfts/contracts/trailblazers-season-2/TrailblazersBadgesS2.sol new file mode 100644 index 00000000000..5984e3a3e75 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-season-2/TrailblazersBadgesS2.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol"; +import "../trailblazers-badges/ECDSAWhitelist.sol"; +import "@taiko/blacklist/IMinimalBlacklist.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "./TrailblazersS1BadgesV4.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +contract TrailblazersBadgesS2 is + ContextUpgradeable, + UUPSUpgradeable, + Ownable2StepUpgradeable, + AccessControlUpgradeable, + ERC1155SupplyUpgradeable +{ + /// @notice Badge types + enum BadgeType { + Ravers, // s1 id: 0 + Robots, // s1 id: 1 + Bouncers, // s1 id: 2 + Masters, // s1 id: 3 + Monks, // s1 id: 4 + Androids, // s1 id: 5 + Drummers, // s1 id: 6 + Shinto // s1 id: 7 + + } + + /// @notice Movement types + enum MovementType { + Undefined, // unused + Whale, // s1 based/pink + Minnow // s1 boosted/purple + + } + + /// @notice Badge struct + struct Badge { + uint256 tokenId; + BadgeType badgeType; + MovementType movementType; + } + + /// @notice Badge mapping + mapping(uint256 tokenId => Badge badge) private badges; + /// @notice User, Badge, and Movement relation to tokenId + mapping( + address user + => mapping(BadgeType badgeType => mapping(MovementType movementType => uint256 tokenId)) + ) private userBadges; + /// @notice Badge URI template + string public uriTemplate; + /// @notice Minter address; BadgeMigration contract + address public minter; + /// @notice Minter role + bytes32 public constant MINTER_ROLE = keccak256("MINTER"); + /// @notice Gap for upgrade safety + uint256[43] private __gap; + + /// @notice Errors + error NOT_MINTER(); + error TOKEN_NOT_MINTED(); + + /// @notice Initialize the contract + /// @param _minter The minter address + /// @param _uriTemplate The badge URI template + function initialize( + address _minter, + string calldata _uriTemplate + ) + external + virtual + initializer + { + __ERC1155_init(""); + __ERC1155Supply_init(); + _transferOwnership(_msgSender()); + __Context_init(); + _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _grantRole(MINTER_ROLE, _minter); + minter = _minter; + uriTemplate = _uriTemplate; + } + + /// @notice Set the minter address + /// @param _minter The minter address + /// @dev Only the owner can call this function + function setMinter(address _minter) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + minter = _minter; + _grantRole(MINTER_ROLE, _minter); + } + + /// @notice Mint a badge + /// @param _to The address to mint the badge to + /// @param _badgeType The badge type + /// @param _movementType The movement type + /// @dev Only the minter can call this function + function mint( + address _to, + BadgeType _badgeType, + MovementType _movementType + ) + external + virtual + onlyRole(MINTER_ROLE) + { + uint256 tokenId_ = totalSupply() + 1; + Badge memory badge_ = Badge(tokenId_, _badgeType, _movementType); + _mint(_to, tokenId_, 1, ""); + badges[tokenId_] = badge_; + } + + /// @notice Internal method to assemble URIs + /// @param _badgeType The badge type + /// @param _movementType The movement type + /// @return The URI + function _uri( + BadgeType _badgeType, + MovementType _movementType + ) + internal + view + virtual + returns (string memory) + { + string memory badgeType_ = Strings.toString(uint256(_badgeType)); + string memory movementType_ = Strings.toString(uint256(_movementType)); + + return string(abi.encodePacked(uriTemplate, "/", movementType_, "/", badgeType_)); + } + + /// @notice Retrieve the URI for a badge given the type & movement + /// @param _badgeType The badge type + /// @param _movementType The movement type + /// @return The URI + function uri( + BadgeType _badgeType, + MovementType _movementType + ) + external + view + virtual + returns (string memory) + { + return _uri(_badgeType, _movementType); + } + + /// @notice Retrieve the URI for a badge given the token ID + /// @param _tokenId The token ID + /// @return The URI + function uri(uint256 _tokenId) public view virtual override returns (string memory) { + if (_tokenId > totalSupply()) { + revert TOKEN_NOT_MINTED(); + } + Badge memory badge_ = badges[_tokenId]; + return _uri(badge_.badgeType, badge_.movementType); + } + + /// @notice Retrieve a badge + /// @param _tokenId The token ID + /// @return The badge + function getBadge(uint256 _tokenId) external view virtual returns (Badge memory) { + if (_tokenId < totalSupply()) { + revert TOKEN_NOT_MINTED(); + } + return badges[_tokenId]; + } + + /// @notice supportsInterface implementation + /// @param _interfaceId The interface ID + /// @return Whether the interface is supported + function supportsInterface(bytes4 _interfaceId) + public + view + virtual + override(AccessControlUpgradeable, ERC1155Upgradeable) + returns (bool) + { + return super.supportsInterface(_interfaceId); + } + + /// @notice Internal method to authorize an upgrade + function _authorizeUpgrade(address) internal virtual override onlyOwner { } + + // v2 + + function version() public pure returns (string memory) { + return "v2"; + } + + function setUri(string memory __uri) public onlyRole(DEFAULT_ADMIN_ROLE) { + uriTemplate = __uri; + } +} diff --git a/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol b/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol new file mode 100644 index 00000000000..c272a552667 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "../trailblazers-badges/TrailblazersBadgesV3.sol"; +import "./BadgeRecruitment.sol"; + +contract TrailblazersBadgesV4 is TrailblazersBadgesV3 { + /// @notice Duration for which a s1 badge is locked after recruitment is started + uint256 public recruitmentLockDuration; + /// @notice BadgeRecruitment contract + BadgeRecruitment public recruitmentContract; + /// @notice Mapping of badge token id to unlock timestamp + mapping(uint256 tokenId => uint256 unlockTimestamp) public unlockTimestamps; + + /// @notice Errors + error BADGE_LOCKED(); + error RECRUITMENT_LOCK_DURATION_NOT_SET(); + + /// @notice Updated version function + /// @return Version string + function version() external pure virtual override returns (string memory) { + return "V4"; + } + + /// @notice Overwritten update function that prevents locked badges from being transferred + /// @param to Address to transfer badge to + /// @param tokenId Badge token id + /// @param auth Address to authorize transfer + /// @return Address of the recipient + function _update( + address to, + uint256 tokenId, + address auth + ) + internal + virtual + override + returns (address) + { + if (unlockTimestamps[tokenId] > block.timestamp) { + revert BADGE_LOCKED(); + } + return super._update(to, tokenId, auth); + } + + /// @notice Set recruitment contract + /// @param _recruitmentContract Address of the recruitment contract + /// @dev Only owner + function setRecruitmentContract(address _recruitmentContract) public onlyOwner { + recruitmentContract = BadgeRecruitment(_recruitmentContract); + } + + /// @notice Set recruitment lock duration + /// @param _duration Duration in seconds + /// @dev Only owner + function setRecruitmentLockDuration(uint256 _duration) public onlyOwner { + recruitmentLockDuration = _duration; + } + + /// @notice Start recruitment for a badge + /// @param _badgeId Badge id + function startRecruitment(uint256 _badgeId) public { + if (recruitmentLockDuration == 0) { + revert RECRUITMENT_LOCK_DURATION_NOT_SET(); + } + uint256 tokenId = getTokenId(_msgSender(), _badgeId); + unlockTimestamps[tokenId] = block.timestamp + recruitmentLockDuration; + recruitmentContract.startRecruitment(_msgSender(), _badgeId); + } +} diff --git a/packages/nfts/deployments/gen-layouts.sh b/packages/nfts/deployments/gen-layouts.sh index 2d0fbb034b4..6f647cfb56c 100755 --- a/packages/nfts/deployments/gen-layouts.sh +++ b/packages/nfts/deployments/gen-layouts.sh @@ -7,6 +7,8 @@ contracts=( "SnaefellToken" "ECDSAWhitelist" "TrailblazersBadges" + "TrailblazersBadgesV2" + "BadgeMigration" ) # Empty the output file initially diff --git a/packages/nfts/deployments/profile/mainnet.json b/packages/nfts/deployments/profile/mainnet.json index eb752c06c71..4093af55cab 100644 --- a/packages/nfts/deployments/profile/mainnet.json +++ b/packages/nfts/deployments/profile/mainnet.json @@ -1,3 +1,3 @@ { - "RegisterProfilePicture": "0xCbC7846351BaEaB1ed51f8FF57e10C367E01488A" + "RegisterProfilePicture": "0x58617427f3d42e5435908661d3c788d7d2EAf3fa" } diff --git a/packages/nfts/deployments/taikoon/hekla.json b/packages/nfts/deployments/taikoon/hekla.json index 6d54379cbe2..f9241cbc4ef 100644 --- a/packages/nfts/deployments/taikoon/hekla.json +++ b/packages/nfts/deployments/taikoon/hekla.json @@ -1,5 +1,5 @@ { "MerkleRoot": "0x1c3b504b4d5640d26ad1aa3b57a9df9ec034f19239768e734b849c306d10b110", "Owner": "0x4100a9B680B1Be1F10Cb8b5a57fE59eA77A8184e", - "TaikoonToken": "0xf3FBa6f1E6C1998195239e7DD794c1EcEA8Da66B" + "TaikoonToken": "0x6e68900B53D6de5c20A4b81CE42A488b887f40Ce" } diff --git a/packages/nfts/deployments/trailblazers-airdrop/hekla.json b/packages/nfts/deployments/trailblazers-airdrop/hekla.json index 5c8dd9aabf2..10960be94d8 100644 --- a/packages/nfts/deployments/trailblazers-airdrop/hekla.json +++ b/packages/nfts/deployments/trailblazers-airdrop/hekla.json @@ -1,4 +1,5 @@ { "ERC20Airdrop": "0xdeC2662Dff4eAB8b94B5257D637204d18D95cb74", - "MerkleRoot": "0xbe8ec647626f95185f551887b3eee43ea9e8965c7baf558a9f8cb22b020597f0" + "MerkleRoot": "0xbe8ec647626f95185f551887b3eee43ea9e8965c7baf558a9f8cb22b020597f0", + "ERC20Token": "0xa9d23408b9ba935c230493c40c73824df71a0975" } diff --git a/packages/nfts/deployments/trailblazers-airdrop/mainnet.json b/packages/nfts/deployments/trailblazers-airdrop/mainnet.json index 289f920f46b..d0f2601ab76 100644 --- a/packages/nfts/deployments/trailblazers-airdrop/mainnet.json +++ b/packages/nfts/deployments/trailblazers-airdrop/mainnet.json @@ -1,4 +1,5 @@ { "ERC20Airdrop": "0x290265ACd21816EE414E64eEC77dd490d8dd9f51", - "MerkleRoot": "0xc7f7e6bb3d1bb31b0ef5e2e34383c12ec9ef8a301ffde9771bd9de7554c70b1d" + "MerkleRoot": "0xc7f7e6bb3d1bb31b0ef5e2e34383c12ec9ef8a301ffde9771bd9de7554c70b1d", + "ERC20Token": "0xa9d23408b9ba935c230493c40c73824df71a0975" } diff --git a/packages/nfts/deployments/trailblazers-badges/hekla.json b/packages/nfts/deployments/trailblazers-badges/hekla.json index 5730ee038c0..ecc500a7b7c 100644 --- a/packages/nfts/deployments/trailblazers-badges/hekla.json +++ b/packages/nfts/deployments/trailblazers-badges/hekla.json @@ -1,5 +1,5 @@ { - "MintSigner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "Owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "TrailblazersBadges": "0x4406dB2850EcE5e44B93a7b0296229DF882d34e6" + "Owner": "0x4100a9B680B1Be1F10Cb8b5a57fE59eA77A8184e", + "TrailblazersBadges": "0xe20616Be39a58cC990c3B8ab069F31e230BeD74D", + "TrailblazersBadgesS2": "0x5dC1F8ef5c7f4f40AdA892B0988b563e71f03eEd" } diff --git a/packages/nfts/deployments/trailblazers-season-2/hekla.json b/packages/nfts/deployments/trailblazers-season-2/hekla.json new file mode 100644 index 00000000000..0b70fbc0bf4 --- /dev/null +++ b/packages/nfts/deployments/trailblazers-season-2/hekla.json @@ -0,0 +1,6 @@ +{ + "BadgeRecruitment": "0xBd368C65Cb354eBAd6c1429b551bD0197f19C2B8", + "Owner": "0x4100a9B680B1Be1F10Cb8b5a57fE59eA77A8184e", + "TrailblazersBadges": "0x9E14C357E964BeE012bA82Ce9d6513dAec6ea961", + "TrailblazersBadgesS2": "0xc84B76a5836Cb0CeF094808af445F7E98504ED5B" +} diff --git a/packages/nfts/deployments/trailblazers-season-2/mainnet.json b/packages/nfts/deployments/trailblazers-season-2/mainnet.json new file mode 100644 index 00000000000..4430837567d --- /dev/null +++ b/packages/nfts/deployments/trailblazers-season-2/mainnet.json @@ -0,0 +1,6 @@ +{ + "BadgeRecruitment": "0xa9Ceb04F3aF71fF123409d426A92BABb5124970C", + "Owner": "0x7d70236E2517f5B95247AF1d806A9E3C328a7860", + "TrailblazersBadges": "0xa20a8856e00F5ad024a55A663F06DCc419FFc4d5", + "TrailblazersBadgesS2": "0x52A7dBeC10B404548066F59DE89484e27b4181dA" +} diff --git a/packages/nfts/package.json b/packages/nfts/package.json index 6f7be4c873c..41926afa56c 100644 --- a/packages/nfts/package.json +++ b/packages/nfts/package.json @@ -12,32 +12,36 @@ "node": "anvil", "layout": "./deployments/gen-layouts.sh", "taikoon:merkle": "node script/taikoon/js/generate-merkle-tree.js", - "snaefell:merkle": "node script/snaefell/js/generate-merkle-tree.js", - "taikoon:deploy:localhost": "forge clean && pnpm compile && forge script script/taikoon/sol/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast", - "snaefell:deploy:localhost": "forge clean && pnpm compile && forge script script/snaefell/sol/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast", + "taikoon:deploy:localhost": "forge clean && forge script script/taikoon/sol/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast", "taikoon:deploy:ipfs": "rm -rf data/taikoon/metadata/* && node script/taikoon/js/4everland.js", + "taikoon:deploy:devnet": "forge clean && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://rpc.internal.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "taikoon:deploy:mainnet": "forge clean && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 13 ", + "taikoon:deploy:holesky": "forge clean && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://1rpc.io/holesky --broadcast --gas-estimate-multiplier 200", + "taikoon:deploy:v2": "forge clean && forge script script/taikoon/sol/UpgradeV2.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast", + "snaefell:merkle": "node script/snaefell/js/generate-merkle-tree.js", + "snaefell:deploy:localhost": "forge clean && forge script script/snaefell/sol/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast", "snaefell:deploy:ipfs": "rm -rf data/snaefell/metadata/* && node script/snaefell/js/4everland.js", - "taikoon:deploy:devnet": "forge clean && pnpm compile && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://rpc.internal.taiko.xyz --broadcast --gas-estimate-multiplier 200", - "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:hekla": "forge clean && pnpm compile && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --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", - "galxe:deploy:mainnet": "forge clean && pnpm compile && forge script script/galxe/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --legacy --with-gas-price 1", - "tbzb:deploy:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 13 ", - "taikoon:deploy:v2": "forge clean && pnpm compile && forge script script/taikoon/sol/UpgradeV2.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast", - "kbw:deploy:hekla": "forge clean && pnpm compile && forge script script/party-ticket/sol/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "snaefell:deploy:devnet": "forge clean && forge script script/snaefell/sol/Deploy.s.sol --rpc-url https://rpc.internal.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "snaefell:deploy:mainnet": "forge clean && forge script script/snaefell/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 13 ", "kbw:deploy:mainnet": "forge clean && pnpm compile && forge script script/party-ticket/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 30 ", "kbw:upgradeV2:hekla": "forge clean && pnpm compile && forge script script/party-ticket/sol/UpgradeV2.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", "kbw:upgradeV2:mainnet": "forge clean && pnpm compile && forge script script/party-ticket/sol/UpgradeV2.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast", + "tbzb:deploy:localhost": "forge clean && forge script script/trailblazers-badges/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast", + "tbzb:deploy:hekla": "forge clean && forge script script/trailblazers-badges/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "galxe:deploy:mainnet": "forge clean && forge script script/galxe/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --legacy --with-gas-price 1", + "tbzb:deploy:mainnet": "forge clean && forge script script/trailblazers-badges/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 13 ", + "kbw:deploy:hekla": "forge clean && forge script script/party-ticket/sol/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "tbz:s2:deploy:hekla": "forge clean && forge script script/trailblazers-season-2/Deploy.s.sol --tc DeployS2Script --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "tbz:s2:deploy:mainnet": "forge clean && forge script script/trailblazers-season-2/Deploy.s.sol --tc DeployS2Script --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "taikoon:deploy:hekla": "forge clean && pnpm compile && forge script script/taikoon/sol/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", "pfp:deploy:hekla": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", "pfp:deploy:mainnet": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 200", "tbz:airdrop:hekla": "forge clean && pnpm compile && forge script script/trailblazers-airdrop/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", "tbz:airdrop:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-airdrop/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 100", "tbz:upgradeV3:hekla": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/UpgradeV3.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", - "tbz:upgradeV3:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/UpgradeV3.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 100" + "tbz:upgradeV3:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-badges/sol/UpgradeV3.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 100", + "tbz:upgradeV4:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-badges/UpgradeV4.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 100", + "tbz-s2:upgradeV2:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-season-2/UpgradeV2.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 100" }, "devDependencies": { "@types/node": "^20.11.30", diff --git a/packages/nfts/script/trailblazer/trailblazers-badges/sol/Deploy.s.sol b/packages/nfts/script/trailblazer/trailblazers-badges/Deploy.s.sol similarity index 90% rename from packages/nfts/script/trailblazer/trailblazers-badges/sol/Deploy.s.sol rename to packages/nfts/script/trailblazer/trailblazers-badges/Deploy.s.sol index 63bb1ea352e..932321e9237 100644 --- a/packages/nfts/script/trailblazer/trailblazers-badges/sol/Deploy.s.sol +++ b/packages/nfts/script/trailblazer/trailblazers-badges/Deploy.s.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import { UtilsScript } from "./Utils.s.sol"; +import { UtilsScript, MockBlacklist } 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 { TrailblazersBadges } from "../../../contracts/trailblazers-badges/TrailblazersBadges.sol"; import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; contract DeployScript is Script { @@ -50,6 +49,12 @@ contract DeployScript is Script { vm.startBroadcast(deployerPrivateKey); + if (block.chainid == 167_000) { + // mainnet, use existing blacklist + } else { + blacklist = new MockBlacklist(); + } + // deploy token with empty root address impl = address(new TrailblazersBadges()); address proxy = address( diff --git a/packages/nfts/script/trailblazer/trailblazers-badges/sol/UpgradeV2.sol b/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeS1.sol similarity index 84% rename from packages/nfts/script/trailblazer/trailblazers-badges/sol/UpgradeV2.sol rename to packages/nfts/script/trailblazer/trailblazers-badges/UpgradeS1.sol index a9d80fd2993..662a98a3561 100644 --- a/packages/nfts/script/trailblazer/trailblazers-badges/sol/UpgradeV2.sol +++ b/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeS1.sol @@ -5,8 +5,7 @@ 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 { TrailblazersBadges } from "../../../contracts/trailblazers-badges/TrailblazersBadges.sol"; import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; contract UpgradeV2 is Script { @@ -15,7 +14,7 @@ contract UpgradeV2 is Script { uint256 public deployerPrivateKey; address public deployerAddress; - address tokenV1 = 0xa20a8856e00F5ad024a55A663F06DCc419FFc4d5; + address s1Token = 0xa20a8856e00F5ad024a55A663F06DCc419FFc4d5; TrailblazersBadges public token; function setUp() public { @@ -28,7 +27,7 @@ contract UpgradeV2 is Script { } function run() public { - token = TrailblazersBadges(tokenV1); + token = TrailblazersBadges(s1Token); vm.startBroadcast(deployerPrivateKey); token.upgradeToAndCall( diff --git a/packages/nfts/script/trailblazers-badges/sol/UpgradeV3.s.sol b/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV3.s.sol similarity index 100% rename from packages/nfts/script/trailblazers-badges/sol/UpgradeV3.s.sol rename to packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV3.s.sol diff --git a/packages/nfts/script/trailblazer/trailblazers-badges/sol/UpgradeV3.sol b/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV3.sol similarity index 83% rename from packages/nfts/script/trailblazer/trailblazers-badges/sol/UpgradeV3.sol rename to packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV3.sol index 472192164cd..e212f866987 100644 --- a/packages/nfts/script/trailblazer/trailblazers-badges/sol/UpgradeV3.sol +++ b/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV3.sol @@ -5,13 +5,12 @@ 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 { TrailblazersBadges } from "../../../contracts/trailblazers-badges/TrailblazersBadges.sol"; import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; import { TrailblazersBadgesV3 } from - "../../../../contracts/trailblazers-badges/TrailblazersBadgesV3.sol"; + "../../../contracts/trailblazers-badges/TrailblazersBadgesV3.sol"; -contract UpgradeV2 is Script { +contract UpgradeV3 is Script { UtilsScript public utils; string public jsonLocation; uint256 public deployerPrivateKey; @@ -38,7 +37,7 @@ contract UpgradeV2 is Script { address(new TrailblazersBadgesV3()), abi.encodeCall(TrailblazersBadgesV3.version, ()) ); - tokenV3 = TrailblazersBadgesV3(tokenV3); + tokenV3 = TrailblazersBadgesV3(address(tokenV2)); console.log("Upgraded TrailblazersBadgesV3 to:", address(tokenV3)); } diff --git a/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV4.s.sol b/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV4.s.sol new file mode 100644 index 00000000000..d49ad9cb55a --- /dev/null +++ b/packages/nfts/script/trailblazer/trailblazers-badges/UpgradeV4.s.sol @@ -0,0 +1,52 @@ +// 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"; +import { TrailblazersBadgesV3 } from + "../../../contracts/trailblazers-badges/TrailblazersBadgesV3.sol"; +import { TrailblazersBadgesV4 } from + "../../../contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol"; + +contract UpgradeV4 is Script { + UtilsScript public utils; + string public jsonLocation; + uint256 public deployerPrivateKey; + address public deployerAddress; + + address tokenV3Address = 0xa20a8856e00F5ad024a55A663F06DCc419FFc4d5; + TrailblazersBadgesV3 public tokenV3; + TrailblazersBadgesV4 public tokenV4; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + + jsonLocation = utils.getContractJsonLocation(); + deployerPrivateKey = utils.getPrivateKey(); + deployerAddress = utils.getAddress(); + } + + function run() public { + vm.startBroadcast(deployerPrivateKey); + tokenV3 = TrailblazersBadgesV3(tokenV3Address); + + tokenV3.upgradeToAndCall( + address(new TrailblazersBadgesV4()), abi.encodeCall(TrailblazersBadgesV4.version, ()) + ); + + tokenV4 = TrailblazersBadgesV4(address(tokenV3)); + + console.log("Upgraded TrailblazersBadgesV3 to:", address(tokenV4)); + + // update uri + tokenV4.setUri( + "https://taikonfts.4everland.link/ipfs/bafybeiatuzeeeznd3hi5qiulslxcjd22ebu45t4fra2jvi3smhocr2c66a" + ); + console.log("Updated token URI"); + } +} diff --git a/packages/nfts/script/trailblazers-badges/sol/Utils.s.sol b/packages/nfts/script/trailblazer/trailblazers-badges/Utils.s.sol similarity index 100% rename from packages/nfts/script/trailblazers-badges/sol/Utils.s.sol rename to packages/nfts/script/trailblazer/trailblazers-badges/Utils.s.sol diff --git a/packages/nfts/script/trailblazers-season-2/Deploy.s.sol b/packages/nfts/script/trailblazers-season-2/Deploy.s.sol new file mode 100644 index 00000000000..2775e877772 --- /dev/null +++ b/packages/nfts/script/trailblazers-season-2/Deploy.s.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { UtilsScript, MockBlacklist } 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"; +import { TrailblazersBadgesS2 } from + "../../contracts/trailblazers-season-2/TrailblazersBadgesS2.sol"; +import { TrailblazersBadgesV4 } from + "../../contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol"; +import { BadgeRecruitment } from "../../contracts/trailblazers-season-2/BadgeRecruitment.sol"; + +contract DeployS2Script is Script { + UtilsScript public utils; + string public jsonLocation; + uint256 public deployerPrivateKey; + address public deployerAddress; + + BadgeRecruitment recruitment; + + // Taiko Mainnet Values + //address owner = 0xf8ff2AF0DC1D5BA4811f22aCb02936A1529fd2Be; + address claimMintSigner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address recruitmentSigner = 0x9Fc8d56c7376f9b062FEe7E02BAdFA670d603248; + string baseURI = + "https://taikonfts.4everland.link/ipfs/bafybeiatuzeeeznd3hi5qiulslxcjd22ebu45t4fra2jvi3smhocr2c66a"; + IMinimalBlacklist blacklist = IMinimalBlacklist(0xfA5EA6f9A13532cd64e805996a941F101CCaAc9a); + + uint256 public MAX_INFLUENCES = 5; + uint256 public COOLDOWN_RECRUITMENT = 24 hours; + uint256 public COOLDOWN_INFLUENCE = 30 minutes; + uint256 public INFLUENCE_WEIGHT_PERCENT = 9; + uint256 public MAX_INFLUENCES_DIVIDER = 100; + uint256 public DEFAULT_CYCLE_DURATION = 7 days; + uint256 public s1EndDate = 1_734_350_400; // Dec 16th 2024, noon UTC + uint256 public S1_LOCK_DURATION = (s1EndDate - block.timestamp); + + // Hekla Testnet Values + /* + string baseURI = + "https://taikonfts.4everland.link/ipfs/bafybeiatuzeeeznd3hi5qiulslxcjd22ebu45t4fra2jvi3smhocr2c66a"; + + IMinimalBlacklist blacklist = IMinimalBlacklist(0xe61E9034b5633977eC98E302b33e321e8140F105); + address claimMintSigner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address recruitmentSigner = 0x3cda4F2EaC3fc2FdE78B3DFFe1A1A1Eff88c68c5; + + uint256 public MAX_INFLUENCES = 5; + uint256 public COOLDOWN_RECRUITMENT = 5 minutes; + uint256 public COOLDOWN_INFLUENCE = 1 minutes; + uint256 public INFLUENCE_WEIGHT_PERCENT = 9; + uint256 public MAX_INFLUENCES_DIVIDER = 100; + uint256 public DEFAULT_CYCLE_DURATION = 7 days; + uint256 public S1_LOCK_DURATION = 365 days; + */ + + address s1Contract = 0xa20a8856e00F5ad024a55A663F06DCc419FFc4d5; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + + jsonLocation = utils.getContractJsonLocation(); + + deployerPrivateKey = utils.getPrivateKey(); + deployerAddress = utils.getAddress(); + } + + function run() public { + string memory jsonRoot = "root"; + address owner = deployerAddress; + require(owner != address(0), "Owner must be specified"); + + address impl; + address proxy; + TrailblazersBadgesV4 s1Token; + TrailblazersBadgesS2 s2Token; + + vm.startBroadcast(deployerPrivateKey); + + if (block.chainid == 167_000) { + // mainnet, use existing contract + s1Token = TrailblazersBadgesV4(s1Contract); + } else { + // hekla/localhost, deploy a s1 contract + impl = address(new TrailblazersBadges()); + blacklist = new MockBlacklist(); + proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + TrailblazersBadges.initialize, (owner, baseURI, claimMintSigner, blacklist) + ) + ) + ); + + TrailblazersBadges s1TokenV2 = TrailblazersBadges(proxy); + + // upgrade s1 contract to v4 + s1TokenV2.upgradeToAndCall( + address(new TrailblazersBadgesV4()), + abi.encodeCall(TrailblazersBadgesV4.version, ()) + ); + + s1Token = TrailblazersBadgesV4(address(s1TokenV2)); + } + + // deploy s2 contract + impl = address(new TrailblazersBadgesS2()); + proxy = address( + new ERC1967Proxy( + impl, abi.encodeCall(TrailblazersBadgesS2.initialize, (address(owner), baseURI)) + ) + ); + + s2Token = TrailblazersBadgesS2(proxy); + + // deploy the recruitment contract + + BadgeRecruitment.Config memory config = BadgeRecruitment.Config( + COOLDOWN_RECRUITMENT, + COOLDOWN_INFLUENCE, + INFLUENCE_WEIGHT_PERCENT, + MAX_INFLUENCES, + MAX_INFLUENCES_DIVIDER, + DEFAULT_CYCLE_DURATION + ); + + impl = address(new BadgeRecruitment()); + proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + BadgeRecruitment.initialize, + (address(s1Token), address(s2Token), recruitmentSigner, config) + ) + ) + ); + recruitment = BadgeRecruitment(proxy); + + // assign relations + s1Token.setRecruitmentContract(address(recruitment)); + s2Token.setMinter(address(recruitment)); + + // set the lock duration + s1Token.setRecruitmentLockDuration(S1_LOCK_DURATION); + + console.log("Token Base URI:", baseURI); + console.log("Deployed TrailblazersBadgesS2 to:", address(s2Token)); + console.log("Deployed BadgeRecruitment to:", address(recruitment)); + + // Register deployment + vm.serializeAddress(jsonRoot, "TrailblazersBadges", address(s1Token)); + vm.serializeAddress(jsonRoot, "TrailblazersBadgesS2", address(s2Token)); + vm.serializeAddress(jsonRoot, "BadgeRecruitment", address(recruitment)); + string memory finalJson = vm.serializeAddress(jsonRoot, "Owner", s2Token.owner()); + vm.writeJson(finalJson, jsonLocation); + + vm.stopBroadcast(); + } +} diff --git a/packages/nfts/script/trailblazers-season-2/UpgradeV2.s.sol b/packages/nfts/script/trailblazers-season-2/UpgradeV2.s.sol new file mode 100644 index 00000000000..14c71373b27 --- /dev/null +++ b/packages/nfts/script/trailblazers-season-2/UpgradeV2.s.sol @@ -0,0 +1,50 @@ +// 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"; + +import { TrailblazersBadgesS2 } from + "../../contracts/trailblazers-season-2/TrailblazersBadgesS2.sol"; + +contract UpgradeV2 is Script { + UtilsScript public utils; + string public jsonLocation; + uint256 public deployerPrivateKey; + address public deployerAddress; + + address tokenAddress = 0x52A7dBeC10B404548066F59DE89484e27b4181dA; + TrailblazersBadgesS2 public token; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + + jsonLocation = utils.getContractJsonLocation(); + deployerPrivateKey = utils.getPrivateKey(); + deployerAddress = utils.getAddress(); + } + + function run() public { + vm.startBroadcast(deployerPrivateKey); + token = TrailblazersBadgesS2(tokenAddress); + + token.upgradeToAndCall( + address(new TrailblazersBadgesS2()), abi.encodeCall(TrailblazersBadgesS2.version, ()) + ); + + token = TrailblazersBadgesS2(address(token)); + + console.log("Upgraded TrailblazersBadgesV3 to:", address(token)); + + // update uri + token.setUri( + "https://taikonfts.4everland.link/ipfs/bafybeief7o4u6f676e6uz4yt4cv34ai4mesd7motoq6y4xxaoyjfbna5de" + ); + console.log("Updated token URI"); + } +} diff --git a/packages/nfts/script/trailblazer/trailblazers-badges/sol/Utils.s.sol b/packages/nfts/script/trailblazers-season-2/Utils.s.sol similarity index 88% rename from packages/nfts/script/trailblazer/trailblazers-badges/sol/Utils.s.sol rename to packages/nfts/script/trailblazers-season-2/Utils.s.sol index 3af584af2bb..f52d15f54c8 100644 --- a/packages/nfts/script/trailblazer/trailblazers-badges/sol/Utils.s.sol +++ b/packages/nfts/script/trailblazers-season-2/Utils.s.sol @@ -4,7 +4,6 @@ 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; @@ -59,7 +58,7 @@ contract UtilsScript is Script { function getContractJsonLocation() public view returns (string memory) { string memory root = vm.projectRoot(); return - string.concat(root, "/deployments/trailblazers-badges/", lowercaseNetworkKey, ".json"); + string.concat(root, "/deployments/trailblazers-season-2/", lowercaseNetworkKey, ".json"); } function getBlacklist() public view returns (IMinimalBlacklist blacklistAddress) { @@ -76,3 +75,12 @@ contract UtilsScript is Script { function run() public { } } + +contract MockBlacklist is IMinimalBlacklist { + function isBlacklisted(address _account) external pure returns (bool) { + if (_account == address(0)) { + return true; + } + return false; + } +} diff --git a/packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol b/packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol new file mode 100644 index 00000000000..45ab1952fcc --- /dev/null +++ b/packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol @@ -0,0 +1,493 @@ +// 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"; +import { TrailblazersBadgesS2 } from + "../../contracts/trailblazers-season-2/TrailblazersBadgesS2.sol"; +import { TrailblazerBadgesS1MintTo } from "../util/TrailblazerBadgesS1MintTo.sol"; +import { TrailblazersBadgesV4 } from + "../../contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol"; +import { BadgeRecruitment } from "../../contracts/trailblazers-season-2/BadgeRecruitment.sol"; + +contract TrailblazersBadgesS2Test is Test { + UtilsScript public utils; + + TrailblazersBadgesV4 public s1BadgesV4; + TrailblazersBadgesS2 public s2Badges; + + address public owner = vm.addr(0x5); + + address[3] public minters = [vm.addr(0x1), vm.addr(0x2), vm.addr(0x3)]; + + uint256 public BADGE_ID; + + MockBlacklist public blacklist; + + address mintSigner; + uint256 mintSignerPk; + + uint256 public MAX_INFLUENCES = 3; + uint256 public COOLDOWN_RECRUITMENT = 1 hours; + uint256 public COOLDOWN_INFLUENCE = 5 minutes; + uint256 public INFLUENCE_WEIGHT_PERCENT = 5; + uint256 public MAX_INFLUENCES_DIVIDER = 100; + uint256 public DEFAULT_CYCLE_DURATION = 7 days; + + BadgeRecruitment public recruitment; + + 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) + ) + ) + ); + + TrailblazersBadges s1BadgesV2 = TrailblazersBadges(proxy); + + // upgrade s1 badges contract to use the mock version + + s1BadgesV2.upgradeToAndCall( + address(new TrailblazerBadgesS1MintTo()), + abi.encodeCall(TrailblazerBadgesS1MintTo.call, ()) + ); + + BADGE_ID = s1BadgesV2.BADGE_RAVERS(); + + // upgrade s1 contract to v4 + s1BadgesV2.upgradeToAndCall( + address(new TrailblazersBadgesV4()), abi.encodeCall(TrailblazersBadgesV4.version, ()) + ); + + s1BadgesV4 = TrailblazersBadgesV4(address(s1BadgesV2)); + + // set cooldown recruitment + s1BadgesV4.setRecruitmentLockDuration(365 days); + + // deploy the s2 erc1155 token contract + + impl = address(new TrailblazersBadgesS2()); + proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall(TrailblazersBadgesS2.initialize, (address(recruitment), "ipfs://")) + ) + ); + s2Badges = TrailblazersBadgesS2(proxy); + + // deploy the recruitment contract + + BadgeRecruitment.Config memory config = BadgeRecruitment.Config( + COOLDOWN_RECRUITMENT, + COOLDOWN_INFLUENCE, + INFLUENCE_WEIGHT_PERCENT, + MAX_INFLUENCES, + MAX_INFLUENCES_DIVIDER, + DEFAULT_CYCLE_DURATION + ); + + impl = address(new BadgeRecruitment()); + proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + BadgeRecruitment.initialize, + (address(s1BadgesV2), address(s2Badges), mintSigner, config) + ) + ) + ); + recruitment = BadgeRecruitment(proxy); + s1BadgesV4.setRecruitmentContract(address(recruitment)); + s2Badges.setMinter(address(recruitment)); + // enable recruitment for BADGE_ID + uint256[] memory enabledBadgeIds = new uint256[](1); + enabledBadgeIds[0] = BADGE_ID; + recruitment.enableRecruitments(enabledBadgeIds); + + vm.stopBroadcast(); + } + + function mint_s1(address minter, uint256 badgeId) public { + bytes32 _hash = s1BadgesV4.getHash(minter, badgeId); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + bool canMint = s1BadgesV4.canMint(abi.encodePacked(r, s, v), minter, badgeId); + assertTrue(canMint); + + vm.startPrank(minter); + s1BadgesV4.mint(abi.encodePacked(r, s, v), badgeId); + vm.stopPrank(); + } + + function test_mint_s1() public { + mint_s1(minters[0], s1BadgesV4.BADGE_RAVERS()); + mint_s1(minters[0], s1BadgesV4.BADGE_ROBOTS()); + assertEq(s1BadgesV4.balanceOf(minters[0]), 2); + + mint_s1(minters[1], s1BadgesV4.BADGE_BOUNCERS()); + mint_s1(minters[1], s1BadgesV4.BADGE_MASTERS()); + assertEq(s1BadgesV4.balanceOf(minters[1]), 2); + + mint_s1(minters[2], s1BadgesV4.BADGE_MONKS()); + mint_s1(minters[2], s1BadgesV4.BADGE_DRUMMERS()); + assertEq(s1BadgesV4.balanceOf(minters[2]), 2); + } + + function test_startRecruitment() public { + mint_s1(minters[0], BADGE_ID); + + vm.prank(minters[0]); + wait(100); + s1BadgesV4.startRecruitment(BADGE_ID); + + uint256 tokenId = s1BadgesV4.getTokenId(minters[0], BADGE_ID); + assertEq(s1BadgesV4.balanceOf(minters[0]), 1); + assertEq(recruitment.isRecruitmentActive(minters[0]), true); + assertEq(s1BadgesV4.unlockTimestamps(tokenId), block.timestamp + 365 days); + } + + function wait(uint256 time) public { + vm.warp(block.timestamp + time); + } + + // happy-path, make 3 pink influences, and 2 purple ones + function test_influenceRecruitment() public { + test_startRecruitment(); + + vm.startPrank(minters[0]); + + uint256 points = 0; + bytes32 _hash = + recruitment.generateClaimHash(BadgeRecruitment.HashType.Influence, minters[0], points); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + wait(COOLDOWN_INFLUENCE); + + recruitment.influenceRecruitment( + _hash, v, r, s, points, BadgeRecruitment.InfluenceColor.Minnow + ); + wait(COOLDOWN_INFLUENCE); + + recruitment.influenceRecruitment( + _hash, v, r, s, points, BadgeRecruitment.InfluenceColor.Minnow + ); + + for (uint256 i = 0; i < MAX_INFLUENCES; i++) { + wait(COOLDOWN_INFLUENCE); + recruitment.influenceRecruitment( + _hash, v, r, s, points, BadgeRecruitment.InfluenceColor.Whale + ); + } + + vm.stopPrank(); + + assertEq(recruitment.isInfluenceActive(minters[0]), true); + assertEq(recruitment.isRecruitmentActive(minters[0]), true); + + (uint256 whaleInfluences, uint256 minnowInfluences) = + recruitment.getRecruitmentInfluences(minters[0]); + + assertEq(whaleInfluences, MAX_INFLUENCES); + assertEq(minnowInfluences, 0); + } + + function test_revert_tooManyInfluences() public { + uint256 points = 0; + bytes32 _hash = + recruitment.generateClaimHash(BadgeRecruitment.HashType.Influence, minters[0], points); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + test_influenceRecruitment(); + vm.startPrank(minters[0]); + vm.expectRevert(); + recruitment.influenceRecruitment( + _hash, v, r, s, points, BadgeRecruitment.InfluenceColor.Whale + ); + + vm.stopPrank(); + } + + function test_endRecruitment() public { + test_influenceRecruitment(); + + wait(COOLDOWN_INFLUENCE); + wait(COOLDOWN_RECRUITMENT); + + // generate the claim hash for the current recruitment + bytes32 claimHash = recruitment.generateClaimHash( + BadgeRecruitment.HashType.End, + minters[0], + 0 // experience points + ); + + // simulate the backend signing the hash + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, claimHash); + + // exercise the randomFromSignature function + + vm.prank(minters[0]); + recruitment.endRecruitment(claimHash, v, r, s, 0); + + // check for s2 state reset + assertEq(recruitment.isRecruitmentActive(minters[0]), false); + assertEq(recruitment.isInfluenceActive(minters[0]), false); + + // check for s2 mint + assertEq(s2Badges.balanceOf(minters[0], 1), 1); + } + + function test_revert_startRecruitmentTwice() public { + test_startRecruitment(); + vm.startPrank(minters[0]); + vm.expectRevert(); + s1BadgesV4.startRecruitment(BADGE_ID); + vm.stopPrank(); + } + + function test_revert_migrateDisabled() public { + uint256 badgeId = s1BadgesV4.BADGE_ROBOTS(); + mint_s1(minters[0], badgeId); + + uint256 tokenId = s1BadgesV4.tokenOfOwnerByIndex(minters[0], 0); + + vm.startPrank(minters[0]); + vm.expectRevert(); + s1BadgesV4.startRecruitment(badgeId); + vm.stopPrank(); + // ensure no values got changed/updated + assertEq(s1BadgesV4.balanceOf(minters[0]), 1); + assertEq(s1BadgesV4.balanceOf(address(s2Badges)), 0); + assertEq(s1BadgesV4.ownerOf(tokenId), minters[0]); + assertEq(recruitment.isRecruitmentActive(minters[0]), false); + } + + function test_randomFromSignature() public view { + bytes32 signatureHash = keccak256( + abi.encodePacked( + keccak256("1234567890"), // should use the block's hash + minters[0] + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, signatureHash); + + uint256 random = recruitment.randomFromSignature(signatureHash, v, r, s); + + assertEq( + random, + 28_417_844_340_632_250_945_870_465_294_567_768_196_388_504_060_802_704_441_612_911_129_119_444_309_664 + ); + } + + function test_setConfig() public { + BadgeRecruitment.Config memory config = + BadgeRecruitment.Config(1 hours, 5 minutes, 5, 3, 100, DEFAULT_CYCLE_DURATION); + vm.prank(owner); + recruitment.setConfig(config); + + BadgeRecruitment.Config memory newConfig = recruitment.getConfig(); + + assertEq(newConfig.cooldownRecruitment, 1 hours); + assertEq(newConfig.cooldownInfluence, 5 minutes); + assertEq(newConfig.influenceWeightPercent, 5); + assertEq(newConfig.baseMaxInfluences, 3); + } + + function test_setConfig_revert__notOwner() public { + BadgeRecruitment.Config memory config = + BadgeRecruitment.Config(1 hours, 5 minutes, 5, 3, 100, DEFAULT_CYCLE_DURATION); + + vm.startPrank(minters[0]); + vm.expectRevert(); + recruitment.setConfig(config); + vm.stopPrank(); + } + + function test_rollCycle() public { + BadgeRecruitment.RecruitmentCycle memory cycle = + recruitment.getRecruitmentCycle(recruitment.recruitmentCycleId()); + assertEq(cycle.cycleId, 1); + assertEq(cycle.startTime, block.timestamp); + assertEq(cycle.endTime, block.timestamp + DEFAULT_CYCLE_DURATION); + assertEq(cycle.s1BadgeIds.length, 1); + assertEq(cycle.s1BadgeIds[0], BADGE_ID); + test_endRecruitment(); + + // close the current cycle + vm.startPrank(owner); + //recruitment.endRecruitment(); + wait(DEFAULT_CYCLE_DURATION + 1); + // launch the next cycle + uint256[] memory enabledBadgeIds = new uint256[](1); + enabledBadgeIds[0] = 2; + recruitment.enableRecruitments(enabledBadgeIds); + + // check cycle roll forward + cycle = recruitment.getRecruitmentCycle(recruitment.recruitmentCycleId()); + assertEq(cycle.cycleId, 2); + assertEq(cycle.startTime, block.timestamp); + assertEq(cycle.endTime, block.timestamp + DEFAULT_CYCLE_DURATION); + assertEq(cycle.s1BadgeIds.length, 1); + assertEq(cycle.s1BadgeIds[0], 2); + } + + function test_revertTransferAfterRecruitmentStarts() public { + test_startRecruitment(); + assertEq(s1BadgesV4.balanceOf(minters[0]), 1); + uint256 tokenId = s1BadgesV4.getTokenId(minters[0], BADGE_ID); + vm.prank(minters[0]); + vm.expectRevert(); + s1BadgesV4.transferFrom(minters[0], minters[1], tokenId); + } + + function test_startRecruitment_expBased() public { + uint256 points = 100; + bytes32 _hash = + recruitment.generateClaimHash(BadgeRecruitment.HashType.Start, minters[0], points); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + vm.prank(minters[0]); + recruitment.startRecruitment(_hash, v, r, s, points); + + assertEq(recruitment.isRecruitmentActive(minters[0]), true); + } + + function test_startRecruitment_expBased_revert_hashMismatch() public { + mint_s1(minters[0], BADGE_ID); + + uint256 points = 100; + bytes32 _hash = + recruitment.generateClaimHash(BadgeRecruitment.HashType.Start, minters[0], points); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + + vm.prank(minters[0]); + vm.expectRevert(BadgeRecruitment.HASH_MISMATCH.selector); + recruitment.startRecruitment(_hash, v, s, r, points + 1); + } + + function test_startRecruitment_expBased_revert_notRandomSigner() public { + mint_s1(minters[0], BADGE_ID); + + uint256 points = 100; + bytes32 _hash = + recruitment.generateClaimHash(BadgeRecruitment.HashType.Start, minters[0], points); + (, uint256 badSignerPk) = makeAddrAndKey("badSigner"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(badSignerPk, _hash); + + vm.prank(minters[0]); + vm.expectRevert(BadgeRecruitment.NOT_RANDOM_SIGNER.selector); + recruitment.startRecruitment(_hash, v, r, s, points); + } + + function test_startRecruitment_multiCycle() public { + // start a recruitment for minters[0] and badgeId + test_endRecruitment(); + + // mint the same badge on minters[1], and transfer to minters[0] + mint_s1(minters[1], BADGE_ID); + uint256 tokenId = s1BadgesV4.getTokenId(minters[1], BADGE_ID); + vm.prank(minters[1]); + s1BadgesV4.transferFrom(minters[1], minters[0], tokenId); + assertEq(s1BadgesV4.balanceOf(minters[0]), 2); + + // ensure they can migrate via exp + test_startRecruitment_expBased(); + wait(COOLDOWN_INFLUENCE); + wait(COOLDOWN_RECRUITMENT); + + // generate the claim hash for the current recruitment + bytes32 claimHash = recruitment.generateClaimHash( + BadgeRecruitment.HashType.End, + minters[0], + 0 // experience points + ); + + // simulate the backend signing the hash + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, claimHash); + + // exercise the randomFromSignature function + vm.prank(minters[0]); + recruitment.endRecruitment(claimHash, v, r, s, 0); + assertEq(s2Badges.balanceOf(minters[0], 1), 1); + + // + + // ensure minters[0] cannot recruit again within this same cycle + vm.startPrank(minters[0]); + vm.expectRevert(BadgeRecruitment.ALREADY_MIGRATED_IN_CYCLE.selector); + s1BadgesV4.startRecruitment(BADGE_ID); + // ensure they can't either via exp call + uint256 points = 100; + bytes32 _hash = + recruitment.generateClaimHash(BadgeRecruitment.HashType.Start, minters[0], points); + + (v, r, s) = vm.sign(mintSignerPk, _hash); + + vm.expectRevert(BadgeRecruitment.ALREADY_MIGRATED_IN_CYCLE.selector); + recruitment.startRecruitment(_hash, v, r, s, points); + + // move the cycle forward + wait(DEFAULT_CYCLE_DURATION + 1); + + // no cycle is set, should fail to recruit + vm.startPrank(minters[0]); + vm.expectRevert(BadgeRecruitment.RECRUITMENT_NOT_ENABLED.selector); + s1BadgesV4.startRecruitment(BADGE_ID); + + // enable the next cycle + uint256[] memory enabledBadgeIds = new uint256[](1); + enabledBadgeIds[0] = BADGE_ID; + vm.startPrank(owner); + recruitment.enableRecruitments(enabledBadgeIds); + + // ensure they can recruit with the second badge + vm.startPrank(minters[0]); + s1BadgesV4.startRecruitment(BADGE_ID); + + tokenId = s1BadgesV4.getTokenId(minters[0], BADGE_ID); + assertEq(s1BadgesV4.balanceOf(minters[0]), 2); + assertEq(recruitment.isRecruitmentActive(minters[0]), true); + assertEq(s1BadgesV4.unlockTimestamps(tokenId), block.timestamp + 365 days); + + // ensure they cannot start it again + vm.expectRevert(BadgeRecruitment.RECRUITMENT_ALREADY_STARTED.selector); + s1BadgesV4.startRecruitment(BADGE_ID); + } + + function test_enableDisableRecruitment_admin() public { + // should fail to enable new recruitments + uint256[] memory enabledBadgeIds = new uint256[](1); + enabledBadgeIds[0] = BADGE_ID; + vm.startPrank(owner); + vm.expectRevert(BadgeRecruitment.CURRENT_CYCLE_NOT_OVER.selector); + recruitment.enableRecruitments(enabledBadgeIds); + // wait out the currently-open cycle + wait(DEFAULT_CYCLE_DURATION + 1); + // create a second cycle + recruitment.enableRecruitments(enabledBadgeIds); + } +} diff --git a/packages/nfts/test/trailblazers-season-2/TrailblazersBadgesS2.t.sol b/packages/nfts/test/trailblazers-season-2/TrailblazersBadgesS2.t.sol new file mode 100644 index 00000000000..40918273115 --- /dev/null +++ b/packages/nfts/test/trailblazers-season-2/TrailblazersBadgesS2.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { Test } from "forge-std/src/Test.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { UtilsScript } from "../../script/taikoon/sol/Utils.s.sol"; +import { TrailblazersBadgesS2 } from + "../../contracts/trailblazers-season-2/TrailblazersBadgesS2.sol"; + +contract TrailblazersBadgesS2Test is Test { + UtilsScript public utils; + + address public owner = vm.addr(0x5); + address public authorizedMinter = vm.addr(0x6); + address[3] public minters = [vm.addr(0x1), vm.addr(0x2), vm.addr(0x3)]; + + string public uriTemplate = "ipfs://hash"; + + TrailblazersBadgesS2 public nft; + + uint256 public TOKEN_ID = 1; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + // create whitelist merkle tree + vm.startBroadcast(owner); + + address impl = address(new TrailblazersBadgesS2()); + address proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall(TrailblazersBadgesS2.initialize, (authorizedMinter, uriTemplate)) + ) + ); + + nft = TrailblazersBadgesS2(proxy); + + vm.stopBroadcast(); + } + + function test_mint() public { + vm.prank(authorizedMinter); + nft.mint( + minters[0], + TrailblazersBadgesS2.BadgeType.Ravers, + TrailblazersBadgesS2.MovementType.Minnow + ); + + assertEq(nft.balanceOf(minters[0], TOKEN_ID), 1); + + TrailblazersBadgesS2.Badge memory badge = nft.getBadge(TOKEN_ID); + assertEq(badge.tokenId, TOKEN_ID); + assertEq(uint8(badge.badgeType), uint8(TrailblazersBadgesS2.BadgeType.Ravers)); + assertEq(uint8(badge.movementType), uint8(TrailblazersBadgesS2.MovementType.Minnow)); + } + + function test_uri_byTokenId() public { + test_mint(); + assertEq(nft.uri(TOKEN_ID), "ipfs://hash/2/0"); + } + + function test_uri_byTypeAndMovement() public { + test_mint(); + assertEq( + nft.uri(TrailblazersBadgesS2.BadgeType.Ravers, TrailblazersBadgesS2.MovementType.Minnow), + "ipfs://hash/2/0" + ); + } + + function test_uri_full() public { + vm.startPrank(authorizedMinter); + uint8 tokenId = 1; + + TrailblazersBadgesS2.Badge memory badge; + for (uint8 i = 1; i < 3; i++) { + for (uint8 j = 0; j < 8; j++) { + nft.mint( + minters[0], + TrailblazersBadgesS2.BadgeType(j), + TrailblazersBadgesS2.MovementType(i) + ); + + badge = nft.getBadge(tokenId); + string memory badgeType = vm.toString(uint256(badge.badgeType)); + string memory movementType = vm.toString(uint256(badge.movementType)); + + string memory uri = + string(abi.encodePacked("ipfs://hash/", movementType, "/", badgeType)); + assertEq(nft.uri(tokenId), uri); + tokenId++; + } + } + } + + function test_uri_revert__tokenNotMinted() public { + vm.expectRevert(); + nft.uri(TOKEN_ID); + } + + function test_mint_revert__notAuthorizedMinter() public { + vm.prank(minters[1]); + vm.expectRevert(); + nft.mint( + minters[1], + TrailblazersBadgesS2.BadgeType.Ravers, + TrailblazersBadgesS2.MovementType.Minnow + ); + } +} diff --git a/packages/nfts/test/util/TrailblazerBadgesS1MintTo.sol b/packages/nfts/test/util/TrailblazerBadgesS1MintTo.sol new file mode 100644 index 00000000000..27f3697fd7c --- /dev/null +++ b/packages/nfts/test/util/TrailblazerBadgesS1MintTo.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { Test } from "forge-std/src/Test.sol"; + +import { TrailblazersBadgesV4 } from + "../../contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol"; + +contract TrailblazerBadgesS1MintTo is TrailblazersBadgesV4 { + function mintTo(address _minter, uint256 _badgeId) public onlyOwner { + if (_badgeId > BADGE_SHINTO) revert INVALID_BADGE_ID(); + + uint256 tokenId = totalSupply() + 1; + badges[tokenId] = _badgeId; + + _mint(_minter, tokenId); + + emit BadgeCreated(tokenId, _minter, _badgeId); + } + + function call() public view returns (bool) { + return true; + } +}