Skip to content

Commit

Permalink
feat(quests): implement cancel operation for quests [BOOST-3960] (#277)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan Diep <[email protected]>
  • Loading branch information
ccashwell and jonathandiep authored May 16, 2024
1 parent d0e7c24 commit 01e85fd
Show file tree
Hide file tree
Showing 42 changed files with 2,337 additions and 505 deletions.
2 changes: 1 addition & 1 deletion Quest.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Quest1155.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion QuestFactory.json

Large diffs are not rendered by default.

60 changes: 30 additions & 30 deletions broadcast/Quest.s.sol/10/run-latest.json

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions broadcast/Quest.s.sol/11155111/run-1715718557.json

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions broadcast/Quest.s.sol/11155111/run-1715718618.json

Large diffs are not rendered by default.

98 changes: 49 additions & 49 deletions broadcast/Quest.s.sol/11155111/run-latest.json

Large diffs are not rendered by default.

98 changes: 49 additions & 49 deletions broadcast/Quest.s.sol/137/run-latest.json

Large diffs are not rendered by default.

60 changes: 30 additions & 30 deletions broadcast/Quest.s.sol/42161/run-latest.json

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions broadcast/Quest.s.sol/666666666/run-1715883279.json

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions broadcast/Quest.s.sol/666666666/run-1715883493.json

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions broadcast/Quest.s.sol/666666666/run-latest.json

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions broadcast/Quest.s.sol/7777777/run-1715881634.json

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions broadcast/Quest.s.sol/7777777/run-1715881775.json

Large diffs are not rendered by default.

64 changes: 32 additions & 32 deletions broadcast/Quest.s.sol/7777777/run-latest.json

Large diffs are not rendered by default.

60 changes: 30 additions & 30 deletions broadcast/Quest.s.sol/81457/run-latest.json

Large diffs are not rendered by default.

60 changes: 30 additions & 30 deletions broadcast/Quest.s.sol/8453/run-latest.json

Large diffs are not rendered by default.

105 changes: 86 additions & 19 deletions broadcast/QuestFactory.s.sol/10/run-latest.json

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions broadcast/QuestFactory.s.sol/11155111/run-1715718479.json

Large diffs are not rendered by default.

106 changes: 52 additions & 54 deletions broadcast/QuestFactory.s.sol/11155111/run-latest.json

Large diffs are not rendered by default.

134 changes: 108 additions & 26 deletions broadcast/QuestFactory.s.sol/137/run-latest.json

Large diffs are not rendered by default.

103 changes: 85 additions & 18 deletions broadcast/QuestFactory.s.sol/42161/run-latest.json

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions broadcast/QuestFactory.s.sol/666666666/run-1715882002.json

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions broadcast/QuestFactory.s.sol/666666666/run-1715882643.json

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions broadcast/QuestFactory.s.sol/666666666/run-1715883067.json

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions broadcast/QuestFactory.s.sol/666666666/run-latest.json

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions broadcast/QuestFactory.s.sol/7777777/run-1715881467.json

Large diffs are not rendered by default.

103 changes: 84 additions & 19 deletions broadcast/QuestFactory.s.sol/7777777/run-latest.json

Large diffs are not rendered by default.

107 changes: 87 additions & 20 deletions broadcast/QuestFactory.s.sol/81457/run-latest.json

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions broadcast/QuestFactory.s.sol/8453/run-1715878115.json

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions broadcast/QuestFactory.s.sol/8453/run-1715880200.json

Large diffs are not rendered by default.

76 changes: 56 additions & 20 deletions broadcast/QuestFactory.s.sol/8453/run-latest.json

Large diffs are not rendered by default.

14 changes: 5 additions & 9 deletions contracts/Quest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,12 @@ contract Quest is ReentrancyGuardUpgradeable, PausableUpgradeable, Ownable, IQue
/*//////////////////////////////////////////////////////////////
EXTERNAL UPDATE
//////////////////////////////////////////////////////////////*/
/// @notice Pauses the Quest
/// @dev Only the owner of the Quest can call this function. Also requires that the Quest has started (not by date, but by calling the start function)
function pause() external onlyOwner {
_pause();
}

/// @notice Unpauses the Quest
/// @dev Only the owner of the Quest can call this function. Also requires that the Quest has started (not by date, but by calling the start function)
function unPause() external onlyOwner {
_unpause();
/// @notice Cancels the Quest by setting the end time to 15 minutes from the current time and pausing the Quest. If the Quest has not yet started, it will end immediately.
/// @dev Only the owner of the Quest can call this function.
function cancel() external onlyQuestFactory whenNotPaused whenNotEnded {
_pause();
endTime = startTime > block.timestamp ? block.timestamp : block.timestamp + 15 minutes;
}

/// @dev transfers rewards to the account, can only be called once per account per quest and only by the quest factory
Expand Down
14 changes: 5 additions & 9 deletions contracts/Quest1155.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ contract Quest1155 is ERC1155Holder, ReentrancyGuardUpgradeable, PausableUpgrade
/*//////////////////////////////////////////////////////////////
EXTERNAL UPDATE
//////////////////////////////////////////////////////////////*/
/// @notice Pauses the Quest
/// @dev Only the owner of the Quest can call this function. Also requires that the Quest has started (not by date, but by calling the start function)
function pause() external onlyOwner onlyStarted {

/// @notice Cancels the Quest by setting the end time to 15 minutes from the current time and pausing the Quest. If the Quest has not yet started, it will end immediately.
/// @dev Only the owner of the Quest can call this function.
function cancel() external onlyQuestFactory whenNotPaused whenNotEnded {
_pause();
endTime = startTime > block.timestamp ? block.timestamp : block.timestamp + 15 minutes;
}

/// @notice Queues the quest by marking it ready to start at the contract level. Marking a quest as queued does not mean that it is live. It also requires that the start time has passed
Expand Down Expand Up @@ -142,12 +144,6 @@ contract Quest1155 is ERC1155Holder, ReentrancyGuardUpgradeable, PausableUpgrade
if (questFee > 0) protocolFeeRecipient.safeTransferETH(questFee);
}

/// @notice Unpauses the Quest
/// @dev Only the owner of the Quest can call this function. Also requires that the Quest has started (not by date, but by calling the start function)
function unPause() external onlyOwner onlyStarted {
_unpause();
}

/// @dev Function that transfers all 1155 tokens in the contract to the owner (creator), and eth to the protocol fee recipient and the owner
/// @notice This function can only be called after the quest end time.
function withdrawRemainingTokens() external onlyEnded {
Expand Down
8 changes: 8 additions & 0 deletions contracts/QuestFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto
);
}

function cancelQuest(string calldata questId_) external {
Quest storage _questData = quests[questId_];
if (_questData.questCreator != msg.sender) revert Unauthorized();
IQuestOwnable quest = IQuestOwnable(_questData.questAddress);
quest.cancel();
emit QuestCancelled(_questData.questAddress, questId_, quest.endTime());
}

/*//////////////////////////////////////////////////////////////
CLAIM
//////////////////////////////////////////////////////////////*/
Expand Down
1 change: 1 addition & 0 deletions contracts/interfaces/IQuest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ interface IQuest {
function startTime() external view returns (uint256);
function endTime() external view returns (uint256);
function singleClaim(address account) external;
function cancel() external;
function rewardToken() external view returns (address);
function rewardAmountInWei() external view returns (uint256);
function totalTransferAmount() external view returns (uint256);
Expand Down
3 changes: 1 addition & 2 deletions contracts/interfaces/IQuest1155.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ interface IQuest1155 {
function rewardToken() external view returns (address);

// Update Functions
function pause() external;
function cancel() external;
function queue() external;
function singleClaim(address account_) external;
function unPause() external;
function withdrawRemainingTokens() external;
}
2 changes: 2 additions & 0 deletions contracts/interfaces/IQuestFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ interface IQuestFactory {
event NftQuestFeeListSet(address[] addresses, uint256[] fees);
event NftQuestFeeSet(uint256 nftQuestFee);

event QuestCancelled(address indexed questAddress, string questId, uint256 endsAt);

event QuestClaimedData(
address indexed recipient,
address indexed questAddress,
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mantle = "https://rpc.mantle.xyz/"
scroll = "https://rpc.scroll.io/"
blast = "https://restless-divine-meme.blast-mainnet.quiknode.pro/${QUICKNODE_BLAST_API_KEY}/"
zora = "https://rpc.zora.co"
degen = "https://rpc.degen.tips"

[etherscan]
mainnet = { key = "${MAIN_ETHERSCAN_API_KEY}" }
Expand Down
46 changes: 19 additions & 27 deletions test/Quest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -139,33 +139,34 @@ contract TestQuest is Test, TestUtils, Errors, Events {
}

/*//////////////////////////////////////////////////////////////
PAUSE
CANCEL
//////////////////////////////////////////////////////////////*/
function test_pause() public {
function test_cancel() public {
vm.prank(questFactoryMock);
quest.pause();
quest.cancel();
assertTrue(quest.paused(), "paused should be true");
assertEq(quest.endTime(), block.timestamp, "endTime should be now (quest not started)");
}

function test_RevertIf_pause_Unauthorized() public {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
quest.pause();
function test_cancel_afterStarted() public {
vm.warp(START_TIME);
vm.prank(questFactoryMock);
quest.cancel();
assertTrue(quest.paused(), "paused should be true");
assertEq(quest.endTime(), block.timestamp + 15 minutes, "endTime should be 15 minutes from now");
}

/*//////////////////////////////////////////////////////////////
UNPAUSE
//////////////////////////////////////////////////////////////*/
function test_unpause() public {
vm.startPrank(questFactoryMock);
quest.pause();
quest.unPause();
assertFalse(quest.paused(), "paused should be false");
vm.stopPrank();
function test_cancel_alreadyCanceled() public {
vm.prank(questFactoryMock);
quest.cancel();
vm.expectRevert("Pausable: paused");
vm.prank(questFactoryMock);
quest.cancel();
}

function test_RevertIf_unpause_Unauthorized() public {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
quest.unPause();
function test_RevertIf_cancel_Unauthorized() public {
vm.expectRevert(abi.encodeWithSelector(NotQuestFactory.selector));
quest.cancel();
}

/*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -253,15 +254,6 @@ contract TestQuest is Test, TestUtils, Errors, Events {
quest.singleClaim(participant);
}

function test_RevertIf_singleClaim_whenNotPaused() public {
vm.startPrank(questFactoryMock);
quest.pause();
vm.warp(START_TIME);
vm.expectRevert("Pausable: paused");
quest.singleClaim(participant);
vm.stopPrank();
}

/*//////////////////////////////////////////////////////////////
WITHDRAWREMAININGTOKENS
//////////////////////////////////////////////////////////////*/
Expand Down
46 changes: 17 additions & 29 deletions test/Quest1155.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -109,31 +109,31 @@ contract TestQuest1155 is Test, Errors, Events, TestUtils {
/*//////////////////////////////////////////////////////////////
PAUSE
//////////////////////////////////////////////////////////////*/
function test_pause() public {
function test_cancel() public {
vm.prank(questFactoryMock);
quest.pause();
quest.cancel();
assertTrue(quest.paused(), "paused should be true");
assertEq(quest.endTime(), block.timestamp + 15 minutes, "endTime should be 15 minutes from now");
}

function test_RevertIf_pause_Unauthorized() public {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
quest.pause();
function test_cancel_notStarted() public {
vm.warp(START_TIME - 1);
vm.prank(questFactoryMock);
quest.cancel();
assertEq(quest.endTime(), block.timestamp, "endTime should be now");
}

/*//////////////////////////////////////////////////////////////
UNPAUSE
//////////////////////////////////////////////////////////////*/
function test_unpause() public {
vm.startPrank(questFactoryMock);
quest.pause();
quest.unPause();
assertFalse(quest.paused(), "paused should be false");
vm.stopPrank();
function test_cancel_alreadyCanceled() public {
vm.prank(questFactoryMock);
quest.cancel();
vm.expectRevert("Pausable: paused");
vm.prank(questFactoryMock);
quest.cancel();
}

function test_RevertIf_unpause_Unauthorized() public {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
quest.unPause();
function test_RevertIf_cancel_Unauthorized() public {
vm.expectRevert(abi.encodeWithSelector(NotQuestFactory.selector));
quest.cancel();
}

// /*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -199,18 +199,6 @@ contract TestQuest1155 is Test, Errors, Events, TestUtils {
quest.singleClaim(participant);
}

function test_RevertIf_singleClaim_whenNotPaused() public {
vm.deal(address(quest), LARGE_ETH_AMOUNT);
vm.prank(questFactoryMock);
quest.queue();
vm.startPrank(questFactoryMock);
quest.pause();
vm.warp(START_TIME);
vm.expectRevert("Pausable: paused");
quest.singleClaim(participant);
vm.stopPrank();
}

// /*//////////////////////////////////////////////////////////////
// WITHDRAWREMAININGTOKENS
// //////////////////////////////////////////////////////////////*/
Expand Down
77 changes: 77 additions & 0 deletions test/QuestFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,83 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils {
questFactory.claimOptimized{value: MINT_FEE}(signature, data);
}

/*//////////////////////////////////////////////////////////////
CANCEL
//////////////////////////////////////////////////////////////*/

function test_cancelQuest() public {
vm.startPrank(questCreator);
sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(QUEST.TOTAL_PARTICIPANTS, QUEST.REWARD_AMOUNT, QUEST_FEE));
address questAddress = questFactory.createERC20Quest(
QUEST.CHAIN_ID,
address(sampleERC20),
QUEST.END_TIME,
QUEST.START_TIME,
QUEST.TOTAL_PARTICIPANTS,
QUEST.REWARD_AMOUNT,
QUEST.QUEST_ID_STRING,
QUEST.ACTION_TYPE,
QUEST.QUEST_NAME,
QUEST.PROJECT_NAME,
QUEST.REFERRAL_REWARD_FEE
);

vm.startPrank(questCreator);
questFactory.cancelQuest(QUEST.QUEST_ID_STRING);

Quest quest = Quest(payable(questAddress));
assertEq(quest.paused(), true, "quest should be paused");
assertEq(quest.endTime(), block.timestamp, "endTime should be now");
}

function test_cancelQuest_alreadyStarted() public {
vm.startPrank(questCreator);
sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(QUEST.TOTAL_PARTICIPANTS, QUEST.REWARD_AMOUNT, QUEST_FEE));
address questAddress = questFactory.createERC20Quest(
QUEST.CHAIN_ID,
address(sampleERC20),
QUEST.END_TIME,
QUEST.START_TIME,
QUEST.TOTAL_PARTICIPANTS,
QUEST.REWARD_AMOUNT,
QUEST.QUEST_ID_STRING,
QUEST.ACTION_TYPE,
QUEST.QUEST_NAME,
QUEST.PROJECT_NAME,
QUEST.REFERRAL_REWARD_FEE
);

vm.warp(QUEST.START_TIME + 1);
vm.startPrank(questCreator);
questFactory.cancelQuest(QUEST.QUEST_ID_STRING);

Quest quest = Quest(payable(questAddress));
assertEq(quest.paused(), true, "quest should be paused");
assertEq(quest.endTime(), block.timestamp + 15 minutes, "endTime should be 15 minutes from now");
}

function test_cancelQuest_unauthorized() public {
vm.startPrank(questCreator);
sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(QUEST.TOTAL_PARTICIPANTS, QUEST.REWARD_AMOUNT, QUEST_FEE));
address questAddress = questFactory.createERC20Quest(
QUEST.CHAIN_ID,
address(sampleERC20),
QUEST.END_TIME,
QUEST.START_TIME,
QUEST.TOTAL_PARTICIPANTS,
QUEST.REWARD_AMOUNT,
QUEST.QUEST_ID_STRING,
QUEST.ACTION_TYPE,
QUEST.QUEST_NAME,
QUEST.PROJECT_NAME,
QUEST.REFERRAL_REWARD_FEE
);

vm.startPrank(anyone);
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
questFactory.cancelQuest(QUEST.QUEST_ID_STRING);
}

/*//////////////////////////////////////////////////////////////
VIEW
//////////////////////////////////////////////////////////////*/
Expand Down

0 comments on commit 01e85fd

Please sign in to comment.