diff --git a/contracts/QuestFactory.sol b/contracts/QuestFactory.sol index c9faf901..55a1349d 100644 --- a/contracts/QuestFactory.sol +++ b/contracts/QuestFactory.sol @@ -150,7 +150,7 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto /// @param questId_ The id of the quest /// @param actionType_ The action type for the quest /// @return address the quest contract address - function create1155QuestAndQueue( + function createERC1155Quest( address rewardTokenAddress_, uint256 endTime_, uint256 startTime_, @@ -160,52 +160,52 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto string memory actionType_, string memory questName_ ) external payable nonReentrant returns (address) { - Quest storage currentQuest = quests[questId_]; - - if (msg.value < totalQuestNFTFee(totalParticipants_)) revert MsgValueLessThanQuestNFTFee(); - if (currentQuest.questAddress != address(0)) revert QuestIdUsed(); - - address payable newQuest = - payable(erc1155QuestAddress.cloneDeterministic(keccak256(abi.encodePacked(msg.sender, block.chainid, block.timestamp)))); - currentQuest.questAddress = address(newQuest); - currentQuest.totalParticipants = totalParticipants_; - currentQuest.questAddress.safeTransferETH(msg.value); - currentQuest.questType = "erc1155"; - currentQuest.questCreator = msg.sender; - currentQuest.actionType = actionType_; - currentQuest.questName = questName_; - IQuest1155Ownable questContract = IQuest1155Ownable(newQuest); - - questContract.initialize( - rewardTokenAddress_, - endTime_, - startTime_, - totalParticipants_, - tokenId_, - protocolFeeRecipient, - questId_ + return createERC1155QuestInternal( + ERC1155QuestData( + rewardTokenAddress_, + endTime_, + startTime_, + totalParticipants_, + tokenId_, + questId_, + actionType_, + questName_ + ) ); + } - IERC1155(rewardTokenAddress_).safeTransferFrom(msg.sender, newQuest, tokenId_, totalParticipants_, "0x00"); - questContract.queue(); - questContract.transferOwnership(msg.sender); - - emit QuestCreated( - msg.sender, - address(newQuest), - questId_, - "erc1155", - rewardTokenAddress_, - endTime_, - startTime_, - totalParticipants_, - tokenId_ + /// @notice Depricated + /// @dev Create an erc1155 quest and start it at the same time. The function will transfer the reward amount to the quest contract + /// @param rewardTokenAddress_ The contract address of the reward token + /// @param endTime_ The end time of the quest + /// @param startTime_ The start time of the quest + /// @param totalParticipants_ The total amount of participants (accounts) the quest will have + /// @param tokenId_ The reward token id of the erc1155 at rewardTokenAddress_ + /// @param questId_ The id of the quest + /// @return address the quest contract address + function create1155QuestAndQueue( + address rewardTokenAddress_, + uint256 endTime_, + uint256 startTime_, + uint256 totalParticipants_, + uint256 tokenId_, + string memory questId_, + string memory + ) external payable nonReentrant returns (address) { + return createERC1155QuestInternal( + ERC1155QuestData( + rewardTokenAddress_, + endTime_, + startTime_, + totalParticipants_, + tokenId_, + questId_, + "", + "" + ) ); - - return newQuest; } - // Note: Should probably be called `createQuest()` /// @dev Create an erc20 quest and start it at the same time. The function will transfer the reward amount to the quest contract /// @param rewardTokenAddress_ The contract address of the reward token /// @param endTime_ The end time of the quest @@ -216,7 +216,7 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto /// @param actionType_ The action type for the quest /// @param questName_ The name of the quest /// @return address the quest contract address - function createQuestAndQueue( + function createERC20Quest( address rewardTokenAddress_, uint256 endTime_, uint256 startTime_, @@ -226,7 +226,7 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto string memory actionType_, string memory questName_ ) external checkQuest(questId_, rewardTokenAddress_) returns (address) { - address newQuest = createERC20QuestInternal( + return createERC20QuestInternal( ERC20QuestData( rewardTokenAddress_, endTime_, @@ -240,8 +240,41 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto "erc20" ) ); - transferTokensAndOwnership(newQuest, rewardTokenAddress_); - return newQuest; + } + + /// @notice Depricated + /// @dev Create an erc20 quest and start it at the same time. The function will transfer the reward amount to the quest contract + /// @param rewardTokenAddress_ The contract address of the reward token + /// @param endTime_ The end time of the quest + /// @param startTime_ The start time of the quest + /// @param totalParticipants_ The total amount of participants (accounts) the quest will have + /// @param rewardAmount_ The reward amount for an erc20 quest + /// @param questId_ The id of the quest + /// @return address the quest contract address + function createQuestAndQueue( + address rewardTokenAddress_, + uint256 endTime_, + uint256 startTime_, + uint256 totalParticipants_, + uint256 rewardAmount_, + string memory questId_, + string memory, + uint256 + ) external checkQuest(questId_, rewardTokenAddress_) returns (address) { + return createERC20QuestInternal( + ERC20QuestData( + rewardTokenAddress_, + endTime_, + startTime_, + totalParticipants_, + rewardAmount_, + questId_, + "", + "", + 0, + "erc20" + ) + ); } /*////////////////////////////////////////////////////////////// @@ -270,45 +303,6 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto this.claimOptimized{value: msg.value}(abi.encodePacked(r,vs), claimData); } - function buildJsonString( - string memory txHash, - string memory txHashChainId, - string memory actionType, - string memory questName - ) public pure returns (string memory) { - // { - // actionTxHashes: ["actionTxHash1"], - // actionNetworkChainIds: ["chainId1"], - // questName: "quest name", - // actionType: "mint" - // } - return string(abi.encodePacked( - '{"actionTxHashes":["', txHash, - '"],"actionNetworkChainIds":[', txHashChainId, - '],"questName":"', questName, - '","actionType":"', actionType, '"}' - )); - } - - function bytes16ToUUID(bytes16 data) public pure returns (string memory) { - bytes memory hexChars = "0123456789abcdef"; - bytes memory uuid = new bytes(36); // UUID length with hyphens - - uint256 j = 0; // Position in uuid - for (uint256 i = 0; i < 16; i++) { - // Insert hyphens at the appropriate positions (after 4, 6, 8, 10 bytes) - if (i == 4 || i == 6 || i == 8 || i == 10) { - uuid[j++] = '-'; - } - - uuid[j++] = hexChars[uint8(data[i] >> 4)]; - uuid[j++] = hexChars[uint8(data[i] & 0x0F)]; - } - - return string(uuid); - } - - function claimOptimized(bytes calldata signature_, bytes calldata data_) external payable { ( address claimer_, @@ -645,6 +639,54 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto } } + /// @dev Internal function to create an erc1155 quest + /// @param data_ The erc20 quest data struct + function createERC1155QuestInternal(ERC1155QuestData memory data_) internal returns (address) { + Quest storage currentQuest = quests[data_.questId]; + + if (msg.value < totalQuestNFTFee(data_.totalParticipants)) revert MsgValueLessThanQuestNFTFee(); + if (currentQuest.questAddress != address(0)) revert QuestIdUsed(); + + address payable newQuest = + payable(erc1155QuestAddress.cloneDeterministic(keccak256(abi.encodePacked(msg.sender, block.chainid, block.timestamp)))); + currentQuest.questAddress = address(newQuest); + currentQuest.totalParticipants = data_.totalParticipants; + currentQuest.questAddress.safeTransferETH(msg.value); + currentQuest.questType = "erc1155"; + currentQuest.questCreator = msg.sender; + currentQuest.actionType = data_.actionType; + currentQuest.questName = data_.questName; + IQuest1155Ownable questContract = IQuest1155Ownable(newQuest); + + questContract.initialize( + data_.rewardTokenAddress, + data_.endTime, + data_.startTime, + data_.totalParticipants, + data_.tokenId, + protocolFeeRecipient, + data_.questId + ); + + IERC1155(data_.rewardTokenAddress).safeTransferFrom(msg.sender, newQuest, data_.tokenId, data_.totalParticipants, "0x00"); + questContract.queue(); + questContract.transferOwnership(msg.sender); + + emit QuestCreated( + msg.sender, + address(newQuest), + data_.questId, + "erc1155", + data_.rewardTokenAddress, + data_.endTime, + data_.startTime, + data_.totalParticipants, + data_.tokenId + ); + + return newQuest; + } + /// @dev Internal function to create an erc20 quest /// @param data_ The erc20 quest data struct function createERC20QuestInternal(ERC20QuestData memory data_) internal returns (address) { @@ -684,6 +726,7 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto sablierV2LockupLinearAddress ); + transferTokensAndOwnership(newQuest, data_.rewardTokenAddress); return newQuest; } @@ -740,6 +783,44 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto questContract.transferOwnership(sender); } + function buildJsonString( + string memory txHash, + string memory txHashChainId, + string memory actionType, + string memory questName + ) internal pure returns (string memory) { + // { + // actionTxHashes: ["actionTxHash1"], + // actionNetworkChainIds: ["chainId1"], + // questName: "quest name", + // actionType: "mint" + // } + return string(abi.encodePacked( + '{"actionTxHashes":["', txHash, + '"],"actionNetworkChainIds":[', txHashChainId, + '],"questName":"', questName, + '","actionType":"', actionType, '"}' + )); + } + + function bytes16ToUUID(bytes16 data) internal pure returns (string memory) { + bytes memory hexChars = "0123456789abcdef"; + bytes memory uuid = new bytes(36); // UUID length with hyphens + + uint256 j = 0; // Position in uuid + for (uint256 i = 0; i < 16; i++) { + // Insert hyphens at the appropriate positions (after 4, 6, 8, 10 bytes) + if (i == 4 || i == 6 || i == 8 || i == 10) { + uuid[j++] = '-'; + } + + uuid[j++] = hexChars[uint8(data[i] >> 4)]; + uuid[j++] = hexChars[uint8(data[i] & 0x0F)]; + } + + return string(uuid); + } + /*////////////////////////////////////////////////////////////// DEFAULTS //////////////////////////////////////////////////////////////*/ diff --git a/contracts/interfaces/IQuestFactory.sol b/contracts/interfaces/IQuestFactory.sol index 93d27d4d..e8f04233 100644 --- a/contracts/interfaces/IQuestFactory.sol +++ b/contracts/interfaces/IQuestFactory.sol @@ -83,6 +83,17 @@ interface IQuestFactory { string questType; } + struct ERC1155QuestData { + address rewardTokenAddress; + uint256 endTime; + uint256 startTime; + uint256 totalParticipants; + uint256 tokenId; + string questId; + string actionType; + string questName; + } + // Events event ExtraMintFeeReturned(address indexed recipient, uint256 amount); event MintFeeSet(uint256 mintFee); @@ -160,8 +171,7 @@ interface IQuestFactory { uint256 totalParticipants_, uint256 tokenId_, string memory questId_, - string memory actionType_, - string memory questName_ + string memory ) external payable returns (address); // Set diff --git a/test/QuestFactory.t.sol b/test/QuestFactory.t.sol index 8b902553..67f97317 100644 --- a/test/QuestFactory.t.sol +++ b/test/QuestFactory.t.sol @@ -85,13 +85,13 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { /*////////////////////////////////////////////////////////////// CREATE QUESTS //////////////////////////////////////////////////////////////*/ - function test_create1155QuestAndQueue() public { + function test_createERC1155Quest() public { vm.startPrank(questCreator); sampleERC1155.mintSingle(questCreator, 1, TOTAL_PARTICIPANTS); sampleERC1155.setApprovalForAll(address(questFactory), true); - address questAddress = questFactory.create1155QuestAndQueue{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS}( + address questAddress = questFactory.createERC1155Quest{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS}( address(sampleERC1155), END_TIME, START_TIME, @@ -108,14 +108,14 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { vm.stopPrank(); } - function test_RevertIf_create1155QuestAndQueue_MsgValueLessThanQuestNFTFee() public { + function test_RevertIf_createERC1155Quest_MsgValueLessThanQuestNFTFee() public { vm.startPrank(questCreator); sampleERC1155.mintSingle(questCreator, 1, TOTAL_PARTICIPANTS); sampleERC1155.setApprovalForAll(address(questFactory), true); vm.expectRevert(abi.encodeWithSelector(MsgValueLessThanQuestNFTFee.selector)); - questFactory.create1155QuestAndQueue{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS - 1}( + questFactory.createERC1155Quest{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS - 1}( address(sampleERC1155), END_TIME, START_TIME, @@ -127,7 +127,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { ); } - function test_createQuestAndQueue() public{ + function test_createERC20Quest() public{ vm.startPrank(owner); questFactory.setRewardAllowlistAddress(address(sampleERC20), true); @@ -137,7 +137,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { vm.expectEmit(true,false,true,true); emit QuestCreated(questCreator, address(0), "questId", "erc20", address(sampleERC20), END_TIME, START_TIME, TOTAL_PARTICIPANTS, REWARD_AMOUNT); - address questAddress = questFactory.createQuestAndQueue( + address questAddress = questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -156,14 +156,14 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { vm.stopPrank(); } - function test_RevertIf_createQuestAndQueue_RewardNotAllowed() public{ + function test_RevertIf_createERC20Quest_RewardNotAllowed() public{ vm.startPrank(owner); questFactory.setRewardAllowlistAddress(address(sampleERC20), false); vm.startPrank(questCreator); sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(TOTAL_PARTICIPANTS, REWARD_AMOUNT, QUEST_FEE)); vm.expectRevert(abi.encodeWithSelector(RewardNotAllowed.selector)); - questFactory.createQuestAndQueue( + questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -175,13 +175,13 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { ); } - function test_RevertIf_createQuestAndQueue_QuestIdUsed() public{ + function test_RevertIf_createERC20Quest_QuestIdUsed() public{ vm.startPrank(owner); questFactory.setRewardAllowlistAddress(address(sampleERC20), true); vm.startPrank(questCreator); sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(TOTAL_PARTICIPANTS, REWARD_AMOUNT, QUEST_FEE)); - questFactory.createQuestAndQueue( + questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -193,7 +193,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { ); vm.expectRevert(abi.encodeWithSelector(QuestIdUsed.selector)); - questFactory.createQuestAndQueue( + questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -205,7 +205,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { ); } - function test_RevertIf_createQuestAndQueue_Erc20QuestAddressNotSet() public{ + function test_RevertIf_createERC20Quest_Erc20QuestAddressNotSet() public{ vm.startPrank(owner); questFactory.setRewardAllowlistAddress(address(sampleERC20), true); questFactory.setErc20QuestAddress(address(0)); @@ -214,7 +214,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(TOTAL_PARTICIPANTS, REWARD_AMOUNT, QUEST_FEE)); vm.expectRevert(abi.encodeWithSelector(Erc20QuestAddressNotSet.selector)); - questFactory.createQuestAndQueue( + questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -235,7 +235,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { sampleERC1155.mintSingle(questCreator, 1, TOTAL_PARTICIPANTS); sampleERC1155.setApprovalForAll(address(questFactory), true); - questFactory.create1155QuestAndQueue{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS}( + questFactory.createERC1155Quest{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS}( address(sampleERC1155), END_TIME, START_TIME, @@ -291,7 +291,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { vm.startPrank(questCreator); sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(TOTAL_PARTICIPANTS, REWARD_AMOUNT, QUEST_FEE)); - questFactory.createQuestAndQueue( + questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -320,7 +320,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { vm.startPrank(questCreator); sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(TOTAL_PARTICIPANTS, REWARD_AMOUNT, QUEST_FEE)); - address questAddress = questFactory.createQuestAndQueue( + address questAddress = questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -391,7 +391,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { sampleERC1155.mintSingle(questCreator, 1, TOTAL_PARTICIPANTS); sampleERC1155.setApprovalForAll(address(questFactory), true); - questFactory.create1155QuestAndQueue{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS}( + questFactory.createERC1155Quest{value: NFT_QUEST_FEE * TOTAL_PARTICIPANTS}( address(sampleERC1155), END_TIME, START_TIME, @@ -424,7 +424,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { vm.startPrank(questCreator); sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(TOTAL_PARTICIPANTS, REWARD_AMOUNT, QUEST_FEE)); - address questAddress = questFactory.createQuestAndQueue( + address questAddress = questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME, @@ -477,7 +477,7 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils { vm.startPrank(questCreator); sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(TOTAL_PARTICIPANTS, REWARD_AMOUNT, QUEST_FEE)); - address questAddress = questFactory.createQuestAndQueue( + address questAddress = questFactory.createERC20Quest( address(sampleERC20), END_TIME, START_TIME,