// SPDX-License-Identifier: MPL-2.0 pragma solidity ^0.8.17; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import {AbstractConditionOracle} from "./AbstractConditionOracle.sol"; /** * @dev Distribute a fixed reward in tokens to holder of ERC721 NFT, each NFT can claim the reward once. */ contract ERC721Holder is AbstractConditionOracle { uint256 NO_REWARD = 119225934112114; // losser in hex IERC721 public nftToken; uint256 public defaultReward; mapping(uint256 => uint256) rewards; mapping(uint256 => bool) disqualifiedNfts; event RewardChanged( uint256 indexed nftId, uint256 indexed amount ); event DefaultRewardChanged( uint256 amount ); event ClaimedNftReward( address indexed account, uint256 indexed nftId, bytes32 indexed integrityHash, uint256 amount, bytes claim ); /** * @dev Expose configuration API for reward engine for ERC721 holder rewards to owner address. * @param _nftToken ERC721 NFT contract address. * @param _rewardToken ERC20 or ERC1155 reward token contract address. * @param _rewardTokenId ERC1155 token id used for reward, pass 0 for ERC20 reward token. * @param _claimInterface interface used to distribute reward tokens, usually mint or transfer. * @param _defaultReward Default reward, unless overriden by adminSetReward. * @param _defaultConsumer address of contract which is authorized to call `consumeClaim`, pass address(0) to configure this later. */ constructor( address _nftToken, bytes4 _claimInterface, address _rewardToken, uint256 _rewardTokenId, uint256 _defaultReward, address _defaultConsumer ) { nftToken = IERC721(_nftToken); _changeSettings(_claimInterface, _rewardToken, _rewardTokenId); if (_defaultReward > 0) defaultReward = _defaultReward; if (_defaultConsumer != address(0)) { canConsumeClaims[_defaultConsumer] = true; emit ConsumerAdded(_defaultConsumer); } } /** * @dev Expose configuration API for reward engine for ERC721 holder rewards to owner address. * @param _nftToken ERC721 NFT contract address. * @param _rewardToken ERC20 or ERC1155 reward token contract address. * @param _rewardTokenId ERC1155 token id used for reward, pass 0 for ERC20 reward token. * @param _claimInterface interface used to distribute reward tokens, usually mint or transfer. */ function adminChangeSettings( address _nftToken, bytes4 _claimInterface, address _rewardToken, uint256 _rewardTokenId ) external onlyOwner { nftToken = IERC721(_nftToken); _changeSettings(_claimInterface, _rewardToken, _rewardTokenId); } /** * @dev Set default reward amount for NFT holding. * @param _amount Reward amount in rewardToken. */ function adminSetDefaultReward(uint256 _amount) external onlyOwner { require(_amount >= 0, "invalid reward"); defaultReward = _amount; emit DefaultRewardChanged(_amount); } /** * @dev Set reward amount for specific NFT id. * @param _amount Reward amount in rewardToken. * @param _nftIds Token ids to recieve reward. */ function adminSetReward(uint256 _amount, uint256[] calldata _nftIds) external onlyOwner { require(_amount >= 0, "invalid reward"); for (uint16 _i = 0; _i < _nftIds.length; _i++) { uint256 _nftId = _nftIds[_i]; if (_amount == 0) { disqualifiedNfts[_nftId] = true; emit RewardChanged(_nftId, 0); } else { rewards[_nftId] = _amount; emit RewardChanged(_nftId, _amount); } } } /** * @dev Prepare claim based on NFT ids. * @param _claimInterface interface used to distribute reward tokens, usually mint or transfer. * @param _token ERC20 or ERC1155 reward token contract address. * @param _tokenId ERC1155 token id used for reward, pass 0 for ERC20 reward token. * @param _nftId NFT id ownec by account. * @return encoded claim. */ function prepareClaim( bytes4 _claimInterface, address _token, uint256 _tokenId, uint256 _nftId ) public pure returns (bytes memory) { bytes32 _integrityHash = keccak256(abi.encodePacked(_token, _tokenId, _claimInterface)); return abi.encode(_integrityHash, abi.encode(_nftId)); } /** * @dev Check if there is reward for account for the claim. * @param _account Account which holds NFTs. * @param _claim ABI encoded integrity hash and NFT id to claim reward. * @return true if claim is valid. */ function hasClaim(address _account, bytes calldata _claim) public view returns (bool) { (bytes32 _integrity, bytes memory _encNftId) = abi.decode(_claim, (bytes32, bytes)); (uint256 _nftId) = abi.decode(_encNftId, (uint256)); if (_integrity != integrityHash) return false; if (nftToken.ownerOf(_nftId) != _account) return false; uint256 _reward = getReward(_nftId); return _reward > 0; } /** * @dev Return reward amount for the claim, invalidating it in a process. * @param _account Account which holds NFTs. * @param _claim ABI encoded integrity hash and NFT id to claim reward. * @return amount of reward in tokens. */ function consumeClaim(address _account, bytes calldata _claim) public returns (uint256) { require(canConsumeClaims[msg.sender], "not a consumer"); (bytes32 _integrity, bytes memory _encNftId) = abi.decode(_claim, (bytes32, bytes)); (uint256 _nftId) = abi.decode(_encNftId, (uint256)); require(_integrity == integrityHash, "invalid claim"); require(nftToken.ownerOf(_nftId) == _account, "not an owner"); uint256 _reward = getReward(_nftId); if (_reward > 0) disqualifiedNfts[_nftId] = true; emit ConsumedClaim(_account, _claim); emit ClaimedNftReward(_account, _nftId, _integrity, _reward, _claim); return _reward; } /** * @dev Get reward amount for specific NFT ids. Default reward will be returned if specific reward is not set. * @param _nftId NFT id to recieve reward for. * @return reward for specific NFT. */ function getReward(uint256 _nftId) public view returns (uint256) { if (disqualifiedNfts[_nftId]) return 0; uint256 _reward = rewards[_nftId]; if (_reward > 0) return _reward; return defaultReward; } }