Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CrossChain NFT Example. #118

Open
lukema95 opened this issue Jan 30, 2024 · 3 comments
Open

Add CrossChain NFT Example. #118

lukema95 opened this issue Jan 30, 2024 · 3 comments

Comments

@lukema95
Copy link
Collaborator

lukema95 commented Jan 30, 2024

The contract looks similar to the following:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol";
import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol";


contract OmniChainNFT is ERC721, ZetaInteractor, ZetaReceiver {
    error InvalidMessageType();

    event CrossChainMessageEvent(uint256);
    event CrossChainMessageRevertedEvent(uint256);

    uint256 public immutable CHAIN_ID;

    uint256 public constant MINT_CHAIN_ID = 5;

    bytes32 public constant CROSS_CHAIN_MESSAGE_MESSAGE_TYPE =
        keccak256("CROSS_CHAIN_CROSS_CHAIN_MESSAGE");
    ZetaTokenConsumer private immutable _zetaConsumer;
    IERC20 internal immutable _zetaToken;

    uint256 public constant MAX_SUPPLY = 10000;
    uint256 public totalSupply = 0;

    mapping(uint256 => bool) public frozenTokens; 

    modifier onlyNotFrozen(uint256 _tokenId) {
        require(!frozenTokens[_tokenId], "Token is frozen");
        _;
    }

    constructor(
        address connectorAddress,
        address zetaTokenAddress,
        address zetaConsumerAddress,
        string memory _name, 
        string memory _symbol,
        uint256 _chainId
    ) ZetaInteractor(connectorAddress) ERC721(_name, _symbol){
        _zetaToken = IERC20(zetaTokenAddress);
        _zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress);
        CHAIN_ID = _chainId;
    }

    function sendMessage(
        uint256 destinationChainId,
        uint256 tokenId
    ) external payable onlyNotFrozen(tokenId) {
        if (!_isValidChainId(destinationChainId))
            revert InvalidDestinationChainId();
        
        require(ownerOf(tokenId) == msg.sender, "Not owner of token");

        uint256 crossChainGas = 2 * (10 ** 18);
        uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{
            value: msg.value
        }(address(this), crossChainGas);
        _zetaToken.approve(address(connector), zetaValueAndGas);

        connector.send(
            ZetaInterfaces.SendInput({
                destinationChainId: destinationChainId,
                destinationAddress: interactorsByChainId[destinationChainId],
                destinationGasLimit: 300000,
                message: abi.encode(CROSS_CHAIN_MESSAGE_MESSAGE_TYPE, msg.sender, tokenId),
                zetaValueAndGas: zetaValueAndGas,
                zetaParams: abi.encode("")
            })
        );
        
        // Freeze the token
        freeze(tokenId);
    }

    function onZetaMessage(
        ZetaInterfaces.ZetaMessage calldata zetaMessage
    ) external override isValidMessageCall(zetaMessage) {
        (bytes32 messageType, address sender, uint256 tokenId) = abi.decode(
            zetaMessage.message,
            (bytes32, address, uint256)
        );

        if (messageType != CROSS_CHAIN_MESSAGE_MESSAGE_TYPE)
            revert InvalidMessageType();

        // If the token is frozen, unfreeze it
        // Otherwise, mint the token to the user
        if (isFrozen(tokenId)) {
            unfreeze(sender, tokenId);
        } else {
            _safeMint(sender, tokenId);
        }

        emit CrossChainMessageEvent(tokenId);
    }

    function onZetaRevert(
        ZetaInterfaces.ZetaRevert calldata zetaRevert
    ) external override isValidRevertCall(zetaRevert) {
        (bytes32 messageType, address sender, uint256 tokenId) = abi.decode(
            zetaRevert.message,
            (bytes32, address, uint256)
        );

        if (messageType != CROSS_CHAIN_MESSAGE_MESSAGE_TYPE)
            revert InvalidMessageType();

        // unfreeze the token
        unfreeze(sender, tokenId);

        emit CrossChainMessageRevertedEvent(tokenId);
    }

    function mint() external {
        // Only allow minting on the MINT_CHAIN_ID, because we want to avoid minting on multiple chains
        require(CHAIN_ID == MINT_CHAIN_ID, "Only allow minting on the MINT_CHAIN_ID");
        uint256 _tokenId = totalSupply;
        require(_tokenId < MAX_SUPPLY, "Max supply reached");
        _safeMint(msg.sender, _tokenId);
        totalSupply++;
    }

    function burn(uint256 _tokenId) external onlyNotFrozen(_tokenId) {
        require(ownerOf(_tokenId) == msg.sender, "Not owner of token");
        _burn(_tokenId);
    }

    function transferFrom(address from, address to, uint256 tokenId) public virtual override onlyNotFrozen(tokenId) {
        super.transferFrom(from, to, tokenId);
    }

    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override onlyNotFrozen(tokenId) {
        require(!frozenTokens[tokenId], "Token is frozen");
        super.safeTransferFrom(from, to, tokenId);
    }

    function approve(address to, uint256 tokenId) public virtual override onlyNotFrozen(tokenId) {
        super.approve(to, tokenId);
    }

    function freeze(uint256 _tokenId) internal onlyNotFrozen(_tokenId) {
        require(ownerOf(_tokenId) == msg.sender, "Not owner of token");
        // delete _tokenApprovals[tokenId];
        frozenTokens[_tokenId] = true;
    }

    function unfreeze(address to, uint256 _tokenId) internal {
        require(frozenTokens[_tokenId], "Token already unfrozen");
        frozenTokens[_tokenId] = false;
        
        address owner = ownerOf(_tokenId);
        _transfer(owner, to, _tokenId);
    }

    function isFrozen(uint256 _tokenId) public view returns (bool) {
        return frozenTokens[_tokenId];
    }
}
@fadeev
Copy link
Member

fadeev commented Mar 21, 2024

If the same contract is deployed on multiple chains, we should make sure that the mint operation can only be performed on a certain chain, otherwise, it will lead to tokenId conflicts on multiple chains. I suggest adding the following judgment:

@lukema95 in order to prevent chain ID from clashing, instead of limiting minting to a single chain, I think what we can do is add an integer to the constructor, which is different for each chain and prefix IDs with this integer.

NFT IDs on Polygon: 10000001, 10000002, 10000003...
BSC: 20000001, 20000002, 20000003...

@fadeev
Copy link
Member

fadeev commented Mar 21, 2024

In the process of NFT cross-chain, I noticed that the current contract directly destroyed the tokens of the source chain, in which case, the original ownership of the tokenID could not be found on the source chain, so I suggested freezing instead of destroying, for example:

@lukema95 I agree. I think we can freeze NFTs. This could be a first step in the tutorial. The second step would be to send an acknowledgement (that the NFT has been minted on the destination chain) back from destination chain back to source chain to burn the frozen NFT.

@lukema95
Copy link
Collaborator Author

If the same contract is deployed on multiple chains, we should make sure that the mint operation can only be performed on a certain chain, otherwise, it will lead to tokenId conflicts on multiple chains. I suggest adding the following judgment:

@lukema95 in order to prevent chain ID from clashing, instead of limiting minting to a single chain, I think what we can do is add an integer to the constructor, which is different for each chain and prefix IDs with this integer.

NFT IDs on Polygon: 10000001, 10000002, 10000003... BSC: 20000001, 20000002, 20000003...

Another problem is that if mint is performed on multiple chains, there is no way to control the total supply of NFTS because there is no way to synchronize the total supply of NFTS on different chains.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants