diff --git a/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol b/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol index 4b2e1f57a9..44e93329ad 100644 --- a/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol +++ b/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitment.sol @@ -37,9 +37,6 @@ contract BadgeRecruitment is 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; diff --git a/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitmentV2.sol b/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitmentV2.sol new file mode 100644 index 0000000000..cb14278a37 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-season-2/BadgeRecruitmentV2.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./BadgeRecruitment.sol"; + +contract BadgeRecruitmentV2 is BadgeRecruitment { + /// @notice Events + event RecruitmentReset( + uint256 indexed cycleId, address indexed user, uint256 indexed s1TokenId, uint256 s1BadgeId + ); + + /// @notice Errors + error RECRUITMENT_ALREADY_COMPLETED(); + error RECRUITMENT_NOT_FOUND(); + error NOT_ENOUGH_TIME_LEFT(); + + modifier recruitmentHasTimeLeft(address _user) { + uint256 endCycleTime = recruitmentCycles[recruitmentCycleId].endTime; + uint256 potentialRecruitmentEndTime = block.timestamp + this.getConfig().cooldownRecruitment; + + if (potentialRecruitmentEndTime > endCycleTime) { + revert NOT_ENOUGH_TIME_LEFT(); + } + _; + } + + /// @notice Updated version function + function version() external pure virtual returns (string memory) { + return "V2"; + } + + /// @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, + uint256 _s1TokenId + ) + external + virtual + onlyRole(S1_BADGES_ROLE) + recruitmentOpen(_s1BadgeId) + isNotMigrating(_user) + hasntMigratedInCycle(_s1BadgeId, _user, RecruitmentType.Migration) + recruitmentHasTimeLeft(_user) + { + if (s1Badges.ownerOf(_s1TokenId) != _user) { + revert TOKEN_NOT_OWNED(); + } + _startRecruitment(_user, _s1BadgeId, _s1TokenId, RecruitmentType.Migration); + } + + /// @notice Disable all current recruitments + /// @dev Bypasses the default date checks + function forceDisableAllRecruitments() external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + forceDisableRecruitments(); + + emit RecruitmentCycleToggled( + recruitmentCycleId, + recruitmentCycles[recruitmentCycleId].startTime, + recruitmentCycles[recruitmentCycleId].endTime, + recruitmentCycles[recruitmentCycleId].s1BadgeIds, + false + ); + } + + /// @notice Get the active recruitment for a user + /// @param _user The user address + /// @return The active recruitment + function getActiveRecruitmentsFor(address _user) public view returns (Recruitment[] memory) { + if (recruitments[_user].length == 0) { + revert RECRUITMENT_NOT_STARTED(); + } + return recruitments[_user]; + } + + /// @notice Reset a recruitment that hasn't been completed + /// @param _user The user address + /// @param _s1TokenId The s1 token ID + /// @param _s1BadgeId The s1 badge ID + /// @param _recruitmentCycle The recruitment index + /// @dev Must be called from the s1 badges contract + function resetRecruitment( + address _user, + uint256 _s1TokenId, + uint256 _s1BadgeId, + uint256 _recruitmentCycle + ) + public + virtual + onlyRole(S1_BADGES_ROLE) + { + if ( + !recruitmentCycleUniqueMints[_recruitmentCycle][_user][_s1BadgeId][RecruitmentType + .Migration] + && !recruitmentCycleUniqueMints[_recruitmentCycle][_user][_s1BadgeId][RecruitmentType.Claim] + && !recruitmentCycleUniqueMints[_recruitmentCycle][_user][_s1BadgeId][RecruitmentType + .Undefined] + ) { + revert RECRUITMENT_NOT_FOUND(); + } + + bool found = false; + + for (uint256 i = 0; i < recruitments[_user].length; i++) { + if ( + recruitments[_user][i].recruitmentCycle == _recruitmentCycle + && recruitments[_user][i].s1TokenId == _s1TokenId + && recruitments[_user][i].s2TokenId == 0 + ) { + delete recruitments[_user][i]; + found = true; + break; + } + } + + if (!found) { + revert RECRUITMENT_NOT_FOUND(); + } + + recruitmentCycleUniqueMints[_recruitmentCycle][_user][_s1BadgeId][RecruitmentType.Undefined] + = false; + recruitmentCycleUniqueMints[_recruitmentCycle][_user][_s1BadgeId][RecruitmentType.Claim] = + false; + recruitmentCycleUniqueMints[_recruitmentCycle][_user][_s1BadgeId][RecruitmentType.Migration] + = false; + + emit RecruitmentReset(_recruitmentCycle, _user, _s1TokenId, _s1BadgeId); + } + + /// @notice Set the s2 badges contract + /// @param _s2Badges The s2 badges contract address + /// @dev Must be called from the admin account + function setS2BadgesContract(address _s2Badges) external virtual onlyRole(DEFAULT_ADMIN_ROLE) { + s2Badges = TrailblazersBadgesS2(_s2Badges); + } +} diff --git a/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol b/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol index c272a55266..f3313f0335 100644 --- a/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol +++ b/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol @@ -59,7 +59,7 @@ contract TrailblazersBadgesV4 is TrailblazersBadgesV3 { /// @notice Start recruitment for a badge /// @param _badgeId Badge id - function startRecruitment(uint256 _badgeId) public { + function startRecruitment(uint256 _badgeId) public virtual { if (recruitmentLockDuration == 0) { revert RECRUITMENT_LOCK_DURATION_NOT_SET(); } diff --git a/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV5.sol b/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV5.sol new file mode 100644 index 0000000000..9dbf894206 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-season-2/TrailblazersS1BadgesV5.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./TrailblazersS1BadgesV4.sol"; +import "./BadgeRecruitment.sol"; +import "./BadgeRecruitmentV2.sol"; + +contract TrailblazersBadgesV5 is TrailblazersBadgesV4 { + /// @notice Errors + error RECRUITMENT_ALREADY_COMPLETED(); + error NOT_OWNER(); + error NOT_IMPLEMENTED(); + error RECRUITMENT_NOT_FOUND(); + /// @notice Updated version function + /// @return Version string + + function version() external pure virtual override returns (string memory) { + return "V5"; + } + /// @notice Recruitment contract + + BadgeRecruitmentV2 public recruitmentContractV2; + /// @notice Setter for recruitment contract + + function setRecruitmentContractV2(address _recruitmentContractV2) public onlyOwner { + recruitmentContractV2 = BadgeRecruitmentV2(_recruitmentContractV2); + } + + /// @notice Start recruitment for a badge + /// @param _badgeId Badge ID + /// @param _tokenId Token ID + function startRecruitment(uint256 _badgeId, uint256 _tokenId) public { + if (recruitmentLockDuration == 0) { + revert RECRUITMENT_LOCK_DURATION_NOT_SET(); + } + if (ownerOf(_tokenId) != _msgSender()) { + revert NOT_OWNER(); + } + + if (unlockTimestamps[_tokenId] > block.timestamp) { + revert BADGE_LOCKED(); + } + + unlockTimestamps[_tokenId] = block.timestamp + recruitmentLockDuration; + recruitmentContractV2.startRecruitment(_msgSender(), _badgeId, _tokenId); + } + + /// @notice Deprecated of legacy function + function startRecruitment(uint256 /*_badgeId*/ ) public virtual override { + revert NOT_IMPLEMENTED(); + } + + /// @notice Reset an ongoing migration + /// @param _tokenId Token ID + /// @param _badgeId Badge ID + /// @param _cycleId Cycle ID + /// @dev Only the owner of the token can reset the migration + function resetMigration(uint256 _tokenId, uint256 _badgeId, uint256 _cycleId) public virtual { + if (ownerOf(_tokenId) != _msgSender()) { + revert NOT_OWNER(); + } + + recruitmentContractV2.resetRecruitment(_msgSender(), _tokenId, _badgeId, _cycleId); + unlockTimestamps[_tokenId] = 0; + } +} diff --git a/packages/nfts/deployments/trailblazers-season-2/hekla.json b/packages/nfts/deployments/trailblazers-season-2/hekla.json index 0b70fbc0bf..23b05d5ecb 100644 --- a/packages/nfts/deployments/trailblazers-season-2/hekla.json +++ b/packages/nfts/deployments/trailblazers-season-2/hekla.json @@ -1,6 +1,6 @@ { - "BadgeRecruitment": "0xBd368C65Cb354eBAd6c1429b551bD0197f19C2B8", + "BadgeRecruitment": "0xcb00B57e8F5fCFffE87bb65f3047b6e4e5A73cA9", "Owner": "0x4100a9B680B1Be1F10Cb8b5a57fE59eA77A8184e", - "TrailblazersBadges": "0x9E14C357E964BeE012bA82Ce9d6513dAec6ea961", - "TrailblazersBadgesS2": "0xc84B76a5836Cb0CeF094808af445F7E98504ED5B" + "TrailblazersBadges": "0x3a7d7c963EF905FCdb6CefAA21b52497fae3EFC4", + "TrailblazersBadgesS2": "0xDE43b7b9A485d76bc8D48a69DdE6b89540b27DdD" } diff --git a/packages/nfts/package.json b/packages/nfts/package.json index 41926afa56..9203661d0a 100644 --- a/packages/nfts/package.json +++ b/packages/nfts/package.json @@ -37,11 +37,13 @@ "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:airdrop:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-airdrop/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --verify --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: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" + "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", + "tbz-s2:upgradeRecruitmentV2:hekla": "forge clean && pnpm compile && forge script script/trailblazers-season-2/RecruitmentUpgradeV2.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "tbz-s2:upgradeRecruitmentV2:mainnet": "forge clean && pnpm compile && forge script script/trailblazers-season-2/RecruitmentUpgradeV2.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/trailblazers-season-2/RecruitmentUpgradeV2.s.sol b/packages/nfts/script/trailblazers-season-2/RecruitmentUpgradeV2.s.sol new file mode 100644 index 0000000000..50af179fe7 --- /dev/null +++ b/packages/nfts/script/trailblazers-season-2/RecruitmentUpgradeV2.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 "../../contracts/trailblazers-season-2/TrailblazersBadgesS2.sol"; + +import "../../contracts/trailblazers-season-2/TrailblazersS1BadgesV5.sol"; + +contract UpgradeV2 is Script { + // setup + UtilsScript public utils; + string public jsonLocation; + uint256 public deployerPrivateKey; + address public deployerAddress; + + // deployment vars + TrailblazersBadgesV5 public tokenV5; + BadgeRecruitmentV2 public badgeRecruitmentV2; + + // mainnet config + address public s1TokenAddress = 0xa20a8856e00F5ad024a55A663F06DCc419FFc4d5; + address public badgeRecruitmentAddress = 0xa9Ceb04F3aF71fF123409d426A92BABb5124970C; +/* + // hekla config + 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 = 10 minutes; + uint256 public COOLDOWN_INFLUENCE = 5 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); +*/ + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + + jsonLocation = utils.getContractJsonLocation(); + deployerPrivateKey = utils.getPrivateKey(); + deployerAddress = utils.getAddress(); + } + + function run() public { + vm.startBroadcast(deployerPrivateKey); + + // address impl; + // address proxy; + TrailblazersBadgesV4 s1Token; + // TrailblazersBadgesS2 s2Token; + BadgeRecruitment badgeRecruitment; + + if (block.chainid == 167_000) { + // mainnet, use existing contract + s1Token = TrailblazersBadgesV4(s1TokenAddress); + badgeRecruitment = BadgeRecruitment(badgeRecruitmentAddress); + } else { + /* + // non-mainnet, deploy contract chain + impl = address(new TrailblazersBadges()); + blacklist = new MockBlacklist(); + proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + TrailblazersBadges.initialize, + (deployerAddress, 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)); + + s1Token.setRecruitmentLockDuration(S1_LOCK_DURATION); + + // deploy s2 badges + impl = address(new TrailblazersBadgesS2()); + proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall(TrailblazersBadgesS2.initialize, (deployerAddress, baseURI)) + ) + ); + + s2Token = TrailblazersBadgesS2(proxy); + + // deploy 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) + ) + ) + ); + + badgeRecruitment = BadgeRecruitment(proxy); + + // overwrite json deployment data + string memory jsonRoot = "root"; + vm.serializeAddress(jsonRoot, "TrailblazersBadges", address(s1Token)); + vm.serializeAddress(jsonRoot, "TrailblazersBadgesS2", address(s2Token)); + vm.serializeAddress(jsonRoot, "BadgeRecruitment", address(badgeRecruitment)); + string memory finalJson = vm.serializeAddress(jsonRoot, "Owner", s2Token.owner()); + vm.writeJson(finalJson, jsonLocation); + + // further setup + s1Token.setRecruitmentContract(address(badgeRecruitment)); + s2Token.setMinter(address(badgeRecruitment)); + */ + } + + // upgrade token contract + s1Token.upgradeToAndCall( + address(new TrailblazersBadgesV5()), abi.encodeCall(TrailblazersBadgesV5.version, ()) + ); + + tokenV5 = TrailblazersBadgesV5(address(s1Token)); + console.log("Upgraded TrailblazersBadgesV4 to:", address(tokenV5)); + + // upgrade recruitment contract + badgeRecruitment.upgradeToAndCall( + address(new BadgeRecruitmentV2()), abi.encodeCall(BadgeRecruitmentV2.version, ()) + ); + + badgeRecruitmentV2 = BadgeRecruitmentV2(address(badgeRecruitment)); + console.log("Upgraded BadgeRecruitment to:", address(badgeRecruitmentV2)); + + // set upgraded recruitment contract + tokenV5.setRecruitmentContractV2(address(badgeRecruitmentV2)); + console.log("Set recruitment contract to:", address(badgeRecruitmentV2)); + } +} diff --git a/packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol b/packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol index 45ab1952fc..0d1fac24ef 100644 --- a/packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol +++ b/packages/nfts/test/trailblazers-season-2/BadgeRecruitment.t.sol @@ -16,7 +16,7 @@ import { TrailblazersBadgesV4 } from "../../contracts/trailblazers-season-2/TrailblazersS1BadgesV4.sol"; import { BadgeRecruitment } from "../../contracts/trailblazers-season-2/BadgeRecruitment.sol"; -contract TrailblazersBadgesS2Test is Test { +contract BadgeRecruitmentTest is Test { UtilsScript public utils; TrailblazersBadgesV4 public s1BadgesV4; diff --git a/packages/nfts/test/trailblazers-season-2/BadgeRecruitmentV2.t.sol b/packages/nfts/test/trailblazers-season-2/BadgeRecruitmentV2.t.sol new file mode 100644 index 0000000000..9113a5fa12 --- /dev/null +++ b/packages/nfts/test/trailblazers-season-2/BadgeRecruitmentV2.t.sol @@ -0,0 +1,349 @@ +// 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"; +import { BadgeRecruitmentV2 } from "../../contracts/trailblazers-season-2/BadgeRecruitmentV2.sol"; +import "../../contracts/trailblazers-season-2/TrailblazersS1BadgesV5.sol"; + +contract BadgeRecruitmentV2Test is Test { + UtilsScript public utils; + + TrailblazersBadgesV5 public s1BadgesV5; + 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 recruitmentV1; + BadgeRecruitmentV2 public recruitment; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + blacklist = new MockBlacklist(); + // create whitelist merkle tree + vm.startPrank(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)); + + // upgrade to v5 + s1BadgesV4.upgradeToAndCall( + address(new TrailblazersBadgesV5()), abi.encodeCall(TrailblazersBadgesV5.version, ()) + ); + + s1BadgesV5 = TrailblazersBadgesV5(address(s1BadgesV4)); + + // set cooldown recruitment + s1BadgesV5.setRecruitmentLockDuration(365 days); + + // deploy the s2 erc1155 token contract + + impl = address(new TrailblazersBadgesS2()); + proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall(TrailblazersBadgesS2.initialize, (address(recruitmentV1), "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) + ) + ) + ); + recruitmentV1 = BadgeRecruitment(proxy); + + s1BadgesV5.setRecruitmentContract(address(recruitmentV1)); + s2Badges.setMinter(address(recruitmentV1)); + // enable recruitment for BADGE_ID + uint256[] memory enabledBadgeIds = new uint256[](1); + enabledBadgeIds[0] = BADGE_ID; + recruitmentV1.enableRecruitments(enabledBadgeIds); + + vm.stopPrank(); + } + + function wait(uint256 time) public { + vm.warp(block.timestamp + time); + } + + function test_upgradeV2() public { + vm.startPrank(owner); + recruitmentV1.upgradeToAndCall( + address(new BadgeRecruitmentV2()), abi.encodeCall(BadgeRecruitmentV2.version, ()) + ); + + recruitment = BadgeRecruitmentV2(address(recruitmentV1)); + + assertEq(recruitment.version(), "V2"); + + s1BadgesV5.setRecruitmentContractV2(address(recruitment)); + vm.stopPrank(); + } + + function test_legacy_startRecruitment_throws() public { + test_upgradeV2(); + vm.expectRevert(TrailblazersBadgesV5.NOT_IMPLEMENTED.selector); + s1BadgesV5.startRecruitment(BADGE_ID); + } + + function test_full_recruitment() public { + test_upgradeV2(); + + address minter = minters[0]; + vm.startPrank(minter); + + // mint the badge + bytes32 _hash = s1BadgesV5.getHash(minter, BADGE_ID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + bool canMint = s1BadgesV5.canMint(abi.encodePacked(r, s, v), minter, BADGE_ID); + assertTrue(canMint); + + s1BadgesV5.mint(abi.encodePacked(r, s, v), BADGE_ID); + uint256 tokenId = s1BadgesV5.tokenOfOwnerByIndex(minter, 0); + + vm.stopPrank(); + + // mint and transfer to minter a secondary badge with id 0 + + vm.startPrank(minters[1]); + _hash = s1BadgesV5.getHash(minters[1], BADGE_ID); + (v, r, s) = vm.sign(mintSignerPk, _hash); + canMint = s1BadgesV5.canMint(abi.encodePacked(r, s, v), minters[1], BADGE_ID); + assertTrue(canMint); + + s1BadgesV5.mint(abi.encodePacked(r, s, v), BADGE_ID); + uint256 secondTokenId = s1BadgesV5.tokenOfOwnerByIndex(minters[1], 0); + + s1BadgesV5.transferFrom(minters[1], minter, secondTokenId); + + // ensure balances + assertEq(s1BadgesV5.balanceOf(minter), 2); + assertEq(s1BadgesV5.balanceOf(minters[1]), 0); + vm.stopPrank(); + + // start migration with first badge, using v1 methods + vm.startPrank(minter); + wait(100); + s1BadgesV5.startRecruitment(BADGE_ID, tokenId); + assertEq(recruitment.isRecruitmentActive(minter), true); + assertEq(s1BadgesV5.balanceOf(minter), 2); + assertEq(s1BadgesV5.unlockTimestamps(tokenId), block.timestamp + 365 days); + + // and end it + wait(COOLDOWN_INFLUENCE); + wait(COOLDOWN_RECRUITMENT); + + // generate the claim hash for the current recruitment + bytes32 claimHash = recruitment.generateClaimHash( + BadgeRecruitment.HashType.End, + minter, + 0 // experience points + ); + + // simulate the backend signing the hash + (v, r, s) = vm.sign(mintSignerPk, claimHash); + + // exercise the randomFromSignature function + recruitment.endRecruitment(claimHash, v, r, s, 0); + + // check for s2 state reset + assertEq(recruitment.isRecruitmentActive(minter), false); + assertEq(recruitment.isInfluenceActive(minter), false); + + // check for s2 mint + assertEq(s2Badges.balanceOf(minter, 1), 1); + + // open a second migration cycle + vm.stopPrank(); + vm.startPrank(owner); + + // enable recruitment for BADGE_ID + uint256[] memory enabledBadgeIds = new uint256[](1); + enabledBadgeIds[0] = BADGE_ID; + recruitment.forceDisableAllRecruitments(); + recruitment.enableRecruitments(enabledBadgeIds); + vm.stopPrank(); + + // expect legacy method to fail + vm.startPrank(minter); + wait(100); + vm.expectRevert(TrailblazersBadgesV4.BADGE_LOCKED.selector); + s1BadgesV5.startRecruitment(BADGE_ID, tokenId); + // time to start the second migration + wait(100); + + s1BadgesV5.startRecruitment(BADGE_ID, secondTokenId); + assertEq(recruitment.isRecruitmentActive(minter), true); + assertEq(s1BadgesV5.balanceOf(minter), 2); + assertEq(s1BadgesV5.unlockTimestamps(secondTokenId), block.timestamp + 365 days); + + vm.stopPrank(); + } + + function test_resetRecruitment() public { + test_full_recruitment(); + uint256 initialCycleId = recruitment.recruitmentCycleId(); + + assertEq(initialCycleId, 2); + // roll forward the cycle + vm.startPrank(owner); + + uint256[] memory enabledBadgeIds = new uint256[](1); + enabledBadgeIds[0] = BADGE_ID; + recruitment.forceDisableAllRecruitments(); + recruitment.enableRecruitments(enabledBadgeIds); + vm.stopPrank(); + + uint256 cycleId = recruitment.recruitmentCycleId(); + assertEq(cycleId, 3); + + address minter = minters[0]; + uint256 ongoingTokenId = s1BadgesV5.tokenOfOwnerByIndex(minter, 1); + uint256 completedTokenId = s1BadgesV5.tokenOfOwnerByIndex(minter, 0); + + assertEq(s2Badges.balanceOf(minter, 1), 1); + + vm.startPrank(minter); + + // expect revert from the completed migration + vm.expectRevert(TrailblazersBadgesV5.RECRUITMENT_NOT_FOUND.selector); + s1BadgesV5.resetMigration(completedTokenId, BADGE_ID, initialCycleId); + // on both cycles + vm.expectRevert(TrailblazersBadgesV5.RECRUITMENT_NOT_FOUND.selector); + s1BadgesV5.resetMigration(completedTokenId, BADGE_ID, cycleId); + // as well as for the ongoing migration on the current cycle + vm.expectRevert(TrailblazersBadgesV5.RECRUITMENT_NOT_FOUND.selector); + s1BadgesV5.resetMigration(ongoingTokenId, BADGE_ID, cycleId); + + // reset the ongoing recruitment + s1BadgesV5.resetMigration(ongoingTokenId, BADGE_ID, initialCycleId); + + // start over the ongoing migration + wait(100); + s1BadgesV5.startRecruitment(BADGE_ID, ongoingTokenId); + assertEq(recruitment.isRecruitmentActive(minter), true); + assertEq(s1BadgesV5.balanceOf(minter), 2); + assertEq(s1BadgesV5.unlockTimestamps(ongoingTokenId), block.timestamp + 365 days); + + // and end it + wait(COOLDOWN_INFLUENCE); + wait(COOLDOWN_RECRUITMENT); + + bytes32 claimHash = recruitment.generateClaimHash( + BadgeRecruitment.HashType.End, + minter, + 0 // experience points + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, claimHash); + recruitment.endRecruitment(claimHash, v, r, s, 0); + + // check for s2 state reset + assertEq(recruitment.isRecruitmentActive(minter), false); + assertEq(recruitment.isInfluenceActive(minter), false); + + // check for s2 mint + assertEq(s2Badges.balanceOf(minter, 2), 1); + + // fail to reset the ongoing migration + vm.expectRevert(TrailblazersBadgesV5.RECRUITMENT_NOT_FOUND.selector); + s1BadgesV5.resetMigration(ongoingTokenId, BADGE_ID, cycleId); + } + + function test_revertStartTooCloseToCycleEnd() public { + test_upgradeV2(); + address minter = minters[0]; + vm.startPrank(minter); + + // mint the badge + bytes32 _hash = s1BadgesV5.getHash(minter, BADGE_ID); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(mintSignerPk, _hash); + bool canMint = s1BadgesV5.canMint(abi.encodePacked(r, s, v), minter, BADGE_ID); + assertTrue(canMint); + + s1BadgesV5.mint(abi.encodePacked(r, s, v), BADGE_ID); + uint256 tokenId = s1BadgesV5.tokenOfOwnerByIndex(minter, 0); + + // wait until almost the end + wait(DEFAULT_CYCLE_DURATION - COOLDOWN_RECRUITMENT + 1); + vm.expectRevert(BadgeRecruitmentV2.NOT_ENOUGH_TIME_LEFT.selector); + s1BadgesV5.startRecruitment(BADGE_ID, tokenId); + assertEq(recruitment.isRecruitmentActive(minter), false); + vm.stopPrank(); + } +}