Contract Name:
MerkleAirdrop
Contract Source Code:
<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
/*
████████╗ █████╗ ██╗ ██╗███████╗███╗ ███╗ █████╗ ███╗ ██╗
╚══██╔══╝██╔══██╗██║ ██║██╔════╝████╗ ████║██╔══██╗████╗ ██║
██║ ███████║██║ ██║███████╗██╔████╔██║███████║██╔██╗ ██║
██║ ██╔══██║██║ ██║╚════██║██║╚██╔╝██║██╔══██║██║╚██╗██║
██║ ██║ ██║███████╗██║███████║██║ ╚═╝ ██║██║ ██║██║ ╚████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝
*/
import {IERC20} from "lib/forge-std/src/interfaces/IERC20.sol";
/// @title MerkleAirdrop
/// @notice ERC20 airdrop contract backed by an immutable Merkle root and claim deadline.
contract MerkleAirdrop {
/// @dev ERC20 being distributed.
IERC20 public immutable token;
/// @dev Merkle root describing all valid claims.
bytes32 public immutable merkleRoot;
/// @dev Last timestamp (inclusive) when claims are allowed.
uint64 public immutable deadline;
/// @dev Current contract owner allowed to sweep leftover tokens.
address public owner;
/// @dev Tracks which claim indices have already been redeemed (bit-packed).
mapping(uint256 => uint256) private claimedBitMap;
/// @dev Emitted whenever a claim succeeds.
event Claimed(uint256 indexed index, address indexed account, uint256 amount);
/// @dev Emitted after ownership changes.
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/// @dev Emitted when leftover tokens are swept post-deadline.
event OwnerSweep(address indexed owner, uint256 amount);
/// @dev Thrown when attempting to claim or sweep after the deadline has passed.
error ClaimWindowClosed();
/// @dev Thrown when attempting to sweep before the deadline passed.
error ClaimWindowOpen();
/// @dev Thrown when caller lacks the required permissions.
error Unauthorized();
/// @dev Thrown when providing invalid constructor arguments.
error InvalidParams();
/// @dev Thrown when attempting to claim the same allocation twice.
error AlreadyClaimed();
/// @dev Thrown when a supplied Merkle proof does not match the stored root.
error InvalidProof();
/// @dev Thrown when an ERC20 transfer fails.
error TransferFailed();
constructor(IERC20 token_, bytes32 merkleRoot_, uint64 deadline_, address owner_) {
if (
address(token_) == address(0) || merkleRoot_ == bytes32(0) || deadline_ <= block.timestamp
|| owner_ == address(0)
) {
revert InvalidParams();
}
token = token_;
merkleRoot = merkleRoot_;
deadline = deadline_;
owner = owner_;
emit OwnershipTransferred(address(0), owner_);
}
/// @notice Claim tokens if the provided proof validates against the Merkle root.
function claim(uint256 index, address account, uint256 amount, bytes32[] calldata merkleProof) external {
if (block.timestamp > deadline) revert ClaimWindowClosed();
if (_isClaimed(index)) revert AlreadyClaimed();
bytes32 node = keccak256(abi.encodePacked(index, account, amount));
if (!_verify(merkleProof, merkleRoot, node)) revert InvalidProof();
_setClaimed(index);
_safeTransfer(account, amount);
emit Claimed(index, account, amount);
}
/// @notice Sweep remaining tokens to the owner after the claim window closes.
function sweep() external {
if (msg.sender != owner) revert Unauthorized();
if (block.timestamp <= deadline) revert ClaimWindowOpen();
uint256 balance = token.balanceOf(address(this));
if (balance > 0) {
_safeTransfer(owner, balance);
}
emit OwnerSweep(owner, balance);
}
/// @notice Transfer contract ownership.
function transferOwnership(address newOwner) external {
if (msg.sender != owner) revert Unauthorized();
if (newOwner == address(0)) revert InvalidParams();
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
/// @notice Returns true if the claim at `index` has already been made.
function isClaimed(uint256 index) external view returns (bool) {
return _isClaimed(index);
}
/// @dev Sets the bit for the given index in the bitmap.
function _setClaimed(uint256 index) private {
uint256 wordIndex = index >> 8;
uint256 bitIndex = index & 0xff;
claimedBitMap[wordIndex] |= 1 << bitIndex;
}
/// @dev Returns true if the given index has been claimed.
function _isClaimed(uint256 index) private view returns (bool) {
uint256 wordIndex = index >> 8;
uint256 bitIndex = index & 0xff;
return claimedBitMap[wordIndex] & (1 << bitIndex) != 0;
}
/// @dev Performs an ERC20 transfer that tolerates missing return values.
function _safeTransfer(address to, uint256 amount) private {
(bool success, bytes memory data) =
address(token).call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed();
}
/// @dev Verifies the provided Merkle proof against `root`.
function _verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf) private pure returns (bool) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 proofElement = proof[i];
if (computedHash <= proofElement) {
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}
}
return computedHash == root;
}
} <i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2;
/// @dev Interface of the ERC20 standard as defined in the EIP.
/// @dev This includes the optional name, symbol, and decimals metadata.
interface IERC20 {
/// @dev Emitted when `value` tokens are moved from one account (`from`) to another (`to`).
event Transfer(address indexed from, address indexed to, uint256 value);
/// @dev Emitted when the allowance of a `spender` for an `owner` is set, where `value`
/// is the new allowance.
event Approval(address indexed owner, address indexed spender, uint256 value);
/// @notice Returns the amount of tokens in existence.
function totalSupply() external view returns (uint256);
/// @notice Returns the amount of tokens owned by `account`.
function balanceOf(address account) external view returns (uint256);
/// @notice Moves `amount` tokens from the caller's account to `to`.
function transfer(address to, uint256 amount) external returns (bool);
/// @notice Returns the remaining number of tokens that `spender` is allowed
/// to spend on behalf of `owner`
function allowance(address owner, address spender) external view returns (uint256);
/// @notice Sets `amount` as the allowance of `spender` over the caller's tokens.
/// @dev Be aware of front-running risks: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
function approve(address spender, uint256 amount) external returns (bool);
/// @notice Moves `amount` tokens from `from` to `to` using the allowance mechanism.
/// `amount` is then deducted from the caller's allowance.
function transferFrom(address from, address to, uint256 amount) external returns (bool);
/// @notice Returns the name of the token.
function name() external view returns (string memory);
/// @notice Returns the symbol of the token.
function symbol() external view returns (string memory);
/// @notice Returns the decimals places of the token.
function decimals() external view returns (uint8);
}