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

refactor: extracted core logic into an abstract UniversalNFTCore contract #36

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
151 changes: 25 additions & 126 deletions contracts/nft/contracts/evm/UniversalNFT.sol
Original file line number Diff line number Diff line change
@@ -1,45 +1,30 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {ERC721BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import {ERC721PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721PausableUpgradeable.sol";
import {ERC721URIStorageUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import {ERC721BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {ERC721PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721PausableUpgradeable.sol";

import "../shared/UniversalNFTEvents.sol";
import "./UniversalNFTCore.sol";

contract UniversalNFT is
Initializable,
ERC721Upgradeable,
ERC721URIStorageUpgradeable,
ERC721EnumerableUpgradeable,
ERC721URIStorageUpgradeable,
ERC721PausableUpgradeable,
OwnableUpgradeable,
ERC721BurnableUpgradeable,
UUPSUpgradeable,
UniversalNFTEvents
UniversalNFTCore
{
GatewayEVM public gateway;
uint256 private _nextTokenId;
address public universal;
uint256 public gasLimitAmount;

error InvalidAddress();
error Unauthorized();
error InvalidGasLimit();
error GasTokenTransferFailed();

modifier onlyGateway() {
if (msg.sender != address(gateway)) revert Unauthorized();
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand All @@ -56,30 +41,17 @@ contract UniversalNFT is
__ERC721_init(name, symbol);
__ERC721Enumerable_init();
__ERC721URIStorage_init();
__ERC721Pausable_init();
__Ownable_init(initialOwner);
__ERC721Burnable_init();
__UUPSUpgradeable_init();
if (gatewayAddress == address(0)) revert InvalidAddress();
if (gas == 0) revert InvalidGasLimit();
gasLimitAmount = gas;
gateway = GatewayEVM(gatewayAddress);
}

function setGasLimit(uint256 gas) external onlyOwner {
if (gas == 0) revert InvalidGasLimit();
gasLimitAmount = gas;
}

function setUniversal(address contractAddress) external onlyOwner {
if (contractAddress == address(0)) revert InvalidAddress();
universal = contractAddress;
emit SetUniversal(contractAddress);
__UniversalNFTCore_init(gatewayAddress, address(this), gas);
}

function safeMint(
address to,
string memory uri
) public whenNotPaused onlyOwner {
) public onlyOwner whenNotPaused {
uint256 hash = uint256(
keccak256(
abi.encodePacked(address(this), block.number, _nextTokenId++)
Expand All @@ -90,83 +62,19 @@ contract UniversalNFT is

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

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

string memory uri = tokenURI(tokenId);
_burn(tokenId);
bytes memory message = abi.encode(
destination,
receiver,
tokenId,
uri,
msg.sender
);
if (destination == address(0)) {
gateway.call(
universal,
message,
RevertOptions(address(this), false, address(0), message, 0)
);
} else {
gateway.depositAndCall{value: msg.value}(
universal,
message,
RevertOptions(
address(this),
true,
address(0),
abi.encode(receiver, tokenId, uri, msg.sender),
gasLimitAmount
)
);
}

emit TokenTransfer(destination, receiver, tokenId, uri);
function pause() public onlyOwner {
_pause();
}

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

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

_safeMint(receiver, tokenId);
_setTokenURI(tokenId, uri);
if (gasAmount > 0) {
if (sender == address(0)) revert InvalidAddress();
(bool success, ) = payable(sender).call{value: gasAmount}("");
if (!success) revert GasTokenTransferFailed();
}
emit TokenTransferReceived(receiver, tokenId, uri);
return "";
function unpause() public onlyOwner {
_unpause();
}

function onRevert(RevertContext calldata context) external onlyGateway {
(, uint256 tokenId, string memory uri, address sender) = abi.decode(
context.revertMessage,
(address, uint256, string, address)
);

_safeMint(sender, tokenId);
_setTokenURI(tokenId, uri);
emit TokenTransferReverted(sender, tokenId, uri);
}
function _authorizeUpgrade(
address newImplementation
) internal override onlyOwner {}

// The following functions are overrides required by Solidity.

Expand Down Expand Up @@ -198,7 +106,11 @@ contract UniversalNFT is
)
public
view
override(ERC721Upgradeable, ERC721URIStorageUpgradeable)
override(
ERC721Upgradeable,
ERC721URIStorageUpgradeable,
UniversalNFTCore
)
returns (string memory)
{
return super.tokenURI(tokenId);
Expand All @@ -212,24 +124,11 @@ contract UniversalNFT is
override(
ERC721Upgradeable,
ERC721EnumerableUpgradeable,
ERC721URIStorageUpgradeable
ERC721URIStorageUpgradeable,
UniversalNFTCore
)
returns (bool)
{
return super.supportsInterface(interfaceId);
}

function _authorizeUpgrade(
address newImplementation
) internal override onlyOwner {}

function pause() public onlyOwner {
_pause();
}

function unpause() public onlyOwner {
_unpause();
}

receive() external payable {}
}
164 changes: 164 additions & 0 deletions contracts/nft/contracts/evm/UniversalNFTCore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
import {RevertOptions} from "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
import "../shared/UniversalNFTEvents.sol";
import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {ERC721URIStorageUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

abstract contract UniversalNFTCore is
ERC721Upgradeable,
ERC721URIStorageUpgradeable,
OwnableUpgradeable,
UniversalNFTEvents
{
GatewayEVM public gateway;
address public universal;
uint256 public gasLimitAmount;

error InvalidAddress();
error Unauthorized();
error InvalidGasLimit();
error GasTokenTransferFailed();

modifier onlyGateway() {
if (msg.sender != address(gateway)) revert Unauthorized();
_;
}

function setGasLimit(uint256 gas) external onlyOwner {
if (gas == 0) revert InvalidGasLimit();
gasLimitAmount = gas;
}

function setUniversal(address contractAddress) external onlyOwner {
if (contractAddress == address(0)) revert InvalidAddress();
universal = contractAddress;
emit SetUniversal(contractAddress);
}

function __UniversalNFTCore_init(
address gatewayAddress,
address universalAddress,
uint256 gas
) internal {
if (gatewayAddress == address(0)) revert InvalidAddress();
if (universalAddress == address(0)) revert InvalidAddress();
if (gas == 0) revert InvalidGasLimit();
gateway = GatewayEVM(gatewayAddress);
universal = universalAddress;
gasLimitAmount = gas;
}
Dismissed Show dismissed Hide dismissed

/**
* @notice Transfers an NFT to another chain.
* @dev Burns the NFT locally, then sends an encoded message to the
* Gateway to recreate it on the destination chain (or revert if needed).
* @param tokenId The ID of the NFT to transfer.
* @param receiver The address on the destination chain that will receive the NFT.
* @param destination The contract address on the destination chain (or address(0) if same chain).
*/
function transferCrossChain(
uint256 tokenId,
address receiver,
address destination
) external payable virtual {
if (receiver == address(0)) revert InvalidAddress();

string memory uri = tokenURI(tokenId);

_burn(tokenId);

bytes memory message = abi.encode(
destination,
receiver,
tokenId,
uri,
msg.sender
);

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

if (destination == address(0)) {
gateway.call(
universal,
message,
RevertOptions(address(this), false, address(0), message, 0)
);
} else {
gateway.depositAndCall{value: msg.value}(
universal,
message,
RevertOptions(
address(this),
true,
address(0),
abi.encode(receiver, tokenId, uri, msg.sender),
gasLimitAmount
)
);
}
}

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

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

_safeMint(receiver, tokenId);
_setTokenURI(tokenId, uri);
if (gasAmount > 0) {
if (sender == address(0)) revert InvalidAddress();
(bool success, ) = payable(sender).call{value: gasAmount}("");
if (!success) revert GasTokenTransferFailed();
}
emit TokenTransferReceived(receiver, tokenId, uri);
return "";
}
Comment on lines +105 to +128

Check warning

Code scanning / Slither

Low-level calls Warning


function onRevert(RevertContext calldata context) external onlyGateway {
(, uint256 tokenId, string memory uri, address sender) = abi.decode(
context.revertMessage,
(address, uint256, string, address)
);

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

function tokenURI(
uint256 tokenId
)
public
view
virtual
override(ERC721Upgradeable, ERC721URIStorageUpgradeable)
returns (string memory)
{
return super.tokenURI(tokenId);
}

function supportsInterface(
bytes4 interfaceId
)
public
view
virtual
override(ERC721Upgradeable, ERC721URIStorageUpgradeable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
Loading
Loading