Created
March 13, 2025 14:35
-
-
Save dvgui/3d33eb1e9c8a07004423a665d40d0df3 to your computer and use it in GitHub Desktop.
MultiChain NFT Store
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: Unlicensed | |
pragma solidity ^0.8.19; | |
import "@openzeppelin/contracts/security/Pausable.sol"; | |
import "@openzeppelin/contracts/access/AccessControl.sol"; | |
import "@openzeppelin/contracts/utils/Address.sol"; | |
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | |
import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; | |
/// @custom:security-contact [email protected] | |
contract DanteStoreCC is Pausable, AccessControl { | |
using Address for address payable; | |
using SafeERC20 for IERC20Permit; | |
using SafeERC20 for IERC20; | |
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); | |
bytes32 public constant STORE_ADMIN_ROLE = keccak256("STORE_ADMIN_ROLE"); | |
event PurchaseHero(address indexed sender, uint hero); | |
event PurchaseItem(address indexed sender, uint item, uint amount); | |
event TreasuryChanged(address indexed treasury); | |
struct Price { | |
address token; | |
uint amount; | |
} | |
struct Hero { | |
bool enabled; | |
Price currency0; | |
Price currency1; | |
} | |
struct Item { | |
bool enabled; | |
Price currency0; | |
Price currency1; | |
uint id; | |
} | |
struct Signature { | |
uint8 v; | |
bytes32 r; | |
bytes32 s; | |
} | |
mapping(uint => Hero) public heroes; | |
mapping(uint => Item) public items; | |
address public treasuryAddress; | |
address public storeReceiver; | |
IWormholeRelayer public wormholeRelayer; | |
uint constant GAS_LIMIT = 250_000; | |
uint16 public immutable targetChain; // BSC Testnet | |
constructor( | |
address _wormholeRelayer, | |
address _treasuryAddress, | |
address _storeReceiver, | |
uint16 _targetChain | |
) { | |
require(_treasuryAddress != address(0), "Invalid Treasury Address"); | |
require( | |
_wormholeRelayer != address(0), | |
"Invalid Wormhole Relayer Address" | |
); | |
require(_storeReceiver != address(0), "Invalid Store Receiver Address"); | |
require(_targetChain != 0, "Invalid Target Chain"); | |
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender); | |
_grantRole(PAUSER_ROLE, msg.sender); | |
_grantRole(STORE_ADMIN_ROLE, msg.sender); | |
wormholeRelayer = IWormholeRelayer(_wormholeRelayer); | |
treasuryAddress = _treasuryAddress; | |
targetChain = _targetChain; | |
storeReceiver = _storeReceiver; | |
} | |
function setTreasury( | |
address _treasuryAddress | |
) external onlyRole(STORE_ADMIN_ROLE) { | |
require(_treasuryAddress != address(0), "Invalid Treasury Address"); | |
treasuryAddress = _treasuryAddress; | |
emit TreasuryChanged(_treasuryAddress); | |
} | |
function pause() external onlyRole(PAUSER_ROLE) { | |
_pause(); | |
} | |
function unpause() external onlyRole(PAUSER_ROLE) { | |
_unpause(); | |
} | |
function getBalance() external view returns (uint) { | |
return address(this).balance; | |
} | |
function quoteCrossChainCost() public view returns (uint256 cost) { | |
(cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( | |
targetChain, | |
0, | |
GAS_LIMIT | |
); | |
} | |
function claimBalance() external onlyRole(STORE_ADMIN_ROLE) { | |
payable(treasuryAddress).sendValue(address(this).balance); | |
} | |
function addHero( | |
Hero calldata _hero, | |
uint _id | |
) external onlyRole(STORE_ADMIN_ROLE) { | |
require(!heroes[_id].enabled, "Hero Already Exists"); | |
require( | |
_hero.currency0.token != _hero.currency1.token, | |
"Duplicate Payment Tokens" | |
); | |
heroes[_id] = _hero; | |
} | |
function addItem( | |
Item calldata _item, | |
uint _id | |
) external onlyRole(STORE_ADMIN_ROLE) { | |
require(!items[_id].enabled, "Item Already Exists"); | |
require( | |
_item.currency0.token != _item.currency1.token, | |
"Duplicate Payment Tokens" | |
); | |
items[_id] = _item; | |
} | |
function removeHero(uint _id) external onlyRole(STORE_ADMIN_ROLE) { | |
delete heroes[_id]; | |
} | |
function removeItem(uint _id) external onlyRole(STORE_ADMIN_ROLE) { | |
delete items[_id]; | |
} | |
function buyHeroPermit( | |
uint _id, | |
uint deadline, | |
Signature calldata signature0, | |
Signature calldata signature1 | |
) external payable whenNotPaused { | |
Hero storage hero = heroes[_id]; | |
require(hero.enabled, "Invalid Hero"); | |
_buyHeroPermit(_id, deadline, signature0, signature1); | |
emit PurchaseHero(msg.sender, _id); | |
} | |
function buyItemPermit( | |
uint _id, | |
uint amount, | |
uint deadline, | |
Signature calldata signature0, | |
Signature calldata signature1 | |
) external payable whenNotPaused { | |
Item storage item = items[_id]; | |
require(item.enabled, "Invalid Item"); | |
_buyItemPermit(_id, amount, deadline, signature0, signature1); | |
emit PurchaseItem(msg.sender, _id, amount); | |
} | |
function _buyHeroPermit( | |
uint _id, | |
uint deadline, | |
Signature calldata signature0, | |
Signature calldata signature1 | |
) internal { | |
Hero storage hero = heroes[_id]; | |
uint256 cost = quoteCrossChainCost(); | |
if ( | |
hero.currency0.token != address(0) && | |
hero.currency1.token != address(0) | |
) { | |
require(msg.value >= cost, "Invalid Native Amount"); | |
} | |
if (hero.currency0.token == address(0)) { | |
require( | |
hero.currency0.amount + cost <= msg.value, | |
"Insufficient Value" | |
); | |
} else { | |
IERC20Permit(hero.currency0.token).safePermit( | |
msg.sender, | |
address(this), | |
hero.currency0.amount, | |
deadline, | |
signature0.v, | |
signature0.r, | |
signature0.s | |
); | |
IERC20(hero.currency0.token).safeTransferFrom( | |
msg.sender, | |
treasuryAddress, | |
hero.currency0.amount | |
); | |
} | |
if (hero.currency1.token == address(0)) { | |
require( | |
hero.currency1.amount + cost <= msg.value, | |
"Insufficient Value" | |
); | |
} else { | |
IERC20Permit(hero.currency1.token).safePermit( | |
msg.sender, | |
address(this), | |
hero.currency1.amount, | |
deadline, | |
signature1.v, | |
signature1.r, | |
signature1.s | |
); | |
IERC20(hero.currency1.token).safeTransferFrom( | |
msg.sender, | |
treasuryAddress, | |
hero.currency1.amount | |
); | |
} | |
sendCCMint(0, uint16(_id), 1, cost); | |
} | |
function _buyItemPermit( | |
uint _id, | |
uint amount, | |
uint deadline, | |
Signature calldata signature0, | |
Signature calldata signature1 | |
) internal { | |
Item storage item = items[_id]; | |
uint256 cost = quoteCrossChainCost(); | |
if ( | |
item.currency0.token != address(0) && | |
item.currency1.token != address(0) | |
) { | |
require(msg.value >= cost, "Invalid Native Amount"); | |
} | |
if (item.currency0.token == address(0)) { | |
require( | |
item.currency0.amount * amount + cost >= msg.value, | |
"Insufficient Value" | |
); | |
} else { | |
IERC20Permit(item.currency0.token).safePermit( | |
msg.sender, | |
address(this), | |
item.currency0.amount * amount, | |
deadline, | |
signature0.v, | |
signature0.r, | |
signature0.s | |
); | |
IERC20(item.currency0.token).safeTransferFrom( | |
msg.sender, | |
treasuryAddress, | |
item.currency0.amount * amount | |
); | |
} | |
if (item.currency1.token == address(0)) { | |
require( | |
item.currency1.amount * amount + cost >= msg.value, | |
"Insufficient Value" | |
); | |
} else { | |
IERC20Permit(item.currency1.token).safePermit( | |
msg.sender, | |
address(this), | |
item.currency1.amount * amount, | |
deadline, | |
signature1.v, | |
signature1.r, | |
signature1.s | |
); | |
IERC20(item.currency1.token).safeTransferFrom( | |
msg.sender, | |
treasuryAddress, | |
item.currency1.amount * amount | |
); | |
} | |
sendCCMint(1, uint16(item.id), amount, cost); | |
} | |
function sendCCMint( | |
uint8 nftType, | |
uint16 nftId, | |
uint amount, | |
uint cost | |
) internal { | |
wormholeRelayer.sendPayloadToEvm{value: cost}( | |
targetChain, | |
storeReceiver, | |
abi.encode(msg.sender, nftType, nftId, amount), | |
0, // No receiver value needed | |
GAS_LIMIT | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.19; | |
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; | |
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; | |
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; | |
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; | |
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; | |
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | |
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; | |
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; | |
/// @custom:security-contact [email protected] | |
contract HeroNFT is | |
Initializable, | |
ERC721Upgradeable, | |
ERC721EnumerableUpgradeable, | |
PausableUpgradeable, | |
AccessControlUpgradeable, | |
ERC721BurnableUpgradeable, | |
UUPSUpgradeable | |
{ | |
using CountersUpgradeable for CountersUpgradeable.Counter; | |
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); | |
bytes32 public constant WHITELIST_ROLE = keccak256("WHITELIST_ROLE"); | |
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); | |
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); | |
CountersUpgradeable.Counter private _tokenIdCounter; | |
bool public whitelistEnabled; | |
mapping(uint => uint) public tokenTypes; | |
mapping(address => bool) public whitelistAddress; | |
mapping(uint => CountersUpgradeable.Counter) public typeSupply; | |
mapping(uint => uint) public cap; | |
event Mint(address indexed owner, uint tokenId, uint tokenType); | |
event Whitelist(bool enabled); | |
event AddedWhitelist(address _address); | |
event RemovedWhitelist(address _address); | |
event CapUpdated(uint tokenType, uint newCap); | |
/// @custom:oz-upgrades-unsafe-allow constructor | |
constructor() { | |
_disableInitializers(); | |
} | |
function initialize( | |
string memory _name, | |
string memory _symbol | |
) public initializer { | |
__ERC721_init(_name, _symbol); | |
__ERC721Enumerable_init(); | |
__Pausable_init(); | |
__AccessControl_init(); | |
__ERC721Burnable_init(); | |
__UUPSUpgradeable_init(); | |
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender); | |
_grantRole(PAUSER_ROLE, msg.sender); | |
_grantRole(WHITELIST_ROLE, msg.sender); | |
_grantRole(MINTER_ROLE, msg.sender); | |
_grantRole(UPGRADER_ROLE, msg.sender); | |
whitelistEnabled = true; | |
whitelistAddress[address(0)] = true; | |
} | |
function _baseURI() internal pure override returns (string memory) { | |
return "https://edge.dev.dantegames.com/nft/api/p/nft/hero/"; | |
} | |
function pause() external onlyRole(PAUSER_ROLE) { | |
_pause(); | |
} | |
function unpause() external onlyRole(PAUSER_ROLE) { | |
_unpause(); | |
} | |
function setWhitelistEnabled( | |
bool enabled | |
) external onlyRole(WHITELIST_ROLE) { | |
whitelistEnabled = enabled; | |
emit Whitelist(enabled); | |
} | |
function addWhitelist(address _address) external onlyRole(WHITELIST_ROLE) { | |
whitelistAddress[_address] = true; | |
emit AddedWhitelist(_address); | |
} | |
function removeWhitelist( | |
address _address | |
) external onlyRole(WHITELIST_ROLE) { | |
delete whitelistAddress[_address]; | |
emit RemovedWhitelist(_address); | |
} | |
function updateCap( | |
uint tokenType, | |
uint newCap | |
) external onlyRole(WHITELIST_ROLE) { | |
require( | |
newCap > typeSupply[tokenType].current(), | |
"Cap is smaller than supply" | |
); | |
cap[tokenType] = newCap; | |
emit CapUpdated(tokenType, newCap); | |
} | |
function safeMint(address to, uint tokenType) public onlyRole(MINTER_ROLE) { | |
CountersUpgradeable.Counter storage _tokenType = typeSupply[tokenType]; | |
require( | |
cap[tokenType] == 0 || cap[tokenType] > _tokenType.current(), | |
"Hero Cap reached for that type" | |
); | |
uint tokenId = _tokenIdCounter.current(); | |
_tokenIdCounter.increment(); | |
tokenTypes[tokenId] = tokenType; | |
_tokenType.increment(); | |
_safeMint(to, tokenId); | |
emit Mint(to, tokenId, tokenType); | |
} | |
function _beforeTokenTransfer( | |
address from, | |
address to, | |
uint tokenId, | |
uint batchSize | |
) | |
internal | |
override(ERC721Upgradeable, ERC721EnumerableUpgradeable) | |
whenNotPaused | |
{ | |
if (whitelistEnabled) { | |
require( | |
whitelistAddress[from] || whitelistAddress[to], | |
"Transfer Not Allowed" | |
); | |
} | |
super._beforeTokenTransfer(from, to, tokenId, batchSize); | |
} | |
function _authorizeUpgrade( | |
address newImplementation | |
) internal override onlyRole(UPGRADER_ROLE) {} | |
// The following functions are overrides required by Solidity. | |
function supportsInterface( | |
bytes4 interfaceId | |
) | |
public | |
view | |
override( | |
ERC721Upgradeable, | |
ERC721EnumerableUpgradeable, | |
AccessControlUpgradeable | |
) | |
returns (bool) | |
{ | |
return super.supportsInterface(interfaceId); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.18; | |
import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; | |
import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol"; | |
import "./interfaces/IHeroNFT.sol"; | |
import "./interfaces/IDanteItem.sol"; | |
contract StoreReceiver is IWormholeReceiver { | |
IWormholeRelayer public wormholeRelayer; | |
address public registrationOwner; | |
address public immutable heroAddress; | |
address public immutable itemAddress; | |
// Mapping to store registered senders for each chain | |
mapping(uint16 => bytes32) public registeredSenders; | |
enum NFTType { | |
HERO, | |
ITEM | |
} | |
event PurchaseReceived( | |
address buyer, | |
uint8 nftType, | |
uint16 id, | |
uint amount | |
); | |
event SourceChainLogged(uint16 sourceChain); | |
constructor( | |
address _wormholeRelayer, | |
address _heroAddress, | |
address _itemAddress | |
) { | |
wormholeRelayer = IWormholeRelayer(_wormholeRelayer); | |
registrationOwner = msg.sender; // Set contract deployer as the owner | |
heroAddress = _heroAddress; | |
itemAddress = _itemAddress; | |
} | |
// Modifier to check if the sender is registered for the source chain | |
modifier isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) { | |
require( | |
registeredSenders[sourceChain] == sourceAddress, | |
"Not registered sender" | |
); | |
_; | |
} | |
// Function to register the valid sender address for a specific chain | |
function setRegisteredSender( | |
uint16 sourceChain, | |
bytes32 sourceAddress | |
) public { | |
require( | |
msg.sender == registrationOwner, | |
"Not allowed to set registered sender" | |
); | |
registeredSenders[sourceChain] = sourceAddress; | |
} | |
// Update receiveWormholeMessages to include the source address check | |
function receiveWormholeMessages( | |
bytes memory payload, | |
bytes[] memory, // additional VAAs (optional, not needed here) | |
bytes32 sourceAddress, | |
uint16 sourceChain, | |
bytes32 // delivery hash | |
) public payable override isRegisteredSender(sourceChain, sourceAddress) { | |
require( | |
msg.sender == address(wormholeRelayer), | |
"Only the Wormhole relayer can call this function" | |
); | |
// Decode the payload | |
(address buyer, uint8 nftType, uint16 nftId, uint amount) = abi.decode( | |
payload, | |
(address, uint8, uint16, uint) | |
); | |
if (nftType == 0) { | |
IHeroNFT(heroAddress).safeMint(buyer, nftId); | |
} else { | |
IDanteItemNFT(itemAddress).mint(buyer, nftId, amount, ""); | |
} | |
// Example use of sourceChain for logging | |
if (sourceChain != 0) { | |
emit SourceChainLogged(sourceChain); | |
} | |
// Emit an event with the received message | |
emit PurchaseReceived(buyer, nftType, nftId, amount); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment