Skip to content

Instantly share code, notes, and snippets.

@dvgui
Created March 13, 2025 14:35
Show Gist options
  • Save dvgui/3d33eb1e9c8a07004423a665d40d0df3 to your computer and use it in GitHub Desktop.
Save dvgui/3d33eb1e9c8a07004423a665d40d0df3 to your computer and use it in GitHub Desktop.
MultiChain NFT Store
// 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
);
}
}
// 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);
}
}
// 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