Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Protocol fees distributor #46

Merged
merged 12 commits into from
Apr 3, 2024
31 changes: 20 additions & 11 deletions contracts/ProtocolFeesDistributor.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;

Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}

Expand Down
4 changes: 4 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down
101 changes: 83 additions & 18 deletions test/protocolFeesDistributor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@

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();
Expand Down Expand Up @@ -42,11 +49,21 @@

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];
Expand Down Expand Up @@ -101,11 +118,15 @@

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];
Expand Down Expand Up @@ -181,6 +202,12 @@
});

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()],
Expand All @@ -203,9 +230,13 @@
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(
Expand Down Expand Up @@ -335,6 +366,12 @@
});

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()],
Expand All @@ -352,13 +389,59 @@
// 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()) {

Check warning on line 429 in test/protocolFeesDistributor.test.ts

View workflow job for this annotation

GitHub Actions / format

'user' is assigned a value but never used
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 () => {
Expand Down Expand Up @@ -404,23 +487,5 @@
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()");
});
});
});
Loading