diff --git a/README.md b/README.md index 2d8e023220ac..7be9cd78e05f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { AccessControlDefaultAdminRules } from + "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol"; +import { PixelDungeonsItems } from "./PixelDungeonsItems.sol"; + +contract PixelDungeonsRewards is AccessControlDefaultAdminRules, ReentrancyGuard, ERC721Holder, ERC1155Holder { + using BitMaps for BitMaps.BitMap; + using SafeERC20 for IERC20; + + bytes32 public constant SENDER_ROLE = keccak256("SENDER_ROLE"); + bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); + + enum Category { + ETHER, + ITEM, + ERC20, + ERC721, + ERC1155 + } + + struct Item { + Category category; + address receiver; + uint256 amount; + uint256 tokenId; // Used for Item, ERC721, ERC1155 rewards + address tokenAddress; // Used for ERC20, ERC721, ERC1155 rewards + } + + struct Reward { + uint256 topic; // game, tournament, referral, etc. + uint256 id; + Item[] items; + } + + address payable public recipient; + PixelDungeonsItems public immutable items; + mapping(uint256 => BitMaps.BitMap) private sentRewards; + + event RewardSent(uint256 indexed topic, uint256 indexed id); + + error InvalidRecipient(address recipient); + + constructor(PixelDungeonsItems _items, address _recipient) AccessControlDefaultAdminRules(2 days, msg.sender) { + items = _items; + recipient = payable(_recipient); + } + + function hasSentReward(uint256 _topic, uint256 _id) public view returns (bool) { + return sentRewards[_topic].get(_id); + } + + function setRecipient(address payable _recipient) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (_recipient == address(0)) { + revert InvalidRecipient(_recipient); + } + + recipient = _recipient; + } + + function sendRewards(Reward[] calldata _rewards) external virtual onlyRole(SENDER_ROLE) nonReentrant { + for (uint256 i = 0; i < _rewards.length; i++) { + Reward calldata reward = _rewards[i]; + + if (!hasSentReward(reward.topic, reward.id)) { + for (uint256 j = 0; j < reward.items.length; j++) { + Item calldata item = reward.items[j]; + + if (item.category == Category.ETHER) { + Address.sendValue(payable(item.receiver), item.amount); + } else if (item.category == Category.ITEM) { + items.mint(item.receiver, item.tokenId, item.amount, ""); + } else if (item.category == Category.ERC20) { + IERC20(item.tokenAddress).safeTransfer(item.receiver, item.amount); + } else if (item.category == Category.ERC721) { + IERC721(item.tokenAddress).safeTransferFrom(address(this), item.receiver, item.tokenId); + } else if (item.category == Category.ERC1155) { + IERC1155(item.tokenAddress).safeTransferFrom(address(this), item.receiver, item.tokenId, item.amount, ""); + } + } + + _markRewardAsSent(reward.topic, reward.id); + } + } + } + + function _markRewardAsSent(uint256 _topic, uint256 _id) internal { + sentRewards[_topic].set(_id); + emit RewardSent(_topic, _id); + } + + function withdrawEther(uint256 _amount) external onlyRole(WITHDRAW_ROLE) { + Address.sendValue(recipient, _amount); + } + + function withdrawERC20(IERC20 _token, uint256 _amount) external onlyRole(WITHDRAW_ROLE) { + _token.safeTransfer(recipient, _amount); + } + + function withdrawERC721(IERC721 _token, uint256 _tokenId) external onlyRole(WITHDRAW_ROLE) { + _token.safeTransferFrom(address(this), recipient, _tokenId); + } + + function withdrawERC1155(IERC1155 _token, uint256 _id, uint256 _amount) external onlyRole(WITHDRAW_ROLE) { + _token.safeTransferFrom(address(this), recipient, _id, _amount, ""); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC1155Holder, AccessControlDefaultAdminRules) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + receive() external payable { } +} # The Solidity Contract-Oriented Programming Language [![Matrix Chat](https://img.shields.io/badge/Matrix%20-chat-brightgreen?style=plastic&logo=matrix)](https://matrix.to/#/#ethereum_solidity:gitter.im) diff --git a/pixel dungeon b/pixel dungeon new file mode 100644 index 000000000000..85b6c79cc6d7 --- /dev/null +++ b/pixel dungeon @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { AccessControlDefaultAdminRulesUpgradeable } from + "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; +import { ERC1155SupplyUpgradeable } from + "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol"; +import { ERC1155Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract PixelDungeonsItems is + Initializable, + ERC1155Upgradeable, + AccessControlDefaultAdminRulesUpgradeable, + ERC1155SupplyUpgradeable, + UUPSUpgradeable +{ + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + string public constant name = "Pixel Dungeons Items"; + string public constant symbol = "PDI"; + + mapping(uint256 => bool) public soulboundTokens; + mapping(uint256 => uint256) public minimumPrices; + + event SoulboundUpdated(uint256 id, bool soulbound); + event MinimumPriceUpdated(uint256 id, uint256 price); + + error InvalidSoulboundTransfer(uint256 id); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() public initializer { + __ERC1155_init("https://api.pixeldungeons.xyz/items/{id}"); + __AccessControlDefaultAdminRules_init(2 days, msg.sender); + __ERC1155Supply_init(); + __UUPSUpgradeable_init(); + } + + function setMinimumPrice(uint256 _id, uint256 _price) external onlyRole(DEFAULT_ADMIN_ROLE) { + minimumPrices[_id] = _price; + emit MinimumPriceUpdated(_id, _price); + } + + function setSoulbound(uint256 _id, bool _soulbound) external onlyRole(DEFAULT_ADMIN_ROLE) { + soulboundTokens[_id] = _soulbound; + emit SoulboundUpdated(_id, _soulbound); + } + + function setTokenURI(string memory _newUri) external onlyRole(DEFAULT_ADMIN_ROLE) { + _setURI(_newUri); + } + + function mint(address account, uint256 id, uint256 amount, bytes memory data) external onlyRole(MINTER_ROLE) { + _mint(account, id, amount, data); + } + + function mintBatch(address[] memory to, uint256[][] memory ids, uint256[][] memory amounts, bytes memory data) + external + onlyRole(MINTER_ROLE) + { + for (uint256 i = 0; i < to.length; i++) { + _mintBatch(to[i], ids[i], amounts[i], data); + } + } + + function burn(address account, uint256 id, uint256 amount) external onlyRole(BURNER_ROLE) { + _burn(account, id, amount); + } + + function burnBatch(address[] memory accounts, uint256[][] memory ids, uint256[][] memory amounts) + external + onlyRole(BURNER_ROLE) + { + for (uint256 i = 0; i < accounts.length; i++) { + _burnBatch(accounts[i], ids[i], amounts[i]); + } + } + + // Disable soul-bound token transfers + function _updateWithAcceptanceCheck( + address from, + address to, + uint256[] memory ids, + uint256[] memory values, + bytes memory data + ) internal virtual override { + if (from != address(0) && to != address(0)) { + // Only check for transfers, not minting or burning + for (uint256 i = 0; i < ids.length; i++) { + if (soulboundTokens[ids[i]]) { + revert InvalidSoulboundTransfer(ids[i]); + } + } + } + super._updateWithAcceptanceCheck(from, to, ids, values, data); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } + + // The following functions are overrides required by Solidity. + function _update(address from, address to, uint256[] memory ids, uint256[] memory values) + internal + override(ERC1155Upgradeable, ERC1155SupplyUpgradeable) + { + super._update(from, to, ids, values); + } + + function supportsInterface(bytes4 interfaceId) + public + view + override(ERC1155Upgradeable, AccessControlDefaultAdminRulesUpgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId); + } +} diff --git a/reaward b/reaward new file mode 100644 index 000000000000..bc33544ff31c --- /dev/null +++ b/reaward @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { AccessControlDefaultAdminRules } from + "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol"; +import { PixelDungeonsItems } from "./PixelDungeonsItems.sol"; + +contract PixelDungeonsRewards is AccessControlDefaultAdminRules, ReentrancyGuard, ERC721Holder, ERC1155Holder { + using BitMaps for BitMaps.BitMap; + using SafeERC20 for IERC20; + + bytes32 public constant SENDER_ROLE = keccak256("SENDER_ROLE"); + bytes32 public constant WITHDRAW_ROLE = keccak256("WITHDRAW_ROLE"); + + enum Category { + ETHER, + ITEM, + ERC20, + ERC721, + ERC1155 + } + + struct Item { + Category category; + address receiver; + uint256 amount; + uint256 tokenId; // Used for Item, ERC721, ERC1155 rewards + address tokenAddress; // Used for ERC20, ERC721, ERC1155 rewards + } + + struct Reward { + uint256 topic; // game, tournament, referral, etc. + uint256 id; + Item[] items; + } + + address payable public recipient; + PixelDungeonsItems public immutable items; + mapping(uint256 => BitMaps.BitMap) private sentRewards; + + event RewardSent(uint256 indexed topic, uint256 indexed id); + + error InvalidRecipient(address recipient); + + constructor(PixelDungeonsItems _items, address _recipient) AccessControlDefaultAdminRules(2 days, msg.sender) { + items = _items; + recipient = payable(_recipient); + } + + function hasSentReward(uint256 _topic, uint256 _id) public view returns (bool) { + return sentRewards[_topic].get(_id); + } + + function setRecipient(address payable _recipient) public onlyRole(DEFAULT_ADMIN_ROLE) { + if (_recipient == address(0)) { + revert InvalidRecipient(_recipient); + } + + recipient = _recipient; + } + + function sendRewards(Reward[] calldata _rewards) external virtual onlyRole(SENDER_ROLE) nonReentrant { + for (uint256 i = 0; i < _rewards.length; i++) { + Reward calldata reward = _rewards[i]; + + if (!hasSentReward(reward.topic, reward.id)) { + for (uint256 j = 0; j < reward.items.length; j++) { + Item calldata item = reward.items[j]; + + if (item.category == Category.ETHER) { + Address.sendValue(payable(item.receiver), item.amount); + } else if (item.category == Category.ITEM) { + items.mint(item.receiver, item.tokenId, item.amount, ""); + } else if (item.category == Category.ERC20) { + IERC20(item.tokenAddress).safeTransfer(item.receiver, item.amount); + } else if (item.category == Category.ERC721) { + IERC721(item.tokenAddress).safeTransferFrom(address(this), item.receiver, item.tokenId); + } else if (item.category == Category.ERC1155) { + IERC1155(item.tokenAddress).safeTransferFrom(address(this), item.receiver, item.tokenId, item.amount, ""); + } + } + + _markRewardAsSent(reward.topic, reward.id); + } + } + } + + function _markRewardAsSent(uint256 _topic, uint256 _id) internal { + sentRewards[_topic].set(_id); + emit RewardSent(_topic, _id); + } + + function withdrawEther(uint256 _amount) external onlyRole(WITHDRAW_ROLE) { + Address.sendValue(recipient, _amount); + } + + function withdrawERC20(IERC20 _token, uint256 _amount) external onlyRole(WITHDRAW_ROLE) { + _token.safeTransfer(recipient, _amount); + } + + function withdrawERC721(IERC721 _token, uint256 _tokenId) external onlyRole(WITHDRAW_ROLE) { + _token.safeTransferFrom(address(this), recipient, _tokenId); + } + + function withdrawERC1155(IERC1155 _token, uint256 _id, uint256 _amount) external onlyRole(WITHDRAW_ROLE) { + _token.safeTransferFrom(address(this), recipient, _id, _amount, ""); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC1155Holder, AccessControlDefaultAdminRules) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + receive() external payable { } +}