Skip to content

Commit

Permalink
feat: add claimCompressedRefBatch function
Browse files Browse the repository at this point in the history
  • Loading branch information
veganbeef committed Jul 9, 2024
1 parent 0fdf2f5 commit a7ff0c4
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 0 deletions.
24 changes: 24 additions & 0 deletions contracts/QuestFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,30 @@ contract QuestFactory is Initializable, LegacyStorage, OwnableRoles, IQuestFacto
_claimCompressed(compressedData_, claimer);
}

/// @dev Claim rewards for multiple quests on behalf of multiple claimers
function claimCompressedRefBatch(BatchClaimData[] calldata batchClaimDataArray) external payable {
uint256 totalFees = 0;
for (uint256 i = 0; i < batchClaimDataArray.length; i++) {
totalFees += batchClaimDataArray[i].fee;
}
require(msg.value >= totalFees, "Insufficient ETH sent");

for (uint256 i = 0; i < batchClaimDataArray.length; i++) {
try this.claimCompressedRef{value: batchClaimDataArray[i].fee}(
batchClaimDataArray[i].compressedData,
batchClaimDataArray[i].claimer
) {} catch (bytes memory reason) {
emit BatchClaimFailed(batchClaimDataArray[i].claimer, batchClaimDataArray[i].compressedData, reason);
}
}

// Refund any excess ETH
uint256 excess = msg.value - totalFees;
if (excess > 0) {
payable(msg.sender).transfer(excess);
}
}

/// @dev Claim rewards for a quest
/// @param compressedData_ The claim data in abi encoded bytes, compressed with cdCompress from solady LibZip
/// @param claimer The address of the claimer - where rewards are sent
Expand Down
10 changes: 10 additions & 0 deletions contracts/interfaces/IQuestFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ interface IQuestFactory {
string extraData;
}

/// @dev struct to allow for an array of claims in batch claim functions
struct BatchClaimData {
bytes compressedData;
address claimer;
uint256 fee;
}

struct ERC20QuestData {
uint32 txHashChainId;
address rewardTokenAddress;
Expand Down Expand Up @@ -179,6 +186,9 @@ interface IQuestFactory {
);
event ReferralFeeSet(uint16 percent);

/// @dev event to track failed claims in batch claim functions
event BatchClaimFailed(address indexed claimer, bytes compressedData, bytes reason);

// Read Functions
function getAddressMinted(string memory questId_, address address_) external view returns (bool);
function getNumberMinted(string memory questId_) external view returns (uint256);
Expand Down
67 changes: 67 additions & 0 deletions test/QuestFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,73 @@ contract TestQuestFactory is Test, Errors, Events, TestUtils {
assertEq(Quest(payable(questAddress)).getReferralAmount(referrer), Quest(payable(questAddress)).referralRewardAmount());
}

function test_claimCompressedRefBatch_erc20_mocked_data() public {
// Setup for two participants
address participant1 = address(0x1);
address participant2 = address(0x2);
address referrer1 = address(0x3);
address referrer2 = address(0x4);

// Generate signature data for both participants
bytes memory signData1 = abi.encode(participant1, referrer1, QUEST.QUEST_ID_STRING, QUEST.JSON_MSG);
bytes memory signData2 = abi.encode(participant2, referrer2, QUEST.QUEST_ID_STRING, QUEST.JSON_MSG);

bytes32 msgHash1 = keccak256(signData1);
bytes32 msgHash2 = keccak256(signData2);
bytes32 digest1 = ECDSA.toEthSignedMessageHash(msgHash1);
bytes32 digest2 = ECDSA.toEthSignedMessageHash(msgHash2);
(, bytes32 r1, bytes32 vs1) = TestUtils.getSplitSignature(claimSignerPrivateKey, digest1);
(, bytes32 r2, bytes32 vs2) = TestUtils.getSplitSignature(claimSignerPrivateKey, digest2);

vm.deal(participant1, 1000000);
vm.deal(participant2, 1000000);

vm.startPrank(questCreator);
sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(QUEST.TOTAL_PARTICIPANTS, QUEST.REWARD_AMOUNT, QUEST_FEE, REFERRAL_FEE));
address questAddress = questFactory.createERC20Boost(
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
);

vm.warp(QUEST.START_TIME + 1);

bytes memory data1 = abi.encode(QUEST.TX_HASH, r1, vs1, referrer1, QUEST.QUEST_ID, QUEST.CHAIN_ID);
bytes memory data2 = abi.encode(QUEST.TX_HASH, r2, vs2, referrer2, QUEST.QUEST_ID, QUEST.CHAIN_ID);
bytes memory dataCompressed1 = LibZip.cdCompress(data1);
bytes memory dataCompressed2 = LibZip.cdCompress(data2);

IQuestFactory.BatchClaimData[] memory claimDataArray = new IQuestFactory.BatchClaimData[](2);
claimDataArray[0] = IQuestFactory.BatchClaimData({
compressedData: dataCompressed1,
claimer: participant1,
fee: MINT_FEE
});
claimDataArray[1] = IQuestFactory.BatchClaimData({
compressedData: dataCompressed2,
claimer: participant2,
fee: MINT_FEE
});

vm.startPrank(anyone, anyone);
questFactory.claimCompressedRefBatch{value: MINT_FEE * 2}(claimDataArray);

// Check ERC20 rewards
assertEq(sampleERC20.balanceOf(participant1), QUEST.REWARD_AMOUNT, "participant1 erc20 balance");
assertEq(sampleERC20.balanceOf(participant2), QUEST.REWARD_AMOUNT, "participant2 erc20 balance");

// Check referrer claimable amounts
assertEq(Quest(payable(questAddress)).getReferralAmount(referrer1), Quest(payable(questAddress)).referralRewardAmount(), "referrer1 claimable amount");
assertEq(Quest(payable(questAddress)).getReferralAmount(referrer2), Quest(payable(questAddress)).referralRewardAmount(), "referrer2 claimable amount");
}

function test_claimCompressed_erc20_with_ref() public{
vm.startPrank(questCreator);
sampleERC20.approve(address(questFactory), calculateTotalRewardsPlusFee(QUEST.TOTAL_PARTICIPANTS, QUEST.REWARD_AMOUNT, QUEST_FEE, REFERRAL_FEE));
Expand Down

0 comments on commit a7ff0c4

Please sign in to comment.