From 778118532f566fef0027c8f24fd8d061f7ccff65 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Wed, 21 Aug 2024 15:41:46 -0300 Subject: [PATCH 1/7] feat: Instant rewards smart contract --- .../instant-rewards/InstantRewards.sol | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol diff --git a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol new file mode 100644 index 0000000..3d19010 --- /dev/null +++ b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.7; + +import "@openzeppelin/contracts/security/Pausable.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +contract InstantRewards is Ownable, Pausable, ReentrancyGuard { + /* An ECDSA signature. */ + struct Signature { + uint8 v; + bytes32 r; + bytes32 s; + } + + struct ClaimData { + address to; + Signature signature; + bytes32 taskId; + uint256 amount; + } + + mapping(address => mapping(bytes32 => bool)) public taskCompletedByUser; + + address public signerAddress; + + event Claimed(address indexed to, bytes32 indexed taskId, uint256 amount); + + error InvalidSigner(); + error InvalidAddress(); + error TaskAlreadyClaimed(); + + constructor(address signerAddress_, address owner) Ownable() { + if (signerAddress_ == address(0)) revert InvalidAddress(); + transferOwnership(owner); + signerAddress = signerAddress_; + } + + // Helper function to convert uint to string + function _uint2str(uint _i) internal pure returns (string memory _uintAsString) { + if (_i == 0) { + return "0"; + } + uint j = _i; + uint len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint k = len; + while (_i != 0) { + k = k - 1; + uint8 temp = (uint8(48 + (_i % 10))); + bstr[k] = bytes1(temp); + _i /= 10; + } + return string(bstr); + } + + function _verify(ClaimData memory claimData) private view { + bytes32 payloadHash = _calculateHash(claimData); + + bytes32 messageHash = ECDSA.toEthSignedMessageHash(payloadHash); + + address messageSigner = ECDSA.recover( + messageHash, + claimData.signature.v, + claimData.signature.r, + claimData.signature.s + ); + + if (signerAddress != messageSigner) revert InvalidSigner(); + } + + // Function to compute the hash of the data and tasks for a token + function _calculateHash(ClaimData memory claimData) private pure returns (bytes32) { + bytes memory encodedData = abi.encode(claimData.to, claimData.taskId, claimData.amount); + + return keccak256(encodedData); + } + + function claim(ClaimData memory claimData) external whenNotPaused nonReentrant { + claimData.to = msg.sender; + _verify(claimData); + + if (taskCompletedByUser[claimData.to][claimData.taskId]) revert TaskAlreadyClaimed(); + + taskCompletedByUser[claimData.to][claimData.taskId] = true; + + payable(claimData.to).transfer(claimData.amount); + + emit Claimed(claimData.to, claimData.taskId, claimData.amount); + } + + function setSignerAddress(address signerAddress_) external onlyOwner { + if (signerAddress_ == address(0)) revert InvalidAddress(); + signerAddress = signerAddress_; + } + + function withdraw(address wallet) external onlyOwner { + payable(wallet).transfer(address(this).balance); + } + + receive() external payable {} +} From b1539571abdc8a0ffc2affb542c1ee5a65babee2 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Wed, 21 Aug 2024 15:55:00 -0300 Subject: [PATCH 2/7] refactor --- .../instant-rewards/InstantRewards.sol | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol index 3d19010..8bc5e81 100644 --- a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol +++ b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol @@ -30,6 +30,7 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { error InvalidSigner(); error InvalidAddress(); error TaskAlreadyClaimed(); + error TransferFailed(); constructor(address signerAddress_, address owner) Ownable() { if (signerAddress_ == address(0)) revert InvalidAddress(); @@ -37,28 +38,6 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { signerAddress = signerAddress_; } - // Helper function to convert uint to string - function _uint2str(uint _i) internal pure returns (string memory _uintAsString) { - if (_i == 0) { - return "0"; - } - uint j = _i; - uint len; - while (j != 0) { - len++; - j /= 10; - } - bytes memory bstr = new bytes(len); - uint k = len; - while (_i != 0) { - k = k - 1; - uint8 temp = (uint8(48 + (_i % 10))); - bstr[k] = bytes1(temp); - _i /= 10; - } - return string(bstr); - } - function _verify(ClaimData memory claimData) private view { bytes32 payloadHash = _calculateHash(claimData); @@ -89,7 +68,8 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { taskCompletedByUser[claimData.to][claimData.taskId] = true; - payable(claimData.to).transfer(claimData.amount); + (bool success, ) = claimData.to.call{value: claimData.amount}(""); + if (!success) revert TransferFailed(); emit Claimed(claimData.to, claimData.taskId, claimData.amount); } @@ -100,6 +80,7 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { } function withdraw(address wallet) external onlyOwner { + if (wallet == address(0)) revert InvalidAddress(); payable(wallet).transfer(address(this).balance); } From 14c0b78c5065d53170ae985fe599952e0fa72fb7 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Wed, 21 Aug 2024 16:03:38 -0300 Subject: [PATCH 3/7] custom amount to withdraw --- .../contracts/instant-rewards/InstantRewards.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol index 8bc5e81..6aed3c5 100644 --- a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol +++ b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol @@ -79,9 +79,10 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { signerAddress = signerAddress_; } - function withdraw(address wallet) external onlyOwner { + function withdraw(address wallet, uint256 amount) external onlyOwner { if (wallet == address(0)) revert InvalidAddress(); - payable(wallet).transfer(address(this).balance); + if (amount > address(this).balance) revert TransferFailed(); + payable(wallet).transfer(amount); } receive() external payable {} From d8f38345a1b4c517ca25e1c2d4d56d4f01e0baf2 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Thu, 22 Aug 2024 11:10:20 -0300 Subject: [PATCH 4/7] add expiration --- .../contracts/instant-rewards/InstantRewards.sol | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol index 6aed3c5..0103019 100644 --- a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol +++ b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol @@ -17,6 +17,7 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { struct ClaimData { address to; Signature signature; + uint256 sigExpiration; bytes32 taskId; uint256 amount; } @@ -28,6 +29,7 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { event Claimed(address indexed to, bytes32 indexed taskId, uint256 amount); error InvalidSigner(); + error SignatureExpired(); error InvalidAddress(); error TaskAlreadyClaimed(); error TransferFailed(); @@ -51,11 +53,17 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { ); if (signerAddress != messageSigner) revert InvalidSigner(); + if (block.timestamp > claimData.sigExpiration) revert SignatureExpired(); } // Function to compute the hash of the data and tasks for a token function _calculateHash(ClaimData memory claimData) private pure returns (bytes32) { - bytes memory encodedData = abi.encode(claimData.to, claimData.taskId, claimData.amount); + bytes memory encodedData = abi.encode( + claimData.to, + claimData.sigExpiration, + claimData.taskId, + claimData.amount + ); return keccak256(encodedData); } From 805f146c3e3686ca80a93d63879208d3bb5b0af8 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Fri, 23 Aug 2024 11:27:15 -0300 Subject: [PATCH 5/7] add test --- .../test/instant-rewards/instant-rewards.ts | 68 +++++++++++++++++++ .../test/instant-rewards/test.helpers.ts | 33 +++++++++ 2 files changed, 101 insertions(+) create mode 100644 packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts create mode 100644 packages/zevm-app-contracts/test/instant-rewards/test.helpers.ts diff --git a/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts b/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts new file mode 100644 index 0000000..8e2a992 --- /dev/null +++ b/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts @@ -0,0 +1,68 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { utils } from "ethers"; +import { ethers } from "hardhat"; + +import { InstantRewards } from "../../typechain-types"; +import { ClaimData, getSignature } from "./test.helpers"; + +describe("Instant Rewards Contract test", () => { + let instantRewards: InstantRewards, + owner: SignerWithAddress, + signer: SignerWithAddress, + user: SignerWithAddress, + addrs: SignerWithAddress[]; + + const encodeTaskId = (taskId: string) => utils.keccak256(utils.defaultAbiCoder.encode(["string"], [taskId])); + + beforeEach(async () => { + [owner, signer, user, ...addrs] = await ethers.getSigners(); + const instantRewardsFactory = await ethers.getContractFactory("InstantRewards"); + + instantRewards = await instantRewardsFactory.deploy(signer.address, owner.address); + + await instantRewards.deployed(); + }); + + it("Should claim", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp + 1000; + const amount = utils.parseEther("1"); + const taskId = encodeTaskId("1/1/1"); + const to = owner.address; + + // transfer some funds to the contract + await owner.sendTransaction({ + to: instantRewards.address, + value: amount, + }); + + const claimData: ClaimData = { + amount, + sigExpiration, + taskId, + to, + }; + + const signature = await getSignature(signer, claimData); + const claimDataSigned = { + ...claimData, + signature, + }; + + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.emit(instantRewards, "Claimed").withArgs(owner.address, taskId, amount); + }); + + it("Should transfer ownership", async () => { + { + const ownerAddr = await instantRewards.owner(); + expect(ownerAddr).to.be.eq(owner.address); + } + await instantRewards.transferOwnership(user.address); + { + const ownerAddr = await instantRewards.owner(); + expect(ownerAddr).to.be.eq(user.address); + } + }); +}); diff --git a/packages/zevm-app-contracts/test/instant-rewards/test.helpers.ts b/packages/zevm-app-contracts/test/instant-rewards/test.helpers.ts new file mode 100644 index 0000000..4eba66d --- /dev/null +++ b/packages/zevm-app-contracts/test/instant-rewards/test.helpers.ts @@ -0,0 +1,33 @@ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { BigNumber } from "ethers"; +import { ethers } from "hardhat"; + +export interface Signature { + r: string; + s: string; + v: number; +} + +export interface ClaimData { + amount: BigNumber; + sigExpiration: number; + taskId: string; + to: string; +} + +export interface ClaimDataSigned extends ClaimData { + signature: Signature; +} + +export const getSignature = async (signer: SignerWithAddress, claimData: ClaimData) => { + let payload = ethers.utils.defaultAbiCoder.encode( + ["address", "uint256", "bytes32", "uint256"], + [claimData.to, claimData.sigExpiration, claimData.taskId, claimData.amount] + ); + + const payloadHash = ethers.utils.keccak256(payload); + + // This adds the message prefix + const signature = await signer.signMessage(ethers.utils.arrayify(payloadHash)); + return ethers.utils.splitSignature(signature); +}; From 7905bedd5d11f0fc8ad090b9f5d7a7347c76bc09 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Fri, 23 Aug 2024 11:55:34 -0300 Subject: [PATCH 6/7] add more test --- .../instant-rewards/InstantRewards.sol | 10 ++ .../test/instant-rewards/instant-rewards.ts | 124 ++++++++++++++++-- 2 files changed, 121 insertions(+), 13 deletions(-) diff --git a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol index 0103019..8b3c80b 100644 --- a/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol +++ b/packages/zevm-app-contracts/contracts/instant-rewards/InstantRewards.sol @@ -27,6 +27,7 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { address public signerAddress; event Claimed(address indexed to, bytes32 indexed taskId, uint256 amount); + event Withdrawn(address indexed wallet, uint256 amount); error InvalidSigner(); error SignatureExpired(); @@ -91,6 +92,15 @@ contract InstantRewards is Ownable, Pausable, ReentrancyGuard { if (wallet == address(0)) revert InvalidAddress(); if (amount > address(this).balance) revert TransferFailed(); payable(wallet).transfer(amount); + emit Withdrawn(wallet, amount); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); } receive() external payable {} diff --git a/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts b/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts index 8e2a992..5636949 100644 --- a/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts +++ b/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts @@ -1,6 +1,6 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { expect } from "chai"; -import { utils } from "ethers"; +import { BigNumber, utils } from "ethers"; import { ethers } from "hardhat"; import { InstantRewards } from "../../typechain-types"; @@ -15,6 +15,27 @@ describe("Instant Rewards Contract test", () => { const encodeTaskId = (taskId: string) => utils.keccak256(utils.defaultAbiCoder.encode(["string"], [taskId])); + const getClaimDataSigned = async ( + signer: SignerWithAddress, + amount: BigNumber, + sigExpiration: number, + taskId: string, + to: string + ) => { + const claimData: ClaimData = { + amount, + sigExpiration, + taskId, + to, + }; + + const signature = await getSignature(signer, claimData); + return { + ...claimData, + signature, + }; + }; + beforeEach(async () => { [owner, signer, user, ...addrs] = await ethers.getSigners(); const instantRewardsFactory = await ethers.getContractFactory("InstantRewards"); @@ -28,7 +49,7 @@ describe("Instant Rewards Contract test", () => { const currentBlock = await ethers.provider.getBlock("latest"); const sigExpiration = currentBlock.timestamp + 1000; const amount = utils.parseEther("1"); - const taskId = encodeTaskId("1/1/1"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); const to = owner.address; // transfer some funds to the contract @@ -37,23 +58,80 @@ describe("Instant Rewards Contract test", () => { value: amount, }); - const claimData: ClaimData = { - amount, - sigExpiration, - taskId, - to, - }; + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); - const signature = await getSignature(signer, claimData); - const claimDataSigned = { - ...claimData, - signature, - }; + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.emit(instantRewards, "Claimed").withArgs(owner.address, taskId, amount); + }); + + it("Should claim if pause and unpause", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp + 1000; + const amount = utils.parseEther("1"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); + const to = owner.address; + + await instantRewards.pause(); + await instantRewards.unpause(); + + // transfer some funds to the contract + await owner.sendTransaction({ + to: instantRewards.address, + value: amount, + }); + + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); const tx = instantRewards.claim(claimDataSigned); await expect(tx).to.emit(instantRewards, "Claimed").withArgs(owner.address, taskId, amount); }); + it("Should revert if try to claim on behalf of somebody else", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp + 1000; + const amount = utils.parseEther("1"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); + const to = user.address; + + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); + + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.revertedWith("InvalidSigner"); + }); + + it("Should revert if try to claim with an expired signature", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp - 1000; + const amount = utils.parseEther("1"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); + const to = owner.address; + + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); + + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.revertedWith("SignatureExpired"); + }); + + it("Should revert if try to claim when contract it's paused", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp + 1000; + const amount = utils.parseEther("1"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); + const to = owner.address; + + await instantRewards.pause(); + + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); + + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.revertedWith("Pausable: paused"); + }); + + it("Should revert if not owner try to pause", async () => { + const tx = instantRewards.connect(user).pause(); + await expect(tx).to.revertedWith("Ownable: caller is not the owner"); + }); + it("Should transfer ownership", async () => { { const ownerAddr = await instantRewards.owner(); @@ -65,4 +143,24 @@ describe("Instant Rewards Contract test", () => { expect(ownerAddr).to.be.eq(user.address); } }); + + it("Should withdraw by owner", async () => { + const amount = utils.parseEther("2"); + const amountToWithdraw = utils.parseEther("1"); + // transfer some funds to the contract + await owner.sendTransaction({ + to: instantRewards.address, + value: amount, + }); + + const userBalanceBefore = await ethers.provider.getBalance(user.address); + + const tx = instantRewards.withdraw(user.address, amountToWithdraw); + await expect(tx).to.emit(instantRewards, "Withdrawn").withArgs(user.address, amountToWithdraw); + + const balanceOfContract = await ethers.provider.getBalance(instantRewards.address); + expect(balanceOfContract).to.be.eq(amount.sub(amountToWithdraw)); + const balanceOfUser = await ethers.provider.getBalance(user.address); + expect(balanceOfUser).to.be.eq(userBalanceBefore.add(amountToWithdraw)); + }); }); From 85c626877113cdfe6c8d9f9b9432509f495992e3 Mon Sep 17 00:00:00 2001 From: Andres Aiello Date: Fri, 23 Aug 2024 12:22:54 -0300 Subject: [PATCH 7/7] add tests --- .../test/instant-rewards/instant-rewards.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts b/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts index 5636949..27ff901 100644 --- a/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts +++ b/packages/zevm-app-contracts/test/instant-rewards/instant-rewards.ts @@ -1,6 +1,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { expect } from "chai"; import { BigNumber, utils } from "ethers"; +import { parseEther } from "ethers/lib/utils"; import { ethers } from "hardhat"; import { InstantRewards } from "../../typechain-types"; @@ -127,6 +128,72 @@ describe("Instant Rewards Contract test", () => { await expect(tx).to.revertedWith("Pausable: paused"); }); + it("Should revert if try to claim twice with same signature", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp + 1000; + const amount = utils.parseEther("1"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); + const to = owner.address; + + // transfer some funds to the contract + await owner.sendTransaction({ + to: instantRewards.address, + value: amount, + }); + + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); + + instantRewards.claim(claimDataSigned); + + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.revertedWith("TaskAlreadyClaimed"); + }); + + it("Should revert if try to claim same task with another signature", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp + 1000; + const amount = utils.parseEther("1"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); + const to = owner.address; + + // transfer some funds to the contract + await owner.sendTransaction({ + to: instantRewards.address, + value: amount, + }); + + { + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); + instantRewards.claim(claimDataSigned); + } + const claimDataSigned = await getClaimDataSigned(signer, amount.add(parseEther("1")), sigExpiration, taskId, to); + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.revertedWith("TaskAlreadyClaimed"); + }); + + it("Should revert if try to claim with an old valid signature if a new one was used", async () => { + const currentBlock = await ethers.provider.getBlock("latest"); + const sigExpiration = currentBlock.timestamp + 1000; + const amount = utils.parseEther("2"); + const taskId = encodeTaskId("WALLET/TASK/EPOC"); + const to = owner.address; + + // transfer some funds to the contract + await owner.sendTransaction({ + to: instantRewards.address, + value: amount, + }); + + const claimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration, taskId, to); + + const newClaimDataSigned = await getClaimDataSigned(signer, amount, sigExpiration + 1000, taskId, to); + + instantRewards.claim(newClaimDataSigned); + + const tx = instantRewards.claim(claimDataSigned); + await expect(tx).to.revertedWith("TaskAlreadyClaimed"); + }); + it("Should revert if not owner try to pause", async () => { const tx = instantRewards.connect(user).pause(); await expect(tx).to.revertedWith("Ownable: caller is not the owner");