@@ -30,9 +30,9 @@ contract ClaimableAirdrop is
3030 /// @notice Merkle root of the claimants.
3131 bytes32 public claimMerkleRoot;
3232
33- /// @notice Mapping of the claimants that have claimed the tokens .
34- /// @dev true if the claimant has claimed the tokens .
35- mapping (address claimer = > bool claimed ) public hasClaimed;
33+ /// @notice Mapping tracking claimed leaves per address .
34+ /// @dev Key is keccak256(abi.encode(claimer, leaf)), value is true if claimed .
35+ mapping (bytes32 => bool ) public hasClaimed;
3636
3737 /// @notice Event emitted when a claimant claims the tokens.
3838 /// @param to address of the claimant.
@@ -78,41 +78,102 @@ contract ClaimableAirdrop is
7878 _pause ();
7979 }
8080
81- /// @notice Claim the tokens.
82- /// @param amount amount of tokens to claim.
81+ /// @notice Claim tokens for a single vesting stage.
82+ /// @param amount amount of tokens to claim for this stage.
83+ /// @param validFrom timestamp from which this stage is claimable.
8384 /// @param merkleProof Merkle proof of the claim.
8485 function claim (
8586 uint256 amount ,
87+ uint256 validFrom ,
8688 bytes32 [] calldata merkleProof
8789 ) external nonReentrant whenNotPaused {
8890 require (
89- ! hasClaimed[msg .sender ],
90- "Account has already claimed the drop "
91+ block .timestamp <= limitTimestampToClaim,
92+ "Drop is no longer claimable "
93+ );
94+
95+ _verifyAndMark (amount, validFrom, merkleProof);
96+
97+ bool success = IERC20 (tokenProxy).transferFrom (
98+ tokenDistributor,
99+ msg .sender ,
100+ amount
91101 );
102+ require (success, "Failed to transfer funds " );
103+
104+ emit TokensClaimed (msg .sender , amount);
105+ }
106+
107+ /// @notice Claim tokens for multiple vesting stages in a single transaction.
108+ /// @param amounts array of token amounts per stage.
109+ /// @param validFroms array of timestamps from which each stage is claimable.
110+ /// @param merkleProofs array of Merkle proofs, one per stage.
111+ function claimBatch (
112+ uint256 [] calldata amounts ,
113+ uint256 [] calldata validFroms ,
114+ bytes32 [][] calldata merkleProofs
115+ ) external nonReentrant whenNotPaused {
92116 require (
93117 block .timestamp <= limitTimestampToClaim,
94118 "Drop is no longer claimable "
95119 );
96-
97- bytes32 leaf = keccak256 (
98- bytes .concat (keccak256 (abi.encode (msg .sender , amount)))
120+ uint256 length = amounts.length ;
121+ require (
122+ length == validFroms.length && length == merkleProofs.length ,
123+ "Array length mismatch "
99124 );
100- bool verifies = MerkleProof.verify (merkleProof, claimMerkleRoot, leaf);
101125
102- require (verifies, " Invalid Merkle proof " ) ;
126+ uint256 totalClaimable = 0 ;
103127
104- // Done before the transfer call to make sure the reentrancy bug is not possible
105- hasClaimed[msg .sender ] = true ;
128+ for (uint256 i = 0 ; i < length; i++ ) {
129+ _verifyAndMark (amounts[i], validFroms[i], merkleProofs[i]);
130+ totalClaimable += amounts[i];
131+ }
132+
133+ require (totalClaimable > 0 , "Nothing to claim " );
106134
107135 bool success = IERC20 (tokenProxy).transferFrom (
108136 tokenDistributor,
109137 msg .sender ,
110- amount
138+ totalClaimable
111139 );
112-
113140 require (success, "Failed to transfer funds " );
114141
115- emit TokensClaimed (msg .sender , amount);
142+ emit TokensClaimed (msg .sender , totalClaimable);
143+ }
144+
145+ /// @notice Verify a single-stage Merkle proof and mark the stage as claimed.
146+ /// @dev Shared by `claim` and `claimBatch`. Does not perform the token transfer.
147+ /// @param amount amount of tokens for this stage.
148+ /// @param validFrom timestamp from which this stage is claimable.
149+ /// @param merkleProof Merkle proof for this stage.
150+ function _verifyAndMark (
151+ uint256 amount ,
152+ uint256 validFrom ,
153+ bytes32 [] calldata merkleProof
154+ ) internal {
155+ require (
156+ block .timestamp >= validFrom,
157+ "Stage not yet claimable "
158+ );
159+
160+ bytes32 leaf = keccak256 (
161+ bytes .concat (
162+ keccak256 (abi.encode (msg .sender , amount, validFrom))
163+ )
164+ );
165+
166+ bytes32 claimKey = keccak256 (abi.encode (msg .sender , leaf));
167+ require (! hasClaimed[claimKey], "Stage already claimed " );
168+
169+ bool verifies = MerkleProof.verify (
170+ merkleProof,
171+ claimMerkleRoot,
172+ leaf
173+ );
174+ require (verifies, "Invalid Merkle proof " );
175+
176+ hasClaimed[claimKey] = true ;
116177 }
117178
118179 /// @notice Update the Merkle root.
0 commit comments