diff --git a/contracts/QuestBudget.sol b/contracts/QuestBudget.sol index 0720b7ec..ec60e62b 100644 --- a/contracts/QuestBudget.sol +++ b/contracts/QuestBudget.sol @@ -6,6 +6,8 @@ import {IQuestFactory} from "contracts/interfaces/IQuestFactory.sol"; import {IERC1155Receiver} from "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol"; import {IERC1155} from "openzeppelin-contracts/token/ERC1155/IERC1155.sol"; import {IERC165} from "openzeppelin-contracts/utils/introspection/IERC165.sol"; +import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol"; @@ -18,6 +20,7 @@ import {Cloneable} from "contracts/references/Cloneable.sol"; /// @dev This type of budget supports ETH, ERC20, and ERC1155 assets only contract QuestBudget is Budget, IERC1155Receiver, ReentrancyGuard { using SafeTransferLib for address; + using SafeERC20 for IERC20; /// @notice The payload for initializing a SimpleBudget struct InitPayload { @@ -40,6 +43,21 @@ contract QuestBudget is Budget, IERC1155Receiver, ReentrancyGuard { /// @dev The mapping of authorized addresses mapping(address => bool) private _isAuthorized; + /// @dev The management fee percentage (in basis points, i.e., 100 = 1%) + uint256 public managementFee; + + /// @dev Mapping of quest IDs to their respective managers' addresses + mapping(string => address) public questManagers; + + /// @dev Total amount of funds reserved for management fees + uint256 public reservedFunds; + + /// @dev Emitted when the management fee is set or updated + event ManagementFeeSet(uint256 newFee); + + /// @dev Emitted when management fee is paid + event ManagementFeePaid(string indexed questId, address indexed manager, uint256 amount); + /// @notice A modifier that allows only authorized addresses to call the function modifier onlyAuthorized() { if (!isAuthorized(msg.sender)) revert Unauthorized(); @@ -165,8 +183,24 @@ contract QuestBudget is Budget, IERC1155Receiver, ReentrancyGuard { uint256 referralRewardFee = uint256(IQuestFactory(questFactory).referralRewardFee()); uint256 maxProtocolReward = (maxTotalRewards * questFee) / 10_000; uint256 maxReferralReward = (maxTotalRewards * referralRewardFee) / 10_000; + uint256 maxManagementFee = (maxTotalRewards * managementFee) / 10_000; uint256 approvalAmount = maxTotalRewards + maxProtocolReward + maxReferralReward; + + // Ensure the available balance in the budget can cover the required approval amount plus the reserved management fee + require( + this.available(rewardTokenAddress_) >= approvalAmount + maxManagementFee, + "Insufficient funds for quest creation" + ); + + // Reserve the management fee so that the manager can be paid later + reservedFunds += maxManagementFee; + + // Approve the QuestFactory contract to transfer the necessary tokens for this quest rewardTokenAddress_.safeApprove(address(questFactory), approvalAmount); + + // Store the manager address (msg.sender) associated with the questId + questManagers[questId_] = msg.sender; + return IQuestFactory(questFactory).createERC20Quest( txHashChainId_, rewardTokenAddress_, @@ -187,6 +221,55 @@ contract QuestBudget is Budget, IERC1155Receiver, ReentrancyGuard { function cancelQuest(string calldata questId_) public virtual onlyOwner() { IQuestFactory(questFactory).cancelQuest(questId_); } + + /// @notice Sets the management fee percentage + /// @dev Only the owner can call this function. The fee is in basis points (100 = 1%) + /// @param fee_ The new management fee percentage in basis points + function setManagementFee(uint256 fee_) external onlyOwner { + require(fee_ <= 10000, "Fee cannot exceed 100%"); + managementFee = fee_; + emit ManagementFeeSet(fee_); + } + + /// @notice Allows the quest manager to claim the management fee for a completed quest + /// @dev This function can only be called by the authorized quest manager after the quest rewards have been withdrawn + /// @param questId_ The unique identifier of the quest for which the management fee is being claimed + function payManagementFee(string memory questId_) public onlyAuthorized { + // Retrieve the quest data by calling the questData function and decoding the result + IQuestFactory.QuestData memory quest = IQuestFactory(questFactory).questData(questId_); + + // Ensure the caller is the manager who created the quest + require(questManagers[questId_] == msg.sender, "Only the quest creator can claim the management fee"); + + // Ensure the quest has been marked as withdrawn + require(quest.hasWithdrawn, "Management fee cannot be claimed until the quest rewards are withdrawn"); + + // Extract relevant data from the QuestData struct + uint256 totalParticipants = quest.totalParticipants; + uint256 rewardAmount = quest.rewardAmountOrTokenId; + uint256 numberMinted = quest.numberMinted; + + // Calculate the maximum possible management fee based on total participants + uint256 totalPossibleFee = (totalParticipants * rewardAmount * managementFee) / 10_000; + + // Calculate the actual management fee to be paid based on the number of claims (numberMinted) + uint256 feeToPay = (numberMinted * rewardAmount * managementFee) / 10_000; + + // Get the balance of reward tokens available in this contract + uint256 availableFunds = IERC20(quest.rewardToken).balanceOf(address(this)); + + // Ensure the contract has enough funds to pay out the management fee; they should be reserved and not available + require(availableFunds >= feeToPay, "Insufficient funds to pay management fee"); + + // Transfer the management fee to the manager + IERC20(quest.rewardToken).safeTransfer(msg.sender, feeToPay); + + // Subtract the total possible management fee since we reserved the total amount to begin with + reservedFunds = reservedFunds - totalPossibleFee; + + // Emit an event for logging purposes + emit ManagementFeePaid(questId_, msg.sender, feeToPay); + } /// @inheritdoc Budget /// @notice Disburses assets from the budget to a single recipient @@ -286,10 +369,11 @@ contract QuestBudget is Budget, IERC1155Receiver, ReentrancyGuard { /// @notice Get the amount of assets available for distribution from the budget /// @param asset_ The address of the asset (or the zero address for native assets) /// @return The amount of assets available - /// @dev This is simply the current balance held by the budget + /// @dev This returns the current balance held by the budget minus reserved funds /// @dev If the zero address is passed, this function will return the native balance function available(address asset_) public view virtual override returns (uint256) { - return asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this)); + uint256 totalBalance = asset_ == address(0) ? address(this).balance : IERC20(asset_).balanceOf(address(this)); + return totalBalance > reservedFunds ? totalBalance - reservedFunds : 0; } /// @notice Get the amount of ERC1155 assets available for distribution from the budget diff --git a/test/QuestBudget.t.sol b/test/QuestBudget.t.sol index 71589ea0..edf2e9f3 100644 --- a/test/QuestBudget.t.sol +++ b/test/QuestBudget.t.sol @@ -25,7 +25,25 @@ contract QuestBudgetTest is Test, TestUtils, IERC1155Receiver { QuestFactoryMock mockQuestFactory; QuestBudget questBudget; + struct QuestSetupData { + uint32 txHashChainId; + address rewardTokenAddress; + uint256 endTime; + uint256 startTime; + uint256 totalParticipants; + uint256 rewardAmount; + string questId; + string actionType; + string questName; + string projectName; + uint256 referralRewardFee; + uint256 numberMinted; + bool hasWithdrawn; + } + event QuestCancelled(address indexed questAddress, string questId, uint256 endsAt); + event ManagementFeePaid(string indexed questId, address indexed manager, uint256 amount); + event ManagementFeeSet(uint256 newFee); function setUp() public { address owner = address(this); @@ -89,6 +107,11 @@ contract QuestBudgetTest is Test, TestUtils, IERC1155Receiver { assertEq(questBudget.available(address(mockERC1155), 42), 0); } + function test_InitialManagementFee() public { + // Ensure the management fee is 0 + assertEq(questBudget.managementFee(), 0); + } + function test_InitializerDisabled() public { // Because the slot is private, we use `vm.load` to access it then parse out the bits: // - [0] is the `initializing` flag (which should be 0 == false) @@ -368,30 +391,21 @@ contract QuestBudgetTest is Test, TestUtils, IERC1155Receiver { questBudget.reclaim(data); } - /////////////////////////// + ////////////////////////////////// // QuestBudget.createERC20Quest // - /////////////////////////// + ////////////////////////////////// function testCreateERC20Quest() public { - // Define the parameters for the new quest - uint32 txHashChainId_ = 1; - address rewardTokenAddress_ = address(mockERC20); - uint256 endTime_ = block.timestamp + 1 days; - uint256 startTime_ = block.timestamp; - uint256 totalParticipants_ = 10; - uint256 rewardAmount_ = 1 ether; - string memory questId_ = "testQuest"; - string memory actionType_ = "testAction"; - string memory questName_ = "Test Quest"; - string memory projectName_ = "Test Project"; - uint256 referralRewardFee_ = 250; + // Create quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); - uint256 maxTotalRewards = totalParticipants_ * rewardAmount_; + uint256 maxTotalRewards = data.totalParticipants * data.rewardAmount; uint256 questFee = uint256(mockQuestFactory.questFee()); uint256 referralRewardFee = uint256(mockQuestFactory.referralRewardFee()); uint256 maxProtocolReward = (maxTotalRewards * questFee) / 10_000; uint256 maxReferralReward = (maxTotalRewards * referralRewardFee) / 10_000; uint256 approvalAmount = maxTotalRewards + maxProtocolReward + maxReferralReward; mockERC20.mint(address(this), approvalAmount); + // Ensure the budget has enough tokens for the reward mockERC20.approve(address(questBudget), approvalAmount); questBudget.allocate( @@ -400,77 +414,144 @@ contract QuestBudgetTest is Test, TestUtils, IERC1155Receiver { // Create the new quest address questAddress = questBudget.createERC20Quest( - txHashChainId_, - rewardTokenAddress_, - endTime_, - startTime_, - totalParticipants_, - rewardAmount_, - questId_, - actionType_, - questName_, - projectName_, - referralRewardFee_ + data.txHashChainId, + data.rewardTokenAddress, + data.endTime, + data.startTime, + data.totalParticipants, + data.rewardAmount, + data.questId, + data.actionType, + data.questName, + data.projectName, + data.referralRewardFee ); // Ensure the returned quest address is not the zero address assertTrue(questAddress != address(0)); // Ensure the quest contract has the correct reward amount - assertEq(IERC20(rewardTokenAddress_).balanceOf(questAddress), approvalAmount); + assertEq(IERC20(data.rewardTokenAddress).balanceOf(questAddress), approvalAmount); } - /////////////////////////// - // QuestBudget.cancel // - /////////////////////////// + function testCreateERC20Quest_WithManagementFee() public { + // Set management fee + vm.prank(questBudget.owner()); + questBudget.setManagementFee(500); // 5% - function test_cancel() public { - // Define the parameters for the new quest - uint32 txHashChainId_ = 1; - address rewardTokenAddress_ = address(mockERC20); - uint256 endTime_ = block.timestamp + 1 days; - uint256 startTime_ = block.timestamp; - uint256 totalParticipants_ = 10; - uint256 rewardAmount_ = 1 ether; - string memory questId_ = "testQuest"; - string memory actionType_ = "testAction"; - string memory questName_ = "Test Quest"; - string memory projectName_ = "Test Project"; - uint256 referralRewardFee_ = 250; - - uint256 maxTotalRewards = totalParticipants_ * rewardAmount_; + // Create quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + + uint256 maxTotalRewards = data.totalParticipants * data.rewardAmount; uint256 questFee = uint256(mockQuestFactory.questFee()); uint256 referralRewardFee = uint256(mockQuestFactory.referralRewardFee()); uint256 maxProtocolReward = (maxTotalRewards * questFee) / 10_000; uint256 maxReferralReward = (maxTotalRewards * referralRewardFee) / 10_000; - uint256 approvalAmount = maxTotalRewards + maxProtocolReward + maxReferralReward; - mockERC20.mint(address(this), approvalAmount); - // Ensure the budget has enough tokens for the reward - mockERC20.approve(address(questBudget), approvalAmount); - bytes memory allocateBytes = _makeFungibleTransfer(Budget.AssetType.ERC20, address(mockERC20), address(this), approvalAmount); - questBudget.allocate(allocateBytes); - console.logBytes(allocateBytes); + uint256 questFactoryApprovalAmount = maxTotalRewards + maxProtocolReward + maxReferralReward; - // Create the new quest + // Calculate the amounts needed for the quest + uint256 maxManagementFee = (maxTotalRewards * questBudget.managementFee()) / 10_000; + uint256 totalAllocationRequired = questFactoryApprovalAmount + maxManagementFee; + + // Approve questBudget to spend tokens + mockERC20.approve(address(questBudget), totalAllocationRequired); + + // Allocate tokens to questBudget + questBudget.allocate( + _makeFungibleTransfer(Budget.AssetType.ERC20, address(mockERC20), address(this), totalAllocationRequired) + ); + + // Create quest + string memory questId = "testQuest"; address questAddress = questBudget.createERC20Quest( - txHashChainId_, - rewardTokenAddress_, - endTime_, - startTime_, - totalParticipants_, - rewardAmount_, - questId_, - actionType_, - questName_, - projectName_, - referralRewardFee_ + data.txHashChainId, + data.rewardTokenAddress, + data.endTime, + data.startTime, + data.totalParticipants, + data.rewardAmount, + data.questId, + data.actionType, + data.questName, + data.projectName, + data.referralRewardFee ); // Ensure the returned quest address is not the zero address assertTrue(questAddress != address(0)); - // Ensure the quest contract has the correct reward amount - assertEq(IERC20(rewardTokenAddress_).balanceOf(questAddress), approvalAmount); + // Assert that the quest manager is set to the questBudget owner + assertEq(questBudget.questManagers(questId), address(questBudget.owner())); + + // Assert that the reserved funds is equal to the management fee + assertEq(questBudget.reservedFunds(), maxManagementFee); + + // Calculate the expected available balance + uint256 expectedAvailable = totalAllocationRequired - questFactoryApprovalAmount - maxManagementFee; + + // Assert that the available balance is 0 + assertEq(expectedAvailable, 0); + + // Assert that the available balance is equal to the expected available balance + assertEq(questBudget.available(address(mockERC20)), expectedAvailable); + } + + function testCreateERC20Quest_InsufficientFunds() public { + // Set management fee + vm.prank(questBudget.owner()); + questBudget.setManagementFee(500); // 5% + + // Setup quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + + uint256 maxTotalRewards = data.totalParticipants * data.rewardAmount; + uint256 questFee = uint256(mockQuestFactory.questFee()); + uint256 referralRewardFee = uint256(mockQuestFactory.referralRewardFee()); + uint256 maxProtocolReward = (maxTotalRewards * questFee) / 10_000; + uint256 maxReferralReward = (maxTotalRewards * referralRewardFee) / 10_000; + uint256 questFactoryApprovalAmount = maxTotalRewards + maxProtocolReward + maxReferralReward; + + uint256 maxManagementFee = (maxTotalRewards * questBudget.managementFee()) / 10_000; + uint256 totalAllocationRequired = questFactoryApprovalAmount + maxManagementFee; + + // Approve questBudget to spend tokens + mockERC20.approve(address(questBudget), totalAllocationRequired); + + // Allocate the needed amount minus the management fee + questBudget.allocate( + _makeFungibleTransfer(Budget.AssetType.ERC20, address(mockERC20), address(this), totalAllocationRequired - maxManagementFee) + ); + + vm.expectRevert("Insufficient funds for quest creation"); + questBudget.createERC20Quest( + data.txHashChainId, + data.rewardTokenAddress, + data.endTime, + data.startTime, + data.totalParticipants, + data.rewardAmount, + data.questId, + data.actionType, + data.questName, + data.projectName, + data.referralRewardFee + ); + } + + //////////////////////// + // QuestBudget.cancel // + //////////////////////// + + function test_cancel() public { + // Create quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + address questAddress = _createQuestWithMockData(data, questBudget.owner()); + + // Ensure the returned quest address is not the zero address + assertTrue(questAddress != address(0)); + + // Ensure the quest contract has a positive balance + assertGt(IERC20(data.rewardTokenAddress).balanceOf(questAddress), 0); vm.expectEmit(); @@ -788,6 +869,27 @@ contract QuestBudgetTest is Test, TestUtils, IERC1155Receiver { assertEq(questBudget.available(address(otherMockERC20)), 0); } + function testAvailable_ReservedFunds() public { + // Mint some tokens to the questBudget + uint256 totalBalance = 100 ether; + mockERC20.mint(address(questBudget), totalBalance); + + // Set reserved funds equal to total balance + uint256 reservedFunds = totalBalance; + bytes32 reservedFundsSlot = bytes32(uint256(6)); // reservedFunds is at slot 6 + vm.store(address(questBudget), reservedFundsSlot, bytes32(reservedFunds)); + + uint256 availableBalance = questBudget.available(address(mockERC20)); + assertEq(availableBalance, 0, "Available balance should be 0 when reserved funds equal total balance"); + + // Set reserved funds greater than total balance + reservedFunds = totalBalance + 1 wei; + vm.store(address(questBudget), reservedFundsSlot, bytes32(reservedFunds)); + + availableBalance = questBudget.available(address(mockERC20)); + assertEq(availableBalance, 0, "Available balance should be 0 when reserved funds are > total balance"); + } + ////////////////////////////// // QuestBudget.distributed // ////////////////////////////// @@ -954,10 +1056,248 @@ contract QuestBudgetTest is Test, TestUtils, IERC1155Receiver { assertEq(questBudget.available(address(0)), 1 ether); } + ////////////////////////////////// + // QuestBudget.setManagementFee // + ////////////////////////////////// + + function testSetManagementFee() public { + // Simulate a transaction from the owner of the questBudget contract + vm.prank(questBudget.owner()); + + // Expect the ManagementFeeSet event to be emitted + vm.expectEmit(); + emit ManagementFeeSet(500); + + // Call the setManagementFee function with a value of 5% + questBudget.setManagementFee(500); + + // Assert that the managementFee has been correctly set to 5% + assertEq(questBudget.managementFee(), 500); + } + + function testSetManagementFee_ExceedsMax() public { + // Simulate a transaction from the owner of the questBudget contract + vm.prank(questBudget.owner()); + + // Set an initial valid management fee (5%) + questBudget.setManagementFee(500); + + // Attempt to set a management fee that exceeds 100% + vm.expectRevert("Fee cannot exceed 100%"); + questBudget.setManagementFee(10001); + + // Assert that the management fee remains unchanged at 5% + assertEq(questBudget.managementFee(), 500); + } + + ////////////////////////////////// + // QuestBudget.payManagementFee // + ////////////////////////////////// + + function testPayManagementFee() public { + // Set management fee + vm.prank(questBudget.owner()); + questBudget.setManagementFee(500); // 5% + + // Setup quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + + // Simulate that the quest has already withdrawn + data.hasWithdrawn = true; + _createQuestWithMockData(data, questBudget.owner()); + + // Get balance after the quest has withdrawn + uint256 initialBalance = mockERC20.balanceOf(address(this)); + + // Calculate expected fee + uint256 expectedFeeToPay = (data.numberMinted * data.rewardAmount * questBudget.managementFee()) / 10_000; + + // Expect the ManagementFeePaid event to be emitted + vm.expectEmit(); + emit ManagementFeePaid(data.questId, address(this), expectedFeeToPay); + questBudget.payManagementFee(data.questId); + + // Get balance after the management fee is paid + uint256 finalBalance = mockERC20.balanceOf(address(this)); + + // Verify the correct amount was transferred + assertEq(finalBalance - initialBalance, expectedFeeToPay, "Incorrect management fee paid"); + } + + function testPayManagementFee_NotWithdrawn() public { + // Set management fee + vm.prank(questBudget.owner()); + questBudget.setManagementFee(500); // 5% + + // Create quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + _createQuestWithMockData(data, questBudget.owner()); + + vm.expectRevert("Management fee cannot be claimed until the quest rewards are withdrawn"); + questBudget.payManagementFee(data.questId); + } + + function testPayManagementFee_NotQuestCreator() public { + // Set management fee + vm.prank(questBudget.owner()); + questBudget.setManagementFee(500); // 5% + + // Authorize a different address to create quests + address questCreator = address(0xc0ffee); + address[] memory accounts = new address[](1); + bool[] memory authorized = new bool[](1); + accounts[0] = questCreator; + authorized[0] = true; + questBudget.setAuthorized(accounts, authorized); + + // Create quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + _createQuestWithMockData(data, questCreator); + + // Attempt to call payManagementFee as budget owner (not quest creator) + vm.prank(questBudget.owner()); + vm.expectRevert("Only the quest creator can claim the management fee"); + questBudget.payManagementFee(data.questId); + } + + function testPayManagementFee_InsufficientFunds() public { + // Set management fee + vm.prank(questBudget.owner()); + questBudget.setManagementFee(500); // 5% + + // Setup quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + + // Simulate that the quest has already withdrawn + data.hasWithdrawn = true; + _createQuestWithMockData(data, questBudget.owner()); + + // Transfer 1 wei out of the budget + vm.prank(address(questBudget)); + mockERC20.transfer(address(this), 1); + + // Attempt to pay management fee with insufficient funds + vm.expectRevert("Insufficient funds to pay management fee"); + questBudget.payManagementFee(data.questId); + } + + function testPayManagementFee_PartialParticipants() public { + // Set management fee + uint256 managementFeePercentage = 500; // 5% + vm.prank(questBudget.owner()); + questBudget.setManagementFee(managementFeePercentage); + + // Setup quest with standard parameters (60 participants, 1 ETH reward per participant) + QuestSetupData memory data = setupQuestData(); + + // Simulate that the quest has already withdrawn + data.hasWithdrawn = true; + + // Set number minted to 10 (instead of 60) + data.numberMinted = 10; + + // Create quest + _createQuestWithMockData(data, questBudget.owner()); + + // Calculate expected fee + uint256 expectedFeeToPay = (data.numberMinted * data.rewardAmount * managementFeePercentage) / 10_000; + + // Get initial balance of the quest creator + uint256 initialBalance = mockERC20.balanceOf(address(this)); + + // Pay management fee + questBudget.payManagementFee(data.questId); + + // Get final balance of the quest creator + uint256 finalBalance = mockERC20.balanceOf(address(this)); + + // Verify the correct amount was transferred + assertEq(finalBalance - initialBalance, expectedFeeToPay, "Incorrect management fee paid"); + } + + function testPayManagementFee_NotAuthorized() public { + vm.prank(address(0xc0ffee)); + + vm.expectRevert(BoostError.Unauthorized.selector); + questBudget.payManagementFee("testQuest"); + } + /////////////////////////// // Test Helper Functions // /////////////////////////// + /// @notice Sets up a default QuestSetupData struct with common test parameters + /// @dev This function provides a baseline set of quest data that can be easily modified for different test scenarios + /// @return A QuestSetupData struct populated with default values for testing purposes + function setupQuestData() internal view returns (QuestSetupData memory) { + return QuestSetupData({ + txHashChainId: 1, + rewardTokenAddress: address(mockERC20), + endTime: block.timestamp + 1 days, + startTime: block.timestamp, + totalParticipants: 60, + rewardAmount: 1 ether, + questId: "testQuest", + actionType: "testAction", + questName: "Test Quest", + projectName: "Test Project", + referralRewardFee: 250, + numberMinted: 60, + hasWithdrawn: false + }); + } + + /// @notice Creates a quest with mock data for testing purposes + /// @dev This function simulates the entire quest creation process, including token allocation and mocking quest data + /// @param data The struct containing all necessary quest setup data + /// @param questCreator The address that will be set as the quest creator + /// @return questAddress The address of the newly created quest contract + function _createQuestWithMockData(QuestSetupData memory data, address questCreator) internal returns (address questAddress) { + uint256 maxTotalRewards = data.totalParticipants * data.rewardAmount; + uint256 questFee = uint256(mockQuestFactory.questFee()); + uint256 referralRewardFee = uint256(mockQuestFactory.referralRewardFee()); + uint256 maxProtocolReward = (maxTotalRewards * questFee) / 10_000; + uint256 maxReferralReward = (maxTotalRewards * referralRewardFee) / 10_000; + uint256 maxManagementFee = (maxTotalRewards * questBudget.managementFee()) / 10_000; + uint256 requiredApprovalAmount = maxTotalRewards + maxProtocolReward + maxReferralReward + maxManagementFee; + + // Allocate tokens to questBudget + mockERC20.mint(address(questBudget), requiredApprovalAmount); + + // Create quest + vm.prank(questCreator); + questAddress = questBudget.createERC20Quest( + data.txHashChainId, + data.rewardTokenAddress, + data.endTime, + data.startTime, + data.totalParticipants, + data.rewardAmount, + data.questId, + data.actionType, + data.questName, + data.projectName, + data.referralRewardFee + ); + + // Mock quest data + mockQuestFactory.setQuestData(data.questId, IQuestFactory.QuestData({ + questAddress: questAddress, + rewardToken: data.rewardTokenAddress, + queued: false, + questFee: 250, // 2.5% + startTime: data.startTime, + endTime: data.endTime, + totalParticipants: data.totalParticipants, + numberMinted: data.numberMinted, + redeemedTokens: data.numberMinted * data.rewardAmount, + rewardAmountOrTokenId: data.rewardAmount, + hasWithdrawn: data.hasWithdrawn + })); + + return questAddress; + } + function _makeFungibleTransfer(Budget.AssetType assetType, address asset, address target, uint256 value) internal pure diff --git a/test/mocks/QuestFactoryMock.sol b/test/mocks/QuestFactoryMock.sol index fca39259..97dbabb5 100644 --- a/test/mocks/QuestFactoryMock.sol +++ b/test/mocks/QuestFactoryMock.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.19; import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {IQuestFactory} from "../../contracts/interfaces/IQuestFactory.sol"; + contract QuestFactoryMock { uint256 numberMinted; uint256 public mintFee; @@ -22,7 +24,9 @@ contract QuestFactoryMock { uint256 referralRewardFee; } - QuestData public questData; + QuestData public _questData; + + mapping(string => IQuestFactory.QuestData) public questDataMap; event MintFeePaid( string questId, @@ -82,7 +86,7 @@ contract QuestFactoryMock { uint256 maxProtocolReward = (maxTotalRewards * this.questFee()) / 10_000; uint256 maxReferralReward = (maxTotalRewards * this.referralRewardFee()) / 10_000; uint256 approvalAmount = maxTotalRewards + maxProtocolReward + maxReferralReward; - questData = QuestData({ + _questData = QuestData({ txHashChainId: txHashChainId_, rewardTokenAddress: rewardTokenAddress_, endTime: endTime_, @@ -108,4 +112,12 @@ contract QuestFactoryMock { emit QuestCancelled(address(this), questId_, 0); } -} + // test helper function to set mock quest data + function setQuestData(string memory questId, IQuestFactory.QuestData memory data) public { + questDataMap[questId] = data; + } + + function questData(string memory questId) public view returns (IQuestFactory.QuestData memory) { + return questDataMap[questId]; + } +} \ No newline at end of file