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

ERC-20 token transfer example and improvements to NFT #209

Merged
merged 27 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/hello/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@
"@solana/web3.js": "^1.95.2",
"@zetachain/protocol-contracts": "10.0.0-rc11"
}
}
}
10 changes: 10 additions & 0 deletions examples/hello/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2434,6 +2434,16 @@
"@zetachain/networks" "^10.0.0"
ethers "^6.13.1"

"@zetachain/[email protected]":
version "10.0.0-rc11"
resolved "https://registry.yarnpkg.com/@zetachain/protocol-contracts/-/protocol-contracts-10.0.0-rc11.tgz#53f55ead492f7b5802b1feae4e51abc75730af33"
integrity sha512-qWazjqnIGRngf4OmyeSIv7sHICQRdMQ1CKPIQIqxA8qFR+gHhDHSfvMdRAvgWbsfkimXOIFiHVIATypyWhviJw==
dependencies:
"@openzeppelin/contracts" "^5.0.2"
"@openzeppelin/contracts-upgradeable" "^5.0.2"
"@zetachain/networks" "^10.0.0"
ethers "^6.13.1"

"@zetachain/[email protected]":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@zetachain/protocol-contracts/-/protocol-contracts-9.0.0.tgz#c20ad5da43f6f3676f31556b303d1cb4ea17357e"
Expand Down
10 changes: 10 additions & 0 deletions examples/swap/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2172,6 +2172,16 @@
"@zetachain/networks" "^10.0.0"
ethers "^6.13.1"

"@zetachain/[email protected]":
version "10.0.0-rc11"
resolved "https://registry.yarnpkg.com/@zetachain/protocol-contracts/-/protocol-contracts-10.0.0-rc11.tgz#53f55ead492f7b5802b1feae4e51abc75730af33"
integrity sha512-qWazjqnIGRngf4OmyeSIv7sHICQRdMQ1CKPIQIqxA8qFR+gHhDHSfvMdRAvgWbsfkimXOIFiHVIATypyWhviJw==
dependencies:
"@openzeppelin/contracts" "^5.0.2"
"@openzeppelin/contracts-upgradeable" "^5.0.2"
"@zetachain/networks" "^10.0.0"
ethers "^6.13.1"

"@zetachain/[email protected]":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@zetachain/protocol-contracts/-/protocol-contracts-9.0.0.tgz#c20ad5da43f6f3676f31556b303d1cb4ea17357e"
Expand Down
6 changes: 6 additions & 0 deletions examples/token/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.yarn
artifacts
cache
coverage
node_modules
typechain-types
47 changes: 47 additions & 0 deletions examples/token/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const path = require("path");

/**
* @type {import("eslint").Linter.Config}
*/
module.exports = {
env: {
browser: false,
es2021: true,
mocha: true,
node: true,
},
extends: ["plugin:prettier/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
},
plugins: [
"@typescript-eslint",
"prettier",
"simple-import-sort",
"sort-keys-fix",
"typescript-sort-keys",
],
rules: {
"@typescript-eslint/sort-type-union-intersection-members": "error",
camelcase: "off",
"simple-import-sort/exports": "error",
"simple-import-sort/imports": "error",
"sort-keys-fix/sort-keys-fix": "error",
"typescript-sort-keys/interface": "error",
"typescript-sort-keys/string-enum": "error",
},
fadeev marked this conversation as resolved.
Show resolved Hide resolved
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
},
"import/resolver": {
node: {
extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
},
typescript: {
project: path.join(__dirname, "tsconfig.json"),
},
},
},
};
17 changes: 17 additions & 0 deletions examples/token/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
node_modules
.env
coverage
coverage.json
typechain
typechain-types
dependencies

# Hardhat files
cache
artifacts

# Foundry files
out
cache_forge

access_token
21 changes: 21 additions & 0 deletions examples/token/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 ZetaChain

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 4 additions & 0 deletions examples/token/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# NFT Example

This example currently only works with localnet `v4.0.0-rc*`, which supports
authenticated calls and multiple EVM chains.
77 changes: 77 additions & 0 deletions examples/token/contracts/Connected.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@zetachain/protocol-contracts/contracts/evm/GatewayEVM.sol";
import {RevertContext} from "@zetachain/protocol-contracts/contracts/Revert.sol";

contract Connected is ERC20, Ownable {
andresaiello marked this conversation as resolved.
Show resolved Hide resolved
GatewayEVM public immutable gateway;
address public counterparty;

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

function setCounterparty(address contractAddress) external onlyOwner {
counterparty = contractAddress;
}

constructor(
address payable gatewayAddress,
address initialOwner
) ERC20("MyToken", "MTK") Ownable(initialOwner) {
gateway = GatewayEVM(gatewayAddress);
}

function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}

function transferCrossChain(
address receiver,
address destination,
uint256 amount
) external payable {
_burn(msg.sender, amount);
bytes memory encodedData = abi.encode(receiver, amount, destination);

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

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

function onCall(
MessageContext calldata messageContext,
bytes calldata message
) external payable onlyGateway returns (bytes4) {
if (messageContext.sender != counterparty) revert("Unauthorized");
(address receiver, uint256 amount) = abi.decode(
message,
(address, uint256)
);
_mint(receiver, amount);
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

Add the missing return statement in the onCall function

The onCall function declares a bytes4 return type but does not currently return any value. This will result in a compilation error and may cause unexpected behavior. Ensure that you return the appropriate selector as per the interface requirements of the GatewayEVM.

Apply this diff to add the missing return statement:

     _mint(receiver, amount);
+    return _zetaclient.onCallSuccess(
+        messageContext.zetaTxSenderAddress,
+        messageContext.zetaTxOrigin,
+        messageContext.zetaValueAndGas
+    );
 }

Ensure that you replace the return statement with the correct value expected by the GatewayEVM interface.

๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function onCall(
MessageContext calldata messageContext,
bytes calldata message
) external payable onlyGateway returns (bytes4) {
if (messageContext.sender != counterparty) revert("Unauthorized");
(address receiver, uint256 amount) = abi.decode(
message,
(address, uint256)
);
_mint(receiver, amount);
}
function onCall(
MessageContext calldata messageContext,
bytes calldata message
) external payable onlyGateway returns (bytes4) {
if (messageContext.sender != counterparty) revert("Unauthorized");
(address receiver, uint256 amount) = abi.decode(
message,
(address, uint256)
);
_mint(receiver, amount);
return _zetaclient.onCallSuccess(
messageContext.zetaTxSenderAddress,
messageContext.zetaTxOrigin,
messageContext.zetaValueAndGas
);
}


function onRevert(RevertContext calldata context) external onlyGateway {}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

Implement revert handling logic.

The empty onRevert function could lead to lost tokens in case of cross-chain transfer failures. Consider implementing proper revert handling logic to ensure tokens can be recovered.

Would you like me to provide an implementation for handling cross-chain transfer reverts?


receive() external payable {}

fallback() external payable {}
}
fadeev marked this conversation as resolved.
Show resolved Hide resolved
119 changes: 119 additions & 0 deletions examples/token/contracts/Universal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.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";

contract Universal is ERC20, Ownable, UniversalContract {
GatewayZEVM public immutable gateway;
SystemContract public immutable systemContract =
SystemContract(0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9);
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

Avoid hardcoding the system contract address

Hardcoding the systemContract address reduces flexibility and can lead to issues if the address changes across different networks or environments. Consider passing the address as a constructor parameter or providing a setter function with appropriate access control to allow for configuration.

uint256 private _nextTokenId;
bool public isUniversal = true;
uint256 public gasLimit = 700000;

error TransferFailed();

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");
_;
}

constructor(
address payable gatewayAddress,
address initialOwner
) ERC20("MyToken", "MTK") Ownable(initialOwner) {
gateway = GatewayZEVM(gatewayAddress);
}

function setCounterparty(
address zrc20,
bytes memory contractAddress
) external onlyOwner {
counterparty[zrc20] = contractAddress;
emit CounterpartySet(zrc20, contractAddress);
}

function transferCrossChain(
address receiver,
address zrc20,
uint256 amount
) public {
_burn(msg.sender, amount);

(, uint256 gasFee) = IZRC20(zrc20).withdrawGasFeeWithGasLimit(gasLimit);
if (!IZRC20(zrc20).transferFrom(msg.sender, address(this), gasFee))
revert TransferFailed();
IZRC20(zrc20).approve(address(gateway), gasFee);

bytes memory encodedData = abi.encode(receiver, amount);
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

Ensure consistent message encoding and decoding

In transferCrossChain, the message is encoded with two parameters:

bytes memory encodedData = abi.encode(receiver, amount);

However, in onCall, the message is decoded expecting three parameters:

(address receiver, uint256 tokenAmount, address destination) = abi.decode(message, (address, uint256, address));

This inconsistency will cause decoding errors. Ensure that the encoding and decoding formats match between these functions.

Apply this diff to align the encoding and decoding:

  • If destination is intended to be part of the message:
function transferCrossChain(
    address receiver,
    address zrc20,
    uint256 amount,
+   address destination
) public {
    _burn(msg.sender, amount);

    // ... [omitted code] ...

-   bytes memory encodedData = abi.encode(receiver, amount);
+   bytes memory encodedData = abi.encode(receiver, amount, destination);

    // ... [omitted code] ...
}
  • If destination is not required, adjust the decoding in onCall:
function onCall(
    MessageContext calldata messageContext,
    address zrc20,
    uint256 amount,
    bytes calldata message
) external override onlyGateway {
    // ... [omitted code] ...

-   (address receiver, uint256 tokenAmount, address destination) = abi.decode(message, (address, uint256, address));
+   (address receiver, uint256 tokenAmount) = abi.decode(message, (address, uint256));

    // ... [omitted code] ...
}

Also applies to: 92-93


CallOptions memory callOptions = CallOptions(gasLimit, false);

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

gateway.call(
counterparty[zrc20],
zrc20,
encodedData,
callOptions,
revertOptions
);
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue

Enhance security in transferCrossChain function

The transferCrossChain function performs multiple external calls and token transfers, which could introduce reentrancy risks. To strengthen security:

  • Utilize the checks-effects-interactions pattern to order operations safely.
  • Consider incorporating OpenZeppelin's ReentrancyGuard to prevent reentrant calls.
  • Ensure that all external calls are properly validated and that state changes occur before these calls when appropriate.


function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}

function onCall(
MessageContext calldata messageContext,
address zrc20,
uint256 amount,
bytes calldata message
) external override onlyGateway {
if (keccak256(messageContext.origin) != keccak256(counterparty[zrc20]))
revert("Unauthorized");
Copy link

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion

Simplify origin verification in onCall function

Comparing keccak256 hashes of byte arrays is unnecessary and gas-inefficient. Since both messageContext.origin and counterparty[zrc20] are of type bytes, consider using a utility function to compare the byte arrays directly or, if possible, standardize on a fixed-size type like bytes32 for direct comparison. This will improve efficiency and readability.

(address receiver, uint256 tokenAmount, address destination) = abi
.decode(message, (address, uint256, address));
if (destination == address(0)) {
_mint(receiver, tokenAmount);
} else {
(, uint256 gasFee) = IZRC20(destination).withdrawGasFeeWithGasLimit(
700000
);
SwapHelperLib.swapExactTokensForTokens(
systemContract,
zrc20,
amount,
destination,
0
);
IZRC20(destination).approve(address(gateway), gasFee);
gateway.call(
counterparty[destination],
destination,
abi.encode(receiver, tokenAmount),
CallOptions(700000, false),
RevertOptions(address(0), false, address(0), "", 0)
);
}
}

function onRevert(RevertContext calldata context) external onlyGateway {}
}
11 changes: 11 additions & 0 deletions examples/token/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[profile.default]
src = 'contracts'
out = 'out'
viaIR = true
libs = ['node_modules', 'lib']
test = 'test'
cache_path = 'cache_forge'
verbosity = 3

[dependencies]
forge-std = { version = "1.9.2" }
21 changes: 21 additions & 0 deletions examples/token/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import "./tasks/deploy";
import "./tasks/deploy";
fadeev marked this conversation as resolved.
Show resolved Hide resolved
import "./tasks/mint";
import "./tasks/transfer";
import "./tasks/universalSetCounterparty";
import "./tasks/connectedSetCounterparty";
import "@zetachain/localnet/tasks";
import "@nomicfoundation/hardhat-toolbox";
import "@zetachain/toolkit/tasks";

import { getHardhatConfigNetworks } from "@zetachain/networks";
import { HardhatUserConfig } from "hardhat/config";

const config: HardhatUserConfig = {
networks: {
...getHardhatConfigNetworks(),
},
solidity: "0.8.26",
fadeev marked this conversation as resolved.
Show resolved Hide resolved
};

export default config;
Loading
Loading