// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "./interfaces/IMerkleDistributor.sol"; import "./interfaces/IERC1155Minter.sol"; import "./interfaces/IERC20Minter.sol"; /** * @dev Distribute rewards according to a MerkleTree of reward data as nodes. */ contract MerkleDistributor is IMerkleDistributor, Ownable, Pausable, ERC1155Holder { using SafeERC20 for IERC20; using ERC165Checker for address; bytes4 private constant IERC1155_INTERFACE = 0xd9b67a26; bytes4 private constant IERC1155_MINT_INTERFACE = 0x731133e9; bytes4 private constant IERC20_MINT_INTERFACE = 0x40c10f19; bytes4 private constant IERC20_INTERFACE = 0xffffffff; address public immutable override token; uint256 public immutable tokenId; bytes4 public immutable claimInterface; bytes32 public override merkleRoot; bytes32 public ipfsCid; // This is a packed array of booleans, chunked by rounds. mapping(uint256 => mapping(uint256 => uint256)) private claimedBitMap; uint256 public currentRound = 0; event NewAirdropRound( uint256 round, bytes32 merkleRoot, bytes32 ipfsCid ); /** * @dev Initialise MerkleDistributor with initial distributor settings. * @param _token Reward token address. * @param _tokenId Token id for ERC1155, 0 for ERC20. * @param _claimInterface API to claim the reward: 0xd9b67a26, 0x731133e9, 0x40c10f19, 0xffffffff. * @param _merkleRoot Merkle root of distribution. * @param _ipfsCid IPFS hash (without 1220 prefix) with the merkle tree data for the provided root. */ constructor(address _token, uint256 _tokenId, bytes4 _claimInterface, bytes32 _merkleRoot, bytes32 _ipfsCid) { token = _token; tokenId = _tokenId; require( _claimInterface == IERC20_INTERFACE || _claimInterface == IERC20_MINT_INTERFACE || _claimInterface == IERC1155_MINT_INTERFACE || token.supportsInterface(_claimInterface), "MerkleDistributor: Invalid interface" ); claimInterface = _claimInterface; merkleRoot = _merkleRoot; emit NewAirdropRound(0, _merkleRoot, _ipfsCid); } /** * @dev Owner can change token distribution rules. * @param _newRound Passing the new round will expire all unclaimed rewards. * @param _newMerkleRoot New distribution rules, pass current round to update ongoing distribution. * @param _newIpfsCid Distribution data used to generate merkle root. */ function adminSetNewRound(uint256 _newRound, bytes32 _newMerkleRoot, bytes32 _newIpfsCid) external onlyOwner { require(_newRound > currentRound, "MerkleDistributor: Expired airdrop."); currentRound = _newRound; merkleRoot = _newMerkleRoot; ipfsCid = _newIpfsCid; emit NewAirdropRound(_newRound, _newMerkleRoot, _newIpfsCid); } /** * @dev Owner can pause and continue distribution. * @param _state Pass true to pause, false to continue. */ function adminSetPaused(bool _state) external onlyOwner { if (_state) { _pause(); } else { _unpause(); } } /** * @dev Withdraw all unclaimed tokens allocated to this address. * @param _beneficiary Address to recieve unclaimed tokens. */ function adminWithdrawUnclaimed(address _beneficiary) external onlyOwner { if (claimInterface == IERC1155_INTERFACE) { uint256 _tokenId = tokenId; uint256 _currentBalance = IERC1155(token).balanceOf(address(this), _tokenId); IERC1155(token).safeTransferFrom(address(this), _beneficiary, _tokenId, _currentBalance, ""); } else if (claimInterface == IERC20_INTERFACE) { uint256 _currentBalance = IERC20(token).balanceOf(address(this)); IERC20(token).safeTransfer(_beneficiary, _currentBalance); } } /** * @dev Check if specific user has claimed his airdrop in a current round. * @param _index Aidrop index. */ function isClaimed(uint256 _index) public view override returns (bool) { return _isClaimed(_index, currentRound); } /** * @dev Storage optimisation for checking specific airdrop claim. * @param _index Aidrop index. * @param _currentRound Current round. */ function _isClaimed(uint256 _index, uint256 _currentRound) public view returns (bool) { uint256 _claimedWordIndex = _index / 256; uint256 _claimedBitIndex = _index % 256; uint256 _claimedWord = claimedBitMap[_currentRound][_claimedWordIndex]; uint256 _mask = (1 << _claimedBitIndex); return _claimedWord & _mask == _mask; } /** * @dev Mark airdrop in a current round as claimed. * @param _index Aidrop index. * @param _currentRound Current round. */ function _setClaimed(uint256 _index, uint256 _currentRound) private { uint256 _claimedWordIndex = _index / 256; uint256 _claimedBitIndex = _index % 256; claimedBitMap[_currentRound][_claimedWordIndex] = claimedBitMap[_currentRound][_claimedWordIndex] | (1 << _claimedBitIndex); } /** * @dev Claim airdrop allocated for account by index, can be paused. * @param _index Aidrop index. * @param _account Address which will get an airdrop. * @param _amount Amount to claim. * @param _merkleProof Merkle proof of the airdrop. */ function claim(uint256 _index, address _account, uint256 _amount, bytes32[] calldata _merkleProof) external override whenNotPaused { uint256 _currentRound = currentRound; require(!_isClaimed(_index, _currentRound), 'MerkleDistributor: Drop already claimed.'); // Verify the merkle proof. bytes32 _node = keccak256(abi.encodePacked(_index, _account, _amount)); require(MerkleProof.verify(_merkleProof, merkleRoot, _node), 'MerkleDistributor: Invalid proof.'); // Mark it claimed and send the token. _setClaimed(_index, _currentRound); if (claimInterface == IERC1155_INTERFACE) { IERC1155(token).safeTransferFrom(address(this), _account, tokenId, _amount, ""); } else if (claimInterface == IERC1155_MINT_INTERFACE) { IERC1155Minter(token).mint(_account, tokenId, _amount, ""); } else if (claimInterface == IERC20_MINT_INTERFACE) { IERC20Minter(token).mint(_account, _amount); } else { IERC20(token).safeTransfer(_account, _amount); } emit Claimed(_index, _currentRound, _account, _amount); } }