diff --git a/contracts/ProtocolFeesDistributor.sol b/contracts/ProtocolFeesDistributor.sol index f065366..8c46d58 100644 --- a/contracts/ProtocolFeesDistributor.sol +++ b/contracts/ProtocolFeesDistributor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.23; import {LowLevelWETH} from "@looksrare/contracts-libs/contracts/lowLevelCallers/LowLevelWETH.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; @@ -24,6 +24,9 @@ contract ProtocolFeesDistributor is Pausable, ReentrancyGuard, AccessControl, Lo // Current round (users can only claim pending protocol fees for the current round) uint256 public currentRound; + // Users can claim until this timestamp + uint256 public canClaimUntil; + // Max amount per user in current tree uint256 public maximumAmountPerUserInCurrentTree; @@ -33,20 +36,18 @@ contract ProtocolFeesDistributor is Pausable, ReentrancyGuard, AccessControl, Lo // Merkle root for a round mapping(uint256 => bytes32) public merkleRootOfRound; - // Checks whether a merkle root was used - mapping(bytes32 => bool) public merkleRootUsed; - // Keeps track on whether user has claimed at a given round mapping(uint256 => mapping(address => bool)) public hasUserClaimedForRound; event ProtocolFeesClaimed(address indexed user, uint256 indexed round, uint256 amount); event ProtocolFeesDistributionUpdated(uint256 indexed round); event EthWithdrawn(uint256 amount); + event CanClaimUntilUpdated(uint256 timestamp); error AlreadyClaimed(); error AmountHigherThanMax(); + error ClaimPeriodEnded(); error InvalidProof(); - error MerkleRootAlreadyUsed(); /** * @notice Constructor @@ -66,7 +67,6 @@ contract ProtocolFeesDistributor is Pausable, ReentrancyGuard, AccessControl, Lo address _blastPointsOperator ) { WETH = _weth; - merkleRootUsed[bytes32(0)] = true; _grantRole(DEFAULT_ADMIN_ROLE, _owner); _grantRole(OPERATOR_ROLE, _owner); @@ -87,6 +87,10 @@ contract ProtocolFeesDistributor is Pausable, ReentrancyGuard, AccessControl, Lo revert AlreadyClaimed(); } + if (block.timestamp >= canClaimUntil) { + revert ClaimPeriodEnded(); + } + (bool claimStatus, uint256 adjustedAmount) = _canClaim(msg.sender, amount, merkleProof); if (!claimStatus) { @@ -121,20 +125,21 @@ contract ProtocolFeesDistributor is Pausable, ReentrancyGuard, AccessControl, Lo function updateProtocolFeesDistribution(bytes32 merkleRoot, uint256 newMaximumAmountPerUser) external payable + whenPaused onlyRole(OPERATOR_ROLE) { - if (merkleRootUsed[merkleRoot]) { - revert MerkleRootAlreadyUsed(); - } - currentRound++; merkleRootOfRound[currentRound] = merkleRoot; - merkleRootUsed[merkleRoot] = true; maximumAmountPerUserInCurrentTree = newMaximumAmountPerUser; emit ProtocolFeesDistributionUpdated(currentRound); } + function updateCanClaimUntil(uint256 timestamp) external onlyRole(OPERATOR_ROLE) { + canClaimUntil = timestamp; + emit CanClaimUntilUpdated(timestamp); + } + /** * @notice Pause claim */ @@ -170,6 +175,10 @@ contract ProtocolFeesDistributor is Pausable, ReentrancyGuard, AccessControl, Lo uint256 amount, bytes32[] calldata merkleProof ) external view returns (bool, uint256) { + if (block.timestamp >= canClaimUntil) { + return (false, 0); + } + return _canClaim(user, amount, merkleProof); } diff --git a/hardhat.config.ts b/hardhat.config.ts index 877de19..e1ab637 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -32,6 +32,10 @@ const config: HardhatUserConfig = { }, solidity: { compilers: [ + { + version: "0.8.23", + settings: { optimizer: { enabled: true, runs: 888888 } }, + }, { version: "0.8.19", settings: { optimizer: { enabled: true, runs: 888888 } }, diff --git a/test/protocolFeesDistributor.test.ts b/test/protocolFeesDistributor.test.ts index 03f6be2..29182e0 100644 --- a/test/protocolFeesDistributor.test.ts +++ b/test/protocolFeesDistributor.test.ts @@ -11,6 +11,13 @@ describe("ProtocolFeesDistributor", () => { let admin: SignerWithAddress; let accounts: SignerWithAddress[]; + let blockTimestamp; + + const getBlockTimestamp = async () => { + const blockNumber = await ethers.provider.getBlockNumber(); + const block = await ethers.provider.getBlock(blockNumber); + return block.timestamp; + }; beforeEach(async () => { accounts = await ethers.getSigners(); @@ -42,11 +49,21 @@ describe("ProtocolFeesDistributor", () => { let tree = StandardMerkleTree.of(values, ["address", "uint256"]); + await protocolFeesDistributor.connect(admin).pause(); + let tx = await protocolFeesDistributor .connect(admin) .updateProtocolFeesDistribution(tree.root, parseEther("5000"), { value: parseEther("10000") }); await expect(tx).to.emit(protocolFeesDistributor, "ProtocolFeesDistributionUpdated").withArgs("1"); + await protocolFeesDistributor.connect(admin).unpause(); + + blockTimestamp = await getBlockTimestamp(); + tx = await protocolFeesDistributor.connect(admin).updateCanClaimUntil(blockTimestamp + 100); + await expect(tx) + .to.emit(protocolFeesDistributor, "CanClaimUntilUpdated") + .withArgs(blockTimestamp + 100); + // All users except the 4th one claims for (const [index, [user, value]] of tree.entries()) { const signedUser = accounts[Number(index) + 1]; @@ -101,11 +118,15 @@ describe("ProtocolFeesDistributor", () => { tree = StandardMerkleTree.of(values2, ["address", "uint256"]); + await protocolFeesDistributor.connect(admin).pause(); + tx = await protocolFeesDistributor .connect(admin) .updateProtocolFeesDistribution(tree.root, parseEther("8000"), { value: parseEther("10000") }); await expect(tx).to.emit(protocolFeesDistributor, "ProtocolFeesDistributionUpdated").withArgs("2"); + await protocolFeesDistributor.connect(admin).unpause(); + // All users except the 4th one claims for (const [index, [user, value]] of tree.entries()) { const signedUser = accounts[Number(index) + 1]; @@ -181,6 +202,12 @@ describe("ProtocolFeesDistributor", () => { }); it("Claim - Users cannot claim with wrong proofs", async () => { + blockTimestamp = await getBlockTimestamp(); + const tx = await protocolFeesDistributor.connect(admin).updateCanClaimUntil(blockTimestamp + 100); + await expect(tx) + .to.emit(protocolFeesDistributor, "CanClaimUntilUpdated") + .withArgs(blockTimestamp + 100); + // Users 1 to 4 const values = [ ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", parseEther("5000").toString()], @@ -203,9 +230,13 @@ describe("ProtocolFeesDistributor", () => { const hexProof1 = tree.getProof([user1.address, expectedAmountToReceiveForUser1.toString()]); const hexProof2 = tree.getProof([user2.address, expectedAmountToReceiveForUser2.toString()]); + await protocolFeesDistributor.connect(admin).pause(); + // Owner adds protocol fees and unpause distribution await protocolFeesDistributor.connect(admin).updateProtocolFeesDistribution(tree.root, parseEther("5000")); + await protocolFeesDistributor.connect(admin).unpause(); + // 1. Verify leafs for user1/user2 are matched in the tree with the computed root assert.isTrue( StandardMerkleTree.verify( @@ -335,6 +366,12 @@ describe("ProtocolFeesDistributor", () => { }); it("Claim - User cannot claim if error in tree computation due to amount too high", async () => { + blockTimestamp = await getBlockTimestamp(); + const tx = await protocolFeesDistributor.connect(admin).updateCanClaimUntil(blockTimestamp + 100); + await expect(tx) + .to.emit(protocolFeesDistributor, "CanClaimUntilUpdated") + .withArgs(blockTimestamp + 100); + // Users 1 to 4 const values = [ ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", parseEther("5000").toString()], @@ -352,13 +389,59 @@ describe("ProtocolFeesDistributor", () => { // Compute the proof for user1/user2 const hexProof1 = tree.getProof([user1.address, expectedAmountToReceiveForUser1.toString()]); + await protocolFeesDistributor.connect(admin).pause(); + // Owner adds protocol fees and unpause distribution await protocolFeesDistributor.connect(admin).updateProtocolFeesDistribution(tree.root, parseEther("4999.9999")); + await protocolFeesDistributor.connect(admin).unpause(); + await expect( protocolFeesDistributor.connect(user1).claim(expectedAmountToReceiveForUser1, hexProof1) ).to.be.revertedWith("AmountHigherThanMax()"); }); + + it("Claim - Users cannot claim if the current timestamp is >= canClaimUntil", async () => { + // Users 1 to 4 + const values = [ + ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", parseEther("5000").toString()], + ["0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", parseEther("3000").toString()], + ["0x90F79bf6EB2c4f870365E785982E1f101E93b906", parseEther("1000").toString()], + ["0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", parseEther("1000").toString()], + ]; + + const tree = StandardMerkleTree.of(values, ["address", "uint256"]); + + await protocolFeesDistributor.connect(admin).pause(); + + let tx = await protocolFeesDistributor + .connect(admin) + .updateProtocolFeesDistribution(tree.root, parseEther("5000"), { value: parseEther("10000") }); + await expect(tx).to.emit(protocolFeesDistributor, "ProtocolFeesDistributionUpdated").withArgs("1"); + + await protocolFeesDistributor.connect(admin).unpause(); + + blockTimestamp = await getBlockTimestamp(); + tx = await protocolFeesDistributor.connect(admin).updateCanClaimUntil(blockTimestamp); + await expect(tx).to.emit(protocolFeesDistributor, "CanClaimUntilUpdated").withArgs(blockTimestamp); + + // All users except the 4th one claims + for (const [index, [user, value]] of tree.entries()) { + const signedUser = accounts[Number(index) + 1]; + + if (signedUser === accounts[3]) { + break; + } + + // Compute the proof for the user + const hexProof = tree.getProof(index); + + // Cannot double claim + await expect(protocolFeesDistributor.connect(signedUser).claim(value, hexProof)).to.be.revertedWith( + "ClaimPeriodEnded()" + ); + } + }); }); describe("#2 - Owner functions", async () => { @@ -404,23 +487,5 @@ describe("ProtocolFeesDistributor", () => { beforeWithdrawBalance.add(depositAmount).sub(txFee).toString() ); }); - - it("Owner - Owner cannot set twice the same Merkle Root", async () => { - // Users 1 to 4 - const values = [ - ["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", parseEther("5000").toString()], - ["0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", parseEther("3000").toString()], - ["0x90F79bf6EB2c4f870365E785982E1f101E93b906", parseEther("1000").toString()], - ["0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", parseEther("1000").toString()], - ]; - - const tree = StandardMerkleTree.of(values, ["address", "uint256"]); - - await protocolFeesDistributor.connect(admin).updateProtocolFeesDistribution(tree.root, parseEther("5000")); - - await expect( - protocolFeesDistributor.connect(admin).updateProtocolFeesDistribution(tree.root, parseEther("5000")) - ).to.be.revertedWith("MerkleRootAlreadyUsed()"); - }); }); });