Skip to content

feat: Protocol fees distributor #46

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

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";
@@ -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);
}

4 changes: 4 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -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 } },
101 changes: 83 additions & 18 deletions test/protocolFeesDistributor.test.ts
Original file line number Diff line number Diff line change
@@ -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()");
});
});
});