ETH Price: $1,978.23 (-5.02%)

Contract Diff Checker

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

Please enter a contract address above to load the contract details and source code.

Context size (optional):