Skip to content

Commit

Permalink
NFT: improvements to make production-ready (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
fadeev authored Nov 8, 2024
1 parent 4b324d3 commit 9a852e6
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 63 deletions.
61 changes: 38 additions & 23 deletions examples/nft/contracts/Connected.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,90 +4,104 @@ pragma solidity 0.8.26;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol";

contract Connected is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
import "./shared/Events.sol";

contract Connected is
ERC721,
ERC721Enumerable,
ERC721URIStorage,
Ownable2Step,
Events
{
GatewayEVM public immutable gateway;
uint256 private _nextTokenId;
address public counterparty;

error InvalidAddress();
error Unauthorized();

function setCounterparty(address contractAddress) external onlyOwner {
if (contractAddress == address(0)) revert InvalidAddress();
counterparty = contractAddress;
emit SetCounterparty(contractAddress);
}

modifier onlyGateway() {
require(msg.sender == address(gateway), "Caller is not the gateway");
if (msg.sender != address(gateway)) revert Unauthorized();
_;
}

constructor(
address payable gatewayAddress,
address initialOwner
) ERC721("MyToken", "MTK") Ownable(initialOwner) {
address owner,
string memory name,
string memory symbol
) ERC721(name, symbol) Ownable(owner) {
if (gatewayAddress == address(0) || owner == address(0))
revert InvalidAddress();
gateway = GatewayEVM(gatewayAddress);
}

function safeMint(address to, string memory uri) public onlyOwner {
uint256 hash = uint256(
keccak256(
abi.encodePacked(address(this), block.number, _nextTokenId++)
)
);
if (to == address(0)) revert InvalidAddress();

uint256 tokenId = hash & 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
uint256 tokenId = _nextTokenId++;

_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
emit TokenMinted(to, tokenId, uri);
}

function transferCrossChain(
uint256 tokenId,
address receiver,
address destination
) external payable {
if (receiver == address(0)) revert InvalidAddress();

string memory uri = tokenURI(tokenId);
_burn(tokenId);
bytes memory encodedData = abi.encode(
tokenId,
receiver,
uri,
destination
);
bytes memory message = abi.encode(tokenId, receiver, uri, destination);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
encodedData,
message,
0
);

if (destination == address(0)) {
gateway.call(counterparty, encodedData, revertOptions);
gateway.call(counterparty, message, revertOptions);
} else {
gateway.depositAndCall{value: msg.value}(
counterparty,
encodedData,
message,
revertOptions
);
}

emit TokenTransfer(tokenId, receiver, destination, uri);
}

function onCall(
MessageContext calldata messageContext,
MessageContext calldata context,
bytes calldata message
) external payable onlyGateway returns (bytes4) {
if (messageContext.sender != counterparty) revert("Unauthorized");
if (context.sender != counterparty) revert Unauthorized();

(uint256 tokenId, address receiver, string memory uri) = abi.decode(
message,
(uint256, address, string)
);

_safeMint(receiver, tokenId);
_setTokenURI(tokenId, uri);
emit TokenTransferReceived(tokenId, receiver, uri);
return "";
}

Expand All @@ -99,6 +113,7 @@ contract Connected is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {

_safeMint(sender, tokenId);
_setTokenURI(tokenId, uri);
emit TokenTransferReverted(tokenId, sender, uri);
}

receive() external payable {}
Expand Down
44 changes: 30 additions & 14 deletions examples/nft/contracts/Universal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,55 @@ pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import {RevertContext, RevertOptions} from "@zetachain/protocol-contracts/contracts/Revert.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/UniversalContract.sol";
import "@zetachain/protocol-contracts/contracts/zevm/interfaces/IGatewayZEVM.sol";
import "@zetachain/protocol-contracts/contracts/zevm/GatewayZEVM.sol";
import {SwapHelperLib} from "@zetachain/toolkit/contracts/SwapHelperLib.sol";
import {SystemContract} from "@zetachain/toolkit/contracts/SystemContract.sol";
import "./shared/Events.sol";

contract Universal is
ERC721,
ERC721Enumerable,
ERC721URIStorage,
Ownable,
UniversalContract
Ownable2Step,
UniversalContract,
Events
{
GatewayZEVM public immutable gateway;
SystemContract public immutable systemContract =
SystemContract(0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9);
uint256 private _nextTokenId;
bool public isUniversal = true;
uint256 public gasLimit = 700000;
uint256 public gasLimit;

error TransferFailed();
error Unauthorized();
error InvalidAddress();
error InvalidGasLimit();

mapping(address => bytes) public counterparty;

event CounterpartySet(address indexed zrc20, bytes indexed contractAddress);

modifier onlyGateway() {
require(msg.sender == address(gateway), "Caller is not the gateway");
if (msg.sender != address(gateway)) revert Unauthorized();
_;
}

constructor(
address payable gatewayAddress,
address initialOwner
) ERC721("MyToken", "MTK") Ownable(initialOwner) {
address owner,
string memory name,
string memory symbol,
uint256 gas
) ERC721(name, symbol) Ownable(owner) {
if (gatewayAddress == address(0) || owner == address(0))
revert InvalidAddress();
if (gas == 0) revert InvalidGasLimit();
gateway = GatewayZEVM(gatewayAddress);
gasLimit = gas;
}

function setCounterparty(
Expand All @@ -58,6 +68,7 @@ contract Universal is
address receiver,
address destination
) public {
if (receiver == address(0)) revert InvalidAddress();
string memory uri = tokenURI(tokenId);
_burn(tokenId);

Expand All @@ -68,25 +79,27 @@ contract Universal is
!IZRC20(destination).transferFrom(msg.sender, address(this), gasFee)
) revert TransferFailed();
IZRC20(destination).approve(address(gateway), gasFee);
bytes memory encodedData = abi.encode(tokenId, receiver, uri);
bytes memory message = abi.encode(tokenId, receiver, uri);

CallOptions memory callOptions = CallOptions(gasLimit, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
encodedData,
message,
gasLimit
);

gateway.call(
counterparty[destination],
destination,
encodedData,
message,
callOptions,
revertOptions
);

emit TokenTransfer(tokenId, receiver, destination, uri);
}

function safeMint(address to, string memory uri) public onlyOwner {
Expand Down Expand Up @@ -121,9 +134,10 @@ contract Universal is
if (destination == address(0)) {
_safeMint(sender, tokenId);
_setTokenURI(tokenId, uri);
emit TokenTransferReceived(tokenId, sender, uri);
} else {
(, uint256 gasFee) = IZRC20(destination).withdrawGasFeeWithGasLimit(
700000
gasLimit
);

SwapHelperLib.swapExactTokensForTokens(
Expand All @@ -139,9 +153,10 @@ contract Universal is
counterparty[destination],
destination,
abi.encode(tokenId, sender, uri),
CallOptions(700000, false),
CallOptions(gasLimit, false),
RevertOptions(address(0), false, address(0), "", 0)
);
emit TokenTransferToDestination(tokenId, sender, destination, uri);
}
}

Expand All @@ -153,6 +168,7 @@ contract Universal is

_safeMint(sender, tokenId);
_setTokenURI(tokenId, uri);
emit TokenTransferReverted(tokenId, sender, uri);
}

// The following functions are overrides required by Solidity.
Expand Down
30 changes: 30 additions & 0 deletions examples/nft/contracts/shared/Events.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract Events {
event SetCounterparty(address indexed newCounterparty);
event TokenMinted(address indexed to, uint256 indexed tokenId, string uri);
event TokenTransfer(
uint256 indexed tokenId,
address indexed receiver,
address indexed destination,
string uri
);
event TokenTransferReceived(
uint256 indexed tokenId,
address indexed receiver,
string uri
);
event TokenTransferReverted(
uint256 indexed tokenId,
address indexed sender,
string uri
);
event CounterpartySet(address indexed zrc20, bytes indexed contractAddress);
event TokenTransferToDestination(
uint256 indexed tokenId,
address indexed sender,
address indexed destination,
string uri
);
}
26 changes: 12 additions & 14 deletions examples/nft/scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

set -e

if [ "$1" = "localnet" ]; then
npx hardhat localnet --exit-on-error & sleep 10
fi
if [ "$1" = "localnet" ]; then npx hardhat localnet --exit-on-error & sleep 10; fi

function nft_balance() {
function balance() {
local ZETACHAIN=$(cast call "$CONTRACT_ZETACHAIN" "balanceOf(address)(uint256)" "$SENDER")
local ETHEREUM=$(cast call "$CONTRACT_ETHEREUM" "balanceOf(address)(uint256)" "$SENDER")
local BNB=$(cast call "$CONTRACT_BNB" "balanceOf(address)(uint256)" "$SENDER")
echo -e "\n🖼️ NFT Balance"
echo "---------------------------------------------"
echo "🟢 ZetaChain: $ZETACHAIN"
echo "🔵 EVM Chain: $ETHEREUM"
echo "🔵 Ethereum: $ETHEREUM"
echo "🟡 BNB Chain: $BNB"
echo "---------------------------------------------"
}
Expand All @@ -27,11 +25,11 @@ GATEWAY_ETHEREUM=$(jq -r '.addresses[] | select(.type=="gatewayEVM" and .chain==
GATEWAY_BNB=$(jq -r '.addresses[] | select(.type=="gatewayEVM" and .chain=="bnb") | .address' localnet.json)
SENDER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

CONTRACT_ZETACHAIN=$(npx hardhat deploy --network localhost --json | jq -r '.contractAddress')
CONTRACT_ZETACHAIN=$(npx hardhat deploy --network localhost --json --gas-limit 1000000 | jq -r '.contractAddress')
echo -e "\n🚀 Deployed NFT contract on ZetaChain: $CONTRACT_ZETACHAIN"

CONTRACT_ETHEREUM=$(npx hardhat deploy --name Connected --json --network localhost --gateway "$GATEWAY_ETHEREUM" | jq -r '.contractAddress')
echo -e "🚀 Deployed NFT contract on EVM chain: $CONTRACT_ETHEREUM"
echo -e "🚀 Deployed NFT contract on Ethereum: $CONTRACT_ETHEREUM"

CONTRACT_BNB=$(npx hardhat deploy --name Connected --json --network localhost --gateway "$GATEWAY_BNB" | jq -r '.contractAddress')
echo -e "🚀 Deployed NFT contract on BNB chain: $CONTRACT_BNB"
Expand All @@ -45,30 +43,30 @@ npx hardhat universal-set-counterparty --network localhost --contract "$CONTRACT
npx hardhat universal-set-counterparty --network localhost --contract "$CONTRACT_ZETACHAIN" --counterparty "$CONTRACT_BNB" --zrc20 "$ZRC20_BNB" --json &>/dev/null

npx hardhat localnet-check
nft_balance
balance

NFT_ID=$(npx hardhat mint --network localhost --json --contract "$CONTRACT_ZETACHAIN" --token-uri https://example.com/nft/metadata/1 | jq -r '.tokenId')
echo -e "\nMinted NFT with ID: $NFT_ID on ZetaChain."

npx hardhat localnet-check
nft_balance
balance

echo -e "\nTransferring NFT: ZetaChain → Ethereum..."
npx hardhat transfer --network localhost --json --token-id "$NFT_ID" --from "$CONTRACT_ZETACHAIN" --to "$ZRC20_ETHEREUM"
npx hardhat transfer --network localhost --json --token-id "$NFT_ID" --from "$CONTRACT_ZETACHAIN" --to "$ZRC20_ETHEREUM"

npx hardhat localnet-check
nft_balance
balance

echo -e "\nTransferring NFT: Ethereum → BNB..."
npx hardhat transfer --network localhost --json --token-id "$NFT_ID" --from "$CONTRACT_ETHEREUM" --to "$ZRC20_BNB" --gas-amount 0.1

npx hardhat localnet-check
nft_balance
balance

echo -e "\nTransferring NFT: BNB → ZetaChain..."
npx hardhat transfer --network localhost --json --token-id "$NFT_ID" --from "$CONTRACT_BNB"

npx hardhat localnet-check
nft_balance
balance

npx hardhat localnet-stop
if [ "$1" = "localnet" ]; then npx hardhat localnet-stop; fi
Loading

0 comments on commit 9a852e6

Please sign in to comment.