// SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Multicall.sol"; import {IERC1155Minter} from "./interfaces/IERC1155Minter.sol"; import {IERC20Minter} from "./interfaces/IERC20Minter.sol"; import {IConditionOracle} from "./interfaces/IConditionOracle.sol"; import {IConditionalDistributor} from "./interfaces/IConditionalDistributor.sol"; /** * @dev Distribute rewards according to a claim validated by an external oracle contract. */ contract ConditionalDistributor is IConditionalDistributor, ReentrancyGuard, Ownable, Pausable, ERC1155Holder, Multicall { using SafeERC20 for IERC20; 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; mapping(IConditionOracle => bool) public authorizedOracles; event OracleAdded( address indexed oracle ); event OracleRemoved( address indexed oracle ); /** * @dev Initialise contract enabling the default oracle. * @param _defaultOracle Pass non-zero address to enable default oracle. */ constructor(address _defaultOracle) { if (_defaultOracle != address(0)) { authorizedOracles[IConditionOracle(_defaultOracle)] = true; emit OracleAdded(_defaultOracle); } } /** * @dev Enable/disable oracle to check amounts for distribution. * @param _oracle Address of oracle which complies IConditionOracle interface. * @param _state true to enable, false to disable oracle. */ function adminSwitchOracle(address _oracle, bool _state) external onlyOwner { authorizedOracles[IConditionOracle(_oracle)] = _state; if (_state) { emit OracleAdded(_oracle); } else { emit OracleRemoved(_oracle); } } /** * @dev Withdraw all unclaimed tokens allocated to this address. * @param _beneficiary Address which will recieve unclaimed tokens. * @param _claimInterface interface used to distribute reward tokens (mint or transfer selector). * @param _rewardToken ERC20 or ERC1155 reward token contract address. * @param _rewardTokenId ERC1155 token id used for reward, pass 0 for ERC20 reward token. */ function adminWithdrawUnclaimed( address _beneficiary, bytes4 _claimInterface, address _rewardToken, uint256 _rewardTokenId ) external onlyOwner { if (_claimInterface == IERC1155_INTERFACE) { uint256 _currentBalance = IERC1155(_rewardToken).balanceOf(address(this), _rewardTokenId); IERC1155(_rewardToken).safeTransferFrom(address(this), _beneficiary, _rewardTokenId, _currentBalance, ""); } else if (_claimInterface == IERC20_INTERFACE) { uint256 _currentBalance = IERC20(_rewardToken).balanceOf(address(this)); IERC20(_rewardToken).safeTransfer(_beneficiary, _currentBalance); } } /** * @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 Check if airdrop was claimed with specific oracle. * @param _claim Aidrop claim, check prepareClaim from the oracle for specific claim generation. * @return true if airdrop was claimed or claim is otherwise invalid */ function isClaimed(IConditionOracle _oracleContract, bytes calldata _claim) public view returns (bool) { return !_oracleContract.hasClaim(msg.sender, _claim); } /** * @dev Claim airdrop allocated for account with a claim. * @param _account Address which will get an airdrop. * @param _oracleContract Address of oracle which sets the reward amount. * @param _claimInterface interface used to distribute reward tokens (mint or transfer selector). * @param _rewardToken ERC20 or ERC1155 reward token contract address. * @param _rewardTokenId ERC1155 token id used for reward, pass 0 for ERC20 reward token. * @param _claim Aidrop claim, check prepareClaim from the oracle for specific claim generation. */ function claim( address _account, IConditionOracle _oracleContract, bytes4 _claimInterface, address _rewardToken, uint256 _rewardTokenId, bytes calldata _claim ) external whenNotPaused nonReentrant { require(authorizedOracles[_oracleContract], "invalid oracle"); (bytes32 _integrity,) = abi.decode(_claim, (bytes32, bytes)); bytes32 _claimIntegrity = keccak256(abi.encodePacked(_rewardToken, _rewardTokenId, _claimInterface)); require(_integrity == _claimIntegrity, "invalid reward data"); uint256 _amount = _oracleContract.consumeClaim(_account, _claim); require(_amount > 0, "no claim"); if (_claimInterface == IERC1155_INTERFACE) { IERC1155(_rewardToken).safeTransferFrom(address(this), _account, _rewardTokenId, _amount, ""); } else if (_claimInterface == IERC1155_MINT_INTERFACE) { IERC1155Minter(_rewardToken).mint(_account, _rewardTokenId, _amount, ""); } else if (_claimInterface == IERC20_MINT_INTERFACE) { IERC20Minter(_rewardToken).mint(_account, _amount); } else { IERC20(_rewardToken).safeTransfer(_account, _amount); } emit Claimed(_account, _oracleContract, _claimInterface, _rewardToken, _rewardTokenId, _amount); } }