Skip to content

Commit

Permalink
Change to use max amount to claim for airdrop
Browse files Browse the repository at this point in the history
  • Loading branch information
jay-trends committed Aug 31, 2023
1 parent a023e5c commit f3d670a
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 55 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/node_modules/
/*.iml
/artifacts/
/cache/
/coverage/
39 changes: 22 additions & 17 deletions contracts/TrendsAirdrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ contract TrendsAirdrop {
using SafeERC20 for IERC20;

error ClaimEnded();
error NotShareHolder();
error MaxClaimsReached();
error NotCreator();
error InvalidProof();
error NoVestedAmount();
error NoClaimableAmount();
error OnlyClaimOnceAllowed();

IERC20 public immutable trendsToken;
bytes32 public immutable merkleRoot;
bytes32 public immutable chatroomId;

TrendsSharesV1 private immutable trendsShare;

Expand All @@ -27,9 +25,8 @@ contract TrendsAirdrop {
uint256 public immutable blockPerPeriod; // Approximation

// Airdrop configuration
uint256 public deadline;
uint256 public maxClaimableAddresses;
uint256 public claimedAddressesCount;
uint256 public claimed;
uint256 public maxToClaim;

// Vesting structure
struct Vesting {
Expand All @@ -48,26 +45,21 @@ contract TrendsAirdrop {
TrendsSharesV1 _trendsShare,
address _trendsToken,
bytes32 _merkleRoot,
uint256 _deadline,
uint256 _maxClaimableAddresses,
bytes32 _chatroomId,
uint256 _maxToClaim,
uint256 _vestingPeriod,
uint256 _blockPerPeriod
) {
trendsToken = IERC20(_trendsToken);
merkleRoot = _merkleRoot;
deadline = _deadline;
trendsShare = _trendsShare;
maxClaimableAddresses = _maxClaimableAddresses;
chatroomId = _chatroomId;
maxToClaim = _maxToClaim;
vestingPeriod = _vestingPeriod;
blockPerPeriod = _blockPerPeriod;
}

function claim(bytes32[] calldata proof, uint256 amount) external {
if (block.timestamp > deadline) revert ClaimEnded();
if (trendsShare.sharesBalance(chatroomId, msg.sender) == 0) revert NotShareHolder();
if (claimedAddressesCount >= maxClaimableAddresses) revert MaxClaimsReached();
function claim(bytes32[] calldata proof, uint256 amount, bytes32 subject) external {
if (claimed >= maxToClaim) revert ClaimEnded();
if (trendsShare.sharesCreator(subject) == address(0)) revert NotCreator();

// Verify the Merkle proof
bytes32 node = keccak256(abi.encodePacked(msg.sender, amount));
Expand All @@ -76,14 +68,27 @@ contract TrendsAirdrop {
// Initialize or update vesting
Vesting storage v = vesting[msg.sender];
if (v.amount != 0) revert OnlyClaimOnceAllowed();

// Claim the rest if reaching maxToClaim
if (amount + claimed > maxToClaim) {
amount = maxToClaim - claimed;
}
v.amount = amount;
v.startBlock = block.number;

claimedAddressesCount++;
claimed += amount;

emit VestingStarted(msg.sender, amount);
}

function claimable(address recipient) external view returns (uint256) {
Vesting memory v = vesting[recipient];
if (v.amount == 0) return 0;
uint256 blocksSinceStart = block.number - v.startBlock;
uint256 vestedAmount = min(v.amount, (v.amount * blocksSinceStart) / (blockPerPeriod * vestingPeriod));
return vestedAmount - v.claimedAmount;
}

function claimVestedAirdrop() external {
Vesting storage v = vesting[msg.sender];
if (v.amount == 0) revert NoVestedAmount();
Expand Down
68 changes: 30 additions & 38 deletions test/TrendsAirdrop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ contract("TrendsAirdrop", (accounts) => {
let trendsAirdrop;
let trendsSharesV1;
let trendsToken;
let deadline;
let developer = accounts[0];
let user = airdrop[1].address;
let ineligibleUser = accounts[7];
Expand All @@ -52,43 +51,49 @@ contract("TrendsAirdrop", (accounts) => {
}
await trendsToken.transfer(ineligibleUser, _18dc(10000), {from: developer});
trendsSharesV1 = await TrendsSharesV1.new(trendsToken.address);
deadline = (await web3.eth.getBlock("latest")).timestamp + 60 * 60 * 24; // 24 hours from now
trendsAirdrop = await TrendsAirdrop.new(trendsSharesV1.address, trendsToken.address, merkleRoot, deadline, airdrop.length / 2, subject, vestingPeriod, blockPerPeriod);
trendsAirdrop = await TrendsAirdrop.new(
trendsSharesV1.address,
trendsToken.address,
merkleRoot,
_18dc(9000),
vestingPeriod,
blockPerPeriod);
await trendsToken.transfer(trendsAirdrop.address, _18dc(1000000), {from: developer});
});

it("should not allow users to claim airdrop without holding a share", async () => {
it("should not allow users to claim airdrop without creating a subject", async () => {
const proof = merkleTree.getHexProof(allLeaves[1]);

// Do not buy shares for the user
// await trendsSharesV1.buyShares(user, subject, 10, maxInAmount, {from: user});
await expectRevertCustomError(trendsAirdrop.claim(proof, airdrop[1].amount, {from: user}), "NotShareHolder");
// Do not create subject
// await trendsSharesV1.createShares(subject, declineRatio, {from: user});

await expectRevertCustomError(trendsAirdrop.claim(proof, airdrop[1].amount, subject, {from: user}), "NotCreator");
});

it("should allow eligible users to claim, but can't claim twice", async () => {
// Call the create room and buy shares
await trendsSharesV1.createShares(subject, declineRatio);
await trendsToken.approve(trendsSharesV1.address, initBalance, {from: user});
await trendsSharesV1.buyShares(user, subject, 10, maxInAmount, {from: user});

let proof = merkleTree.getHexProof(allLeaves[1]);
await trendsAirdrop.claim(proof, airdrop[1].amount, {from: user});
await trendsAirdrop.claim(proof, airdrop[1].amount, subject, {from: user});
const vesting = await trendsAirdrop.vesting(user);
assert.equal(vesting.amount.toString(), airdrop[1].amount.toString(), "Vesting airdropAmount incorrect");
assert.equal(vesting.claimedAmount.toString(), "0", "Claimed airdropAmount should be 0");

await expectRevertCustomError(trendsAirdrop.claim(proof, airdrop[1].amount, {from: user}), "OnlyClaimOnceAllowed");
await timeMachine.advanceBlock();
const claimable = await trendsAirdrop.claimable(user);
assert.equal(claimable.toString(), _18dc(5).toString(), "Should have claimable");

await expectRevertCustomError(trendsAirdrop.claim(proof, airdrop[1].amount, subject, {from: user}), "OnlyClaimOnceAllowed");

});

it("should allow users to claim vested airdrop", async () => {
// Call the create room and buy shares
await trendsSharesV1.createShares(subject, declineRatio);
await trendsToken.approve(trendsSharesV1.address, initBalance, {from: user});
await trendsSharesV1.buyShares(user, subject, 10, maxInAmount, {from: user});

let proof = merkleTree.getHexProof(allLeaves[1]);
await trendsAirdrop.claim(proof, airdrop[1].amount, {from: user});
await trendsAirdrop.claim(proof, airdrop[1].amount, subject, {from: user});
// Assuming the user is eligible to claim some vested tokens at this point
let balanceBeforeVesting = await trendsToken.balanceOf(user);
await trendsAirdrop.claimVestedAirdrop({from: user});
Expand All @@ -99,37 +104,25 @@ contract("TrendsAirdrop", (accounts) => {
it("should not allow ineligible users to claim", async () => {
const proof = merkleTree.getHexProof(allLeaves[1]);
await trendsSharesV1.createShares(subject, declineRatio);
await trendsToken.approve(trendsSharesV1.address, initBalance, {from: ineligibleUser});
await trendsSharesV1.buyShares(ineligibleUser, subject, 10, maxInAmount, {from: ineligibleUser});

await expectRevertCustomError(trendsAirdrop.claim(proof, airdrop[1].amount, {from: ineligibleUser}), "InvalidProof");
await expectRevertCustomError(trendsAirdrop.claim(proof, airdrop[1].amount, subject, {from: ineligibleUser}), "InvalidProof");
});

it("should not allow eligible users to claim after deadline", async () => {
await trendsSharesV1.createShares(subject, declineRatio);
await trendsToken.approve(trendsSharesV1.address, initBalance, {from: user});
await trendsSharesV1.buyShares(user, subject, 10, maxInAmount, {from: user});
let proof = merkleTree.getHexProof(allLeaves[1]);
await timeMachine.advanceTime(60 * 60 * 24 + 1); // Move time forward by 24 hours and 1 second
await expectRevertCustomError(trendsAirdrop.claim(proof, airdrop[1].amount, {from: user}), "ClaimEnded");
});

it("should handle reaching max claimable addresses", async () => {
it("should handle reaching max claimable amount", async () => {
// Simulate reaching the max claimable addresses by claiming for all eligible users
await trendsSharesV1.createShares(subject, declineRatio);

for (const [index, recipient] of airdrop.entries()) {
// buy share
await trendsToken.approve(trendsSharesV1.address, initBalance, {from: recipient.address});
await trendsSharesV1.buyShares(recipient.address, subject, 1, maxInAmount, {from: recipient.address});

const proof = merkleTree.getHexProof(allLeaves[index]);
await trendsToken.approve(trendsSharesV1.address, initBalance, {from: recipient.address});
await trendsSharesV1.buyShares(recipient.address, subject, 10, maxInAmount, {from: recipient.address});
if (index <= airdrop.length / 2 - 1) {
await trendsAirdrop.claim(proof, recipient.amount, {from: recipient.address});
if (index < 3) {
await trendsAirdrop.claim(proof, recipient.amount, subject, {from: recipient.address});
} else if (index === 3) {
await trendsAirdrop.claim(proof, recipient.amount, subject, {from: recipient.address});
const vestingDetails = await trendsAirdrop.vesting(recipient.address);
assert.equal(vestingDetails.amount.toString(), _18dc(3000).toString(), "Should have only been vesting the rest");
} else {
await expectRevertCustomError(trendsAirdrop.claim(proof, recipient.amount, {from: recipient.address}), "MaxClaimsReached")
await expectRevertCustomError(trendsAirdrop.claim(proof, recipient.amount, subject, {from: recipient.address}), "ClaimEnded")
break;
}
}
Expand All @@ -143,9 +136,8 @@ contract("TrendsAirdrop", (accounts) => {
// Claim the airdrop to set the vesting
let proof = merkleTree.getHexProof(allLeaves[1]);
await trendsSharesV1.createShares(subject, declineRatio);
await trendsToken.approve(trendsSharesV1.address, initBalance, { from: user });
await trendsSharesV1.buyShares(user, subject, 10, maxInAmount, { from: user });
await trendsAirdrop.claim(proof, airdrop[1].amount, { from: user });

await trendsAirdrop.claim(proof, airdrop[1].amount, subject, { from: user });

// Get the vested amount
const airdropAmount = (await trendsAirdrop.vesting(user)).amount;
Expand Down

0 comments on commit f3d670a

Please sign in to comment.