11// SPDX-License-Identifier: MIT
2- pragma solidity 0.8.28 ;
2+ pragma solidity 0.8.35 ;
33
44import {IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol " ;
5+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol " ;
56import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol " ;
67import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol " ;
78import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol " ;
@@ -11,13 +12,24 @@ import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/acces
1112/// @title Claimable Airdrop
1213/// @notice This contract is the implementation of the Claimable Airdrop
1314/// @dev This contract is upgradeable and should be used only through the proxy contract
15+ /// @dev Operational edge cases that cannot be enforced on-chain (the contract only
16+ /// stores the Merkle root, never the full set of leaves):
17+ /// - Revoking unclaimed approvals: the owner can call `updateMerkleRoot` (while
18+ /// paused) to replace the root with one that no longer contains previously valid
19+ /// but unclaimed approvals, effectively revoking them.
20+ /// - First-come, first-served distribution: the contract cannot verify that
21+ /// `tokenDistributor` holds (and has approved) enough tokens to cover every
22+ /// approval. If the distributor is underfunded, later claims revert until it is
23+ /// topped up, so claimants should claim as early as possible.
1424/// @custom:security-contact security@alignedfoundation.org
1525contract ClaimableAirdrop is
1626 Initializable ,
1727 ReentrancyGuardUpgradeable ,
1828 PausableUpgradeable ,
1929 Ownable2StepUpgradeable
2030{
31+ using SafeERC20 for IERC20 ;
32+
2133 /// @notice Address of the token contract to claim.
2234 address public tokenProxy;
2335
@@ -94,12 +106,7 @@ contract ClaimableAirdrop is
94106
95107 _verifyAndMark (amount, validFrom, merkleProof);
96108
97- bool success = IERC20 (tokenProxy).transferFrom (
98- tokenDistributor,
99- msg .sender ,
100- amount
101- );
102- require (success, "Failed to transfer funds " );
109+ IERC20 (tokenProxy).safeTransferFrom (tokenDistributor, msg .sender , amount);
103110
104111 emit TokensClaimed (msg .sender , amount);
105112 }
@@ -132,12 +139,11 @@ contract ClaimableAirdrop is
132139
133140 require (totalClaimable > 0 , "Nothing to claim " );
134141
135- bool success = IERC20 (tokenProxy).transferFrom (
142+ IERC20 (tokenProxy).safeTransferFrom (
136143 tokenDistributor,
137144 msg .sender ,
138145 totalClaimable
139146 );
140- require (success, "Failed to transfer funds " );
141147
142148 emit TokensClaimed (msg .sender , totalClaimable);
143149 }
@@ -206,4 +212,11 @@ contract ClaimableAirdrop is
206212 function unpause () external onlyOwner {
207213 _unpause ();
208214 }
215+
216+ /// @notice Prevents the owner from renouncing ownership.
217+ /// @dev Renouncing would set the owner to address(0), permanently disabling
218+ /// `updateMerkleRoot`, `extendClaimPeriod`, `pause`, and `unpause`.
219+ function renounceOwnership () public view override onlyOwner {
220+ revert ("Cannot renounce ownership " );
221+ }
209222}
0 commit comments