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 13 commits into
base: main
Choose a base branch
from

Conversation

fadeev
Copy link
Member

@fadeev fadeev commented Dec 23, 2024

This refactor makes it possible to take an ERC-721 template from https://wizard.openzeppelin.com/#erc721, modify 5 lines and make it a universal NFT.

Turning any ERC-721 (assuming it's upgradeable) into a universal NFT is pretty cool.

We can create a non-upgradeable version in the future (sort of like OZ), but I don't think it's a priority. For one, how would you upgrade an existing ERC-721 if it's non-upgradeable?

Summary by CodeRabbit

  • New Features

    • Enhanced NFT and token contracts with new functionalities for pausing and unpausing operations.
    • Introduction of a core contract for both NFTs and tokens to streamline operations and improve upgradeability.
    • Cross-chain transfer functionalities have been integrated into the core contracts.
  • Bug Fixes

    • Removed unnecessary checks and state variables to simplify contract initialization and improve efficiency.
  • Documentation

    • Updated parameter names in scripts and tasks for clarity and consistency.
  • Chores

    • Refactored contract inheritance structures to enhance maintainability and reduce complexity.

Copy link

coderabbitai bot commented Dec 23, 2024

📝 Walkthrough

Walkthrough

The pull request introduces a comprehensive refactoring of the NFT and token contracts across EVM and ZetaChain platforms. The primary focus is on upgrading to OpenZeppelin Contracts version 5.0.0, streamlining cross-chain functionality, and introducing new core abstract contracts (UniversalNFTCore and UniversalTokenCore) that centralize common cross-chain transfer logic. The changes simplify contract architectures, remove redundant state variables, and enhance the overall modularity of the smart contract ecosystem.

Changes

File Change Summary
contracts/nft/contracts/evm/UniversalNFT.sol - Updated to OpenZeppelin 5.0.0
- Added ERC721BurnableUpgradeable and ERC721PausableUpgradeable
- Removed cross-chain transfer functionality
- Added pause and unpause functions
contracts/nft/contracts/evm/UniversalNFTCore.sol - New abstract contract for cross-chain NFT transfers
- Includes transferCrossChain, onCall, and onRevert functions
- Manages gateway and universal contract interactions
contracts/nft/contracts/zetachain/UniversalNFT.sol - Simplified inheritance
- Removed cross-chain specific state variables
- Integrated with UniversalNFTCore
contracts/token/contracts/evm/UniversalToken.sol - Removed cross-chain transfer logic
- Simplified contract inheritance
- Removed ERC20PermitUpgradeable
contracts/token/contracts/evm/UniversalTokenCore.sol - New abstract contract for cross-chain token transfers
- Includes transferCrossChain, onCall, and onRevert functions
- Manages gateway and universal contract interactions
contracts/nft/scripts/localnet.sh - Updated NFT transfer command parameters
- Replaced --from with --contract
- Replaced --to with --destination
contracts/nft/tasks/transfer.ts - Updated task parameter names
- Renamed from to contract
- Renamed to to destination

Sequence Diagram

sequenceDiagram
    participant Owner
    participant UniversalNFT
    participant Gateway
    
    Owner->>UniversalNFT: initialize(owner, name, symbol)
    Owner->>UniversalNFT: safeMint(recipient, tokenId)
    
    alt Cross-Chain Transfer
        Owner->>UniversalNFT: transferCrossChain(tokenId, receiver, destination)
        UniversalNFT->>Gateway: Send transfer message
        Gateway->>UniversalNFT: Confirm transfer
    end
    
    alt Pause/Unpause
        Owner->>UniversalNFT: pause()
        Owner->>UniversalNFT: unpause()
    end
Loading

The sequence diagram illustrates the key interactions within the UniversalNFT contract, showcasing initialization, minting, cross-chain transfer, and pause/unpause functionalities.

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Comment on lines +63 to +103
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
);

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);
}
Comment on lines +105 to +128
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 "";
}

Check warning

Code scanning / Slither

Low-level calls Warning

contracts/nft/contracts/evm/UniversalNFT.sol Fixed Show fixed Hide fixed
Comment on lines +42 to +53
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;
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Comment on lines +40 to +51
function __UniversalNFTCore_init(
address gatewayAddress,
uint256 gas,
address uniswapRouterAddress
) internal {
if (gatewayAddress == address(0) || uniswapRouterAddress == address(0))
revert InvalidAddress();
if (gas == 0) revert InvalidGasLimit();
gateway = GatewayZEVM(payable(gatewayAddress));
uniswapRouter = uniswapRouterAddress;
gasLimitAmount = gas;
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Comment on lines +66 to +128
function transferCrossChain(
uint256 tokenId,
address receiver,
address destination
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
string memory uri = tokenURI(tokenId);
_burn(tokenId);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(
receiver,
tokenId,
uri,
0,
msg.sender
);
CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(tokenId, uri, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);

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

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +66 to +128
function transferCrossChain(
uint256 tokenId,
address receiver,
address destination
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
string memory uri = tokenURI(tokenId);
_burn(tokenId);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(
receiver,
tokenId,
uri,
0,
msg.sender
);
CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(tokenId, uri, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);

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

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +66 to +128
function transferCrossChain(
uint256 tokenId,
address receiver,
address destination
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
string memory uri = tokenURI(tokenId);
_burn(tokenId);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(
receiver,
tokenId,
uri,
0,
msg.sender
);
CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(tokenId, uri, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);

emit TokenTransfer(receiver, destination, tokenId, uri);
}
Comment on lines +66 to +128
function transferCrossChain(
uint256 tokenId,
address receiver,
address destination
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
string memory uri = tokenURI(tokenId);
_burn(tokenId);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(
receiver,
tokenId,
uri,
0,
msg.sender
);
CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(tokenId, uri, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);

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

Check warning

Code scanning / Slither

Low-level calls Warning

@fadeev fadeev changed the title nft: refactor to extract transferCrossChain into an abstract contract refactor: extracted core logic into an abstract UniversalNFTCore contract Dec 23, 2024
@fadeev fadeev marked this pull request as ready for review December 25, 2024 10:04
@fadeev fadeev requested a review from a team as a code owner December 25, 2024 10:04
Comment on lines +41 to +52
function __UniversalTokenCore_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;
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Comment on lines +54 to +89
function transferCrossChain(
address destination,
address receiver,
uint256 amount
) external payable {
if (receiver == address(0)) revert InvalidAddress();
_burn(msg.sender, amount);

bytes memory message = abi.encode(
destination,
receiver,
amount,
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(amount, msg.sender),
gasLimitAmount
)
);
}

emit TokenTransfer(destination, receiver, amount);
}
Comment on lines +91 to +110
function onCall(
MessageContext calldata context,
bytes calldata message
) external payable onlyGateway returns (bytes4) {
if (context.sender != universal) revert Unauthorized();
(
address receiver,
uint256 amount,
uint256 gasAmount,
address sender
) = abi.decode(message, (address, uint256, uint256, address));
_mint(receiver, amount);
if (gasAmount > 0) {
if (sender == address(0)) revert InvalidAddress();
(bool success, ) = payable(sender).call{value: amount}("");
if (!success) revert GasTokenTransferFailed();
}
emit TokenTransferReceived(receiver, amount);
return "";
}

Check notice

Code scanning / Slither

Reentrancy vulnerabilities Low

Comment on lines +91 to +110
function onCall(
MessageContext calldata context,
bytes calldata message
) external payable onlyGateway returns (bytes4) {
if (context.sender != universal) revert Unauthorized();
(
address receiver,
uint256 amount,
uint256 gasAmount,
address sender
) = abi.decode(message, (address, uint256, uint256, address));
_mint(receiver, amount);
if (gasAmount > 0) {
if (sender == address(0)) revert InvalidAddress();
(bool success, ) = payable(sender).call{value: amount}("");
if (!success) revert GasTokenTransferFailed();
}
emit TokenTransferReceived(receiver, amount);
return "";
}

Check warning

Code scanning / Slither

Low-level calls Warning

Comment on lines +40 to +51
function __UniversalTokenCore_init(
address gatewayAddress,
uint256 gas,
address uniswapRouterAddress
) internal {
if (gatewayAddress == address(0) || uniswapRouterAddress == address(0))
revert InvalidAddress();
if (gas == 0) revert InvalidGasLimit();
gateway = GatewayZEVM(payable(gatewayAddress));
uniswapRouter = uniswapRouterAddress;
gasLimitAmount = gas;
}

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Comment on lines +66 to +121
function transferCrossChain(
address destination,
address receiver,
uint256 amount
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
_burn(msg.sender, amount);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(receiver, amount, 0, msg.sender);

CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(amount, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);
emit TokenTransfer(destination, receiver, amount);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +66 to +121
function transferCrossChain(
address destination,
address receiver,
uint256 amount
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
_burn(msg.sender, amount);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(receiver, amount, 0, msg.sender);

CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(amount, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);
emit TokenTransfer(destination, receiver, amount);
}

Check warning

Code scanning / Slither

Unused return Medium

Comment on lines +66 to +121
function transferCrossChain(
address destination,
address receiver,
uint256 amount
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
_burn(msg.sender, amount);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(receiver, amount, 0, msg.sender);

CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(amount, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);
emit TokenTransfer(destination, receiver, amount);
}
Comment on lines +66 to +121
function transferCrossChain(
address destination,
address receiver,
uint256 amount
) public payable {
if (msg.value == 0) revert ZeroMsgValue();
if (receiver == address(0)) revert InvalidAddress();
_burn(msg.sender, amount);

(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();

address WZETA = gateway.zetaToken();

IWETH9(WZETA).deposit{value: msg.value}();
IWETH9(WZETA).approve(uniswapRouter, msg.value);

uint256 out = SwapHelperLib.swapTokensForExactTokens(
uniswapRouter,
WZETA,
gasFee,
gasZRC20,
msg.value
);

uint256 remaining = msg.value - out;

if (remaining > 0) {
IWETH9(WZETA).withdraw(remaining);
(bool success, ) = msg.sender.call{value: remaining}("");
if (!success) revert TransferFailed();
}

bytes memory message = abi.encode(receiver, amount, 0, msg.sender);

CallOptions memory callOptions = CallOptions(gasLimitAmount, false);

RevertOptions memory revertOptions = RevertOptions(
address(this),
true,
address(0),
abi.encode(amount, msg.sender),
gasLimitAmount
);

IZRC20(gasZRC20).approve(address(gateway), gasFee);
gateway.call(
abi.encodePacked(connected[destination]),
destination,
message,
callOptions,
revertOptions
);
emit TokenTransfer(destination, receiver, amount);
}

Check warning

Code scanning / Slither

Low-level calls Warning

Comment on lines +123 to +168
function onCall(
MessageContext calldata context,
address zrc20,
uint256 amount,
bytes calldata message
) external override onlyGateway {
if (context.sender != connected[zrc20]) revert Unauthorized();
(
address destination,
address receiver,
uint256 tokenAmount,
address sender
) = abi.decode(message, (address, address, uint256, address));
if (destination == address(0)) {
_mint(receiver, tokenAmount);
} else {
(address gasZRC20, uint256 gasFee) = IZRC20(destination)
.withdrawGasFeeWithGasLimit(gasLimitAmount);
if (destination != gasZRC20) revert InvalidAddress();
uint256 out = SwapHelperLib.swapExactTokensForTokens(
uniswapRouter,
zrc20,
amount,
destination,
0
);
if (!IZRC20(destination).approve(address(gateway), out)) {
revert ApproveFailed();
}
gateway.withdrawAndCall(
abi.encodePacked(connected[destination]),
out - gasFee,
destination,
abi.encode(receiver, tokenAmount, out - gasFee, sender),
CallOptions(gasLimitAmount, false),
RevertOptions(
address(this),
true,
address(0),
abi.encode(tokenAmount, sender),
0
)
);
}
emit TokenTransferToDestination(destination, receiver, amount);
}
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (13)
contracts/token/contracts/evm/UniversalToken.sol (1)

50-50: Consider access controls for minting.
Currently, only the owner can mint. This is fine for certain scenarios but consider if there's a need for role-based access controls (e.g. MINTER_ROLE) for flexible future usage.

contracts/token/contracts/zetachain/UniversalToken.sol (1)

9-9: Remove duplicate import.
ERC20BurnableUpgradeable appears to be imported twice (line 9 and line 9 repeated). Ensure each import is intentional, or remove duplicates to declutter.

- import {ERC20BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
- import {ERC20BurnableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
contracts/nft/contracts/evm/UniversalNFT.sol (2)

2-2: Include rationale for version references in a comment.
Mentioning compatibility with OpenZeppelin Contracts ^5.0.0 is helpful. Consider adding which features from v5.0.0 are being leveraged.


44-44: Initialize pausable last for clarity.
__ERC721Pausable_init() is placed with other inits. Consider grouping related initializers clearly for maintainability.

contracts/token/contracts/evm/UniversalTokenCore.sol (2)

4-10: Imports are well-organized.
Separating protocol-contract imports, OpenZeppelin imports, and local references (UniversalTokenEvents) is good for readability.


91-110: onCall callback
Logic properly checks context.sender == universal, then mints tokens and attempts to forward gas fees. Watch for re-entrancy on external call to payable(sender).call{value:amount}("").

 // Potentially use the OpenZeppelin ReentrancyGuard:
+import "@openzeppelin/contracts/security/ReentrancyGuard.sol"
 ...
 function onCall(...) external payable onlyGateway nonReentrant returns (bytes4) {
     ...
 }
contracts/nft/contracts/evm/UniversalNFTCore.sol (2)

36-40: setUniversal Function
This function updates the universal address and emits an event. Consider adding more checks (e.g., code size of the address) if you expect only contract addresses.


63-103: Potential Re-Entrancy Risks When Transferring Cross-Chain
The code performs multiple external calls (e.g., gateway.call / gateway.depositAndCall) and sends ETH. Although you are using short flows and burning the token prior to cross-chain messaging, carefully review the call order and guard your contract if you add more state changes later.

contracts/token/contracts/zetachain/UniversalTokenCore.sol (3)

35-38: onlyGateway Modifier
Similar to the NFT core, this gating is consistent. Document or reference in the code comments that cross-chain messages must come from the official gateway to avoid confusion for new integrators.


53-56: setGasLimit
Matches the pattern from the NFT core. Confirm that gas usage in the cross-chain calls is consistent with this user-defined field. Also consider event emission for clarity.


66-121: transferCrossChain
You handle zero-value checks (ZeroMsgValue), burn tokens, handle gas fee, deposit WZETA, and swap to pay cross-chain costs. This approach is thorough. Carefully track leftover WZETA logic in future expansions to avoid dust amounts that remain locked.

contracts/nft/contracts/zetachain/UniversalNFTCore.sol (1)

66-128: transferCrossChain and Re-Entrancy
Calls like IWETH9(WZETA).approve(), IZRC20(gasZRC20).approve() and subsequent gateway.call() can be re-entrancy vectors if new storage writes are introduced in the future. An extra nonReentrant guard or pattern might be advisable for safety. Also, ignore return values from the approvals with caution—if any approval fails, it should revert.

contracts/nft/tasks/transfer.ts (1)

45-45: JSON Output
The structured JSON includes the relevant data. Consider also including gas costs or cross-chain fees in this output for debugging or cost analysis.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 785f106 and 7556e00.

📒 Files selected for processing (10)
  • contracts/nft/contracts/evm/UniversalNFT.sol (5 hunks)
  • contracts/nft/contracts/evm/UniversalNFTCore.sol (1 hunks)
  • contracts/nft/contracts/zetachain/UniversalNFT.sol (5 hunks)
  • contracts/nft/contracts/zetachain/UniversalNFTCore.sol (1 hunks)
  • contracts/nft/scripts/localnet.sh (1 hunks)
  • contracts/nft/tasks/transfer.ts (5 hunks)
  • contracts/token/contracts/evm/UniversalToken.sol (2 hunks)
  • contracts/token/contracts/evm/UniversalTokenCore.sol (1 hunks)
  • contracts/token/contracts/zetachain/UniversalToken.sol (4 hunks)
  • contracts/token/contracts/zetachain/UniversalTokenCore.sol (1 hunks)
🔇 Additional comments (72)
contracts/token/contracts/evm/UniversalToken.sol (6)

11-11: Use explicit interface references for clarity.
Importing UniversalTokenCore.sol is necessary for the new inheritance model. Ensure code references to its functions or events are explicitly documented to maintain clarity around cross-chain functionalities.


20-20: Inherited abstract contract usage is appropriate.
The shift to inheriting UniversalTokenCore consolidates cross-chain logic in a dedicated contract and simplifies this file. The approach aligns with DRY principles.


42-43: Protecting pause functionality.
The pause() function is restricted to onlyOwner, which is generally acceptable. Ensure any operational team using multi-sig or governance structure also has access, if needed.


46-47: Unpausing logic is correct.
Similarly, unpausing is restricted to onlyOwner. This is consistent with how pausable patterns are typically implemented.


58-58: Override _update usage is consistent.
Overriding _update to combine ERC20Upgradeable and ERC20PausableUpgradeable logic is straightforward. No issues spotted.


36-39: Check upgrade implications of combined initializers.
Calling multiple initializers (__ERC20Pausable_init(), __UniversalTokenCore_init()) is valid, but ensure they don’t conflict or double-initialize state. Consider verifying each newly inherited member’s storage alignment.

✅ Verification successful

Storage layout appears safe for upgrades

Based on the analysis of the inheritance chain and storage variables:

  1. The contract follows a proper initialization pattern with distinct initializers:

    • UniversalToken inherits from Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable, ERC20PausableUpgradeable, and OwnableUpgradeable
    • Each base contract uses the OpenZeppelin upgradeable pattern
    • The initialization calls are ordered correctly from most basic to most specific
  2. Storage layout examination shows:

    • Base contracts use isolated storage patterns from OpenZeppelin's upgradeable contracts
    • UniversalTokenCore defines non-conflicting storage variables (gateway, universal, gasLimitAmount)
    • No storage collisions are detected between the inherited contracts

The initialization sequence is safe and follows upgradeable contract best practices. The storage layout maintains proper isolation between inherited contracts.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Examine the storage layout for collisions in upgraded contracts.
ast-grep --pattern $'contract $_ {\n  $$$\n}' | rg "storage|slot"

Length of output: 64


Script:

#!/bin/bash
# Let's examine the inheritance chain and initializers
ast-grep --pattern 'contract $_ is $_' contracts/token/contracts/evm/UniversalToken.sol

# Check the implementation of UniversalTokenCore initialization
ast-grep --pattern 'function __UniversalTokenCore_init($$$) {
  $$$
}'

# Check ERC20Pausable initialization
ast-grep --pattern 'function __ERC20Pausable_init() {
  $$$
}'

# Look for storage variable declarations in the inheritance chain
rg "^\s*(uint|int|bool|address|mapping|struct|bytes|string)" contracts/token/contracts/evm/UniversalToken.sol -A 1

# Check for storage gaps in the contracts
rg "uint256\[\d+\]\s+private\s+__gap" -A 1

Length of output: 775


Script:

#!/bin/bash
# Let's check the base contracts and their inheritance
rg "contract UniversalToken" -A 5 contracts/token/contracts/evm/UniversalToken.sol

# Check UniversalTokenCore contract
rg "contract UniversalTokenCore" -A 5 -r .

# Look for storage variables in related contracts
rg "^\s*(uint|int|bool|address|mapping|struct|bytes|string)" -g "**/*Token*.sol" -g "**/*Pausable*.sol" -g "**/*Ownable*.sol" -A 1

# Check for initialize functions in the inheritance chain
ast-grep --pattern 'function initialize($$$) {
  $$$
}'

Length of output: 10292

contracts/token/contracts/zetachain/UniversalToken.sol (5)

17-17: Use consistent import formatting.
import "./UniversalTokenCore.sol"; clarifies the local path. If code is part of the same package, keep imports consistent with the rest of the codebase (relative or absolute).


26-26: Good approach to unify cross-chain logic via UniversalTokenCore.
Inheriting UniversalTokenCore ensures that cross-chain features are consistently maintained in a separate module.


45-45: Confirm compatibility of addresses in _init().
Ensure gatewayAddress and uniswapRouterAddress are valid. Although checks happen in __UniversalTokenCore_init(), confirm no additional constraints are needed (e.g., whitelists or pre-set routers).


56-58: mint is well-protected.
Restricting mint rights to the owner is acceptable. For production scenarios, consider role-based or multi-sig governance.


74-74: Optional fallback function usage.
Allowing receive() external payable {} can be beneficial, but confirm if the contract truly needs to accept native tokens.

contracts/nft/contracts/zetachain/UniversalNFT.sol (4)

13-13: Consistent reference to UniversalNFTCore.
Inheriting a core module mirrors the approach used in tokens. This reduces duplication and centralizes cross-chain logic.


24-24: Inheritance alignment is sound.
UniversalNFTCore integration ensures feature parity and code reuse. No apparent conflicts with other inherited classes.


44-44: Pause functionality properly initialized.
__ERC721Pausable_init() sets up the pausable capacity. No issues seen.


48-48: Initialize cross-chain logic in a single call.
__UniversalNFTCore_init(gatewayAddress, gas, uniswapRouterAddress) clarifies cross-chain setup. That said, ensure thorough testing is done to avoid uninitialized storage collisions.

contracts/nft/contracts/evm/UniversalNFT.sol (14)

3-3: Newer pragma version is acceptable.
^0.8.26 is stable unless there's a known bug. Confirm any known vulnerabilities are addressed if the contract is widely used.


6-6: Burnable NFT extension is properly included.
This extension is a standard approach to allow NFT burning. Ensure any additional burning restrictions are in place if user minted tokens shouldn’t be burned arbitrarily.


8-8: Pausable NFT extension is consistent.
Adding pausing offers an operational safeguard. Check if ephemeral token states are impacted by pause/unpause transitions.


10-10: Initializable usage is standard.
Initializable is necessary for upgradeable proxies. This is correct.


14-14: Core functionality factorization.
UniversalNFTCore centralizes cross-chain NFT logic. This fosters reusability across chain-specific contracts.


20-20: Override ordering for clarity.
ERC721URIStorageUpgradeable is placed before ERC721PausableUpgradeable. The order is relevant for function overrides. Confirm that no overshadow of _update() or _setTokenURI() occurs.


25-25: Abstract inheritance with UniversalNFTCore.
Centralizing cross-chain operations in a single base is a good design move.


48-48: Chain-specific parameter passing.
Passing address(this) to _init ensures the forward references are correct. Confirm extra trust assumptions around calling the same address.


54-54: safeMint gating with both onlyOwner and whenNotPaused
Combining the two modifiers is typically best practice for controlled minting. Good approach.


67-68: Pause function.
Only the owner can invoke pause(). This is consistent with typical pattern usage.


71-72: Unpause function.
Similarly, unpause() is restricted to the owner. This is standard and correct.


75-77: Ensure upgrade logic is tested.
_authorizeUpgrade is limited to onlyOwner. Confirm thorough testing or usage of test upgrade proxies before production.


109-113: tokenURI override order is valid.
Overriding across multiple inheritors, including UniversalNFTCore, is consistent. Confirm that any custom token URI logic in UniversalNFTCore is accounted for here.


127-128: supportsInterface override is thorough.
Aggregating interfaces from each inherited class helps ensure broad compatibility. No issues found.

contracts/token/contracts/evm/UniversalTokenCore.sol (10)

1-3: File header and pragma usage.
Using // SPDX-License-Identifier: MIT with ^0.8.26 is standard. This approach is recommended for clarity on licensing.


11-15: Abstract contract with multi-inheritance.
UniversalTokenCore extends multiple classes. This is acceptable, but confirm that each base class’s storage layouts align.


16-18: Gateway, universal, and gas limit are clearly named.
Straightforward naming and public visibility for essential cross-chain variables. No issues found.


20-23: Custom errors enhance revert clarity.
Defining typed errors clarifies revert reasons cost-effectively. Good practice.


25-28: onlyGateway guard ensures call origin.
This helps secure onCall and onRevert. Verify the gateway contract is properly authenticated off-chain.


30-33: Gas limit setter with zero check.
Reverts on zero gas limit, preventing misconfiguration. This is correct.


35-39: Universal address setter.
Ensuring non-zero address with an event is a robust approach for cross-chain usage. Good practice.


41-52: Core initializer validations.
Ensuring non-zero addresses and non-zero gas is crucial. Contract properly references gateway = GatewayEVM(gatewayAddress).


112-119: onRevert callback
Re-mints tokens to revert a failed cross-chain transfer. This ensures user funds aren’t lost. Consider event logs for debugging cross-chain reverts. The emit TokenTransferReverted(...) is sufficient.


120-120: Contract end
No issues.

contracts/nft/contracts/evm/UniversalNFTCore.sol (9)

1-10: Ensure Up-to-Date Imports and Licenses
The imports look correct and the license is set to MIT. Confirm that all imported contracts (e.g., GatewayEVM) are at the desired versions for production.


11-20: Inheritance Order and Access Modifiers
Your inheritance hierarchy clearly places ERC721Upgradeable and related contracts before OwnableUpgradeable. This is acceptable. Review whether any constructor-based logic remains in these inherited classes and ensure they do not conflict with your upgradeable initialization approach.


21-25: Centralized State Variables
Storing gateway, universal address, and gas limit in one contract centralizes cross-chain logic. Ensure no naming conflicts with derived classes or sibling contracts that might also define similar variables.


26-29: onlyGateway Modifier
This implementation correctly restricts callable functions from the gateway only. Make sure to document external usage if integrators need to identify gateway addresses programmatically.


31-34: setGasLimit Validation
The check if (gas == 0) revert InvalidGasLimit(); helps prevent invalid input. Verify that a zero gas limit is indeed never a valid scenario.


42-53: Initializer Naming: Not in MixedCase
This function name __UniversalNFTCore_init triggers a prior naming-convention warning.


105-128: onCall Implementation
Good approach to decode the message and safely mint tokens. Confirm that all reverts or errors are handled consistently across all cross-chain callbacks to avoid inconsistent state on partial failures.


130-140: Revert Logic
When reverting a cross-chain transfer, the contract remints the token to the original sender. Explicitly ensure that any partial state updates made before the revert scenario are also accounted for in derived contracts.


141-163: tokenURI and supportsInterface Overrides
Your overrides conform to OpenZeppelin’s approach, which is good. Keep documentation updated to inform integrators about differences between local URIs and cross-chain minted URIs if that arises.

contracts/token/contracts/zetachain/UniversalTokenCore.sol (7)

1-11: Imports & Modularity
You are combining multiple interfaces (e.g., IGatewayZEVM, IWZETA) and libraries (SwapHelperLib). Continue ensuring that contract size remains manageable; otherwise, consider splitting some functionality into separate libraries.


12-19: Contract Declaration & State Variables
Centralizing gateway, uniswapRouter, and gasLimitAmount is consistent with the cross-chain approach. Ensure these do not conflict with possible inherited variables if the derived contracts expand functionality further.


26-34: Comprehensive Error Handling
Your custom errors (e.g., TransferFailed, Unauthorized, etc.) provide clarity. Confirm all revert messages are consistent across the codebase so integrators can reliably detect error states.


40-51: __UniversalTokenCore_init
Checks for gatewayAddress != address(0) and gas != 0 are robust. If you anticipate future expansions, consider a pattern for re-initialization or versioning if your contract is upgradeable.


58-64: setConnected
The function sets up a mapping from ZRC20 to connected contract addresses. Consider validating code size for contractAddress if needed.


123-168: onCall Logic & Another Nested Swap
The contract does a nested swap for gas fees or token bridging, then calls gateway.withdrawAndCall. Confirm that the usage of approvals is validated or at least logged for transparency, since it's granting permission to external contracts.


170-178: onRevert
Similar to the NFT approach, you mint tokens back to the sender. This is a sensible fallback. Maintain consistent revert messages (abi.encode) to unify the debugging process across all cross-chain calls.

contracts/nft/contracts/zetachain/UniversalNFTCore.sol (7)

1-13: Imports and Base Structure
The contract extends UniversalContract, ERC721Upgradeable, ERC721URIStorageUpgradeable, and OwnableUpgradeable. This multi-inheritance approach is logical for cross-chain NFT use cases. Validate final bytecode size if usage grows.


14-20: State Variables & connected Mapping
You track gateway, uniswapRouter, and a connected mapping. This parallels the approach in UniversalTokenCore. Keep consistent naming across token and NFT cores to ease comprehension for integrators.


28-34: Error Descriptions
The custom errors are consistent with your approach in the EVM version. Ensure mismatch scenarios (e.g., user provides a 0 address or invalid ZRC20) always revert, since partial usage could lead to unpredictability.


40-51: Initializer: Non-MixedCase
Like the EVM version, __UniversalNFTCore_init triggers a naming convention warning.


130-182: onCall Handling
Correctly decodes cross-chain messages and mints or swaps tokens as needed. Keep a watch for potential gas griefing scenarios if the contract runs out of gas partway. Document recommended gas usage for clients.


184-193: onRevert
Recreate the NFT for the original sender. Matches earlier patterns. Make sure all reverts are consistently documented.


195-218: ERC721 Standard Overrides
tokenURI and supportsInterface overrides align with standard practices. Provide clarity in documentation if any cross-chain minted URI differs from local minted URI assumptions.

contracts/nft/tasks/transfer.ts (7)

10-10: Validation of Addresses
Nice to see you validate both args.destination and args.revertAddress. This proactive check is valuable for scripts.


14-17: Approving the Contract
You approve the same contract as your args.contract. Verify you actually need to approve the contract to transfer from itself or if another contract address is more appropriate (e.g., the universal core).


27-27: Contract Retrieval
The script retrieves ZetaChainUniversalNFT by args.contract. Ensure that the correct ABI (IERC721 or other advanced interface) is used for subsequent calls if the contract extends beyond basic ERC721.


37-37: transferCrossChain Usage
You pass args.destination to the contract with an optional receiver. Confirm that your contract can handle a scenario where receiver is not the same as signer.address.


53-53: Success Log
Clear message for the user. Ensure that this log remains correct if the user changes the addresses or the flow.


66-66: Renamed Parameter
--contract replaced the old --from. This is a better name but ensure existing script references are updated to avoid confusion.


96-96: destination Parameter
Renaming --to to --destination clarifies usage. Ensure the help text is equally clear that this expects a ZRC20 address for cross-chain bridging.

contracts/nft/scripts/localnet.sh (3)

59-59: Parameter Updates: ZetaChain → Ethereum
You’ve replaced --to with --destination and --from with --contract. This aligns with your updated Hardhat task. Confirm other references in your documentation are also updated.


65-65: Parameter Updates: Ethereum → BNB
Same parameter rename. Good consistency across chain calls.


71-71: No --destination Provided
Here you omit --destination. If the contract’s default handling is to treat a missing destination as a local chain transfer or a defined fallback, confirm that’s intended.

Comment on lines +54 to +89
function transferCrossChain(
address destination,
address receiver,
uint256 amount
) external payable {
if (receiver == address(0)) revert InvalidAddress();
_burn(msg.sender, amount);

bytes memory message = abi.encode(
destination,
receiver,
amount,
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(amount, msg.sender),
gasLimitAmount
)
);
}

emit TokenTransfer(destination, receiver, amount);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Cross-chain transfer logic.
Burning tokens locally, building the message, and dispatching to gateway is a typical cross-chain design.
Consider potential bridging re-entrancy or concurrency with repeated messages.

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

Successfully merging this pull request may close these issues.

1 participant