Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/workflows/localnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ jobs:
run: |
cd "${{ matrix.example-dir }}"
yarn
forge soldeer update
chmod +x ./scripts/localnet.sh
./scripts/localnet.sh start
1 change: 1 addition & 0 deletions examples/call/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ src = 'contracts'
out = 'out'
viaIR = true
libs = ['node_modules', "dependencies"]
evm_version = "paris"
test = 'test'
cache_path = 'cache_forge'
verbosity = 3
Expand Down
1 change: 1 addition & 0 deletions examples/hello/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ src = 'contracts'
out = 'out'
viaIR = true
libs = ['node_modules', "dependencies"]
evm_version = "paris"
test = 'test'
cache_path = 'cache_forge'
verbosity = 3
Expand Down
1 change: 1 addition & 0 deletions examples/messaging/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ src = 'contracts'
out = 'out'
viaIR = true
libs = ['node_modules', "dependencies"]
evm_version = "paris"
test = 'test'
cache_path = 'cache_forge'
verbosity = 3
Expand Down
1 change: 1 addition & 0 deletions examples/nft/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ src = 'contracts'
out = 'out'
viaIR = true
libs = ['node_modules', "dependencies"]
evm_version = "paris"
test = 'test'
cache_path = 'cache_forge'
verbosity = 3
Expand Down
29 changes: 29 additions & 0 deletions examples/swap/commands/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import path from "path";
import fs from "fs";

/**
* Load contract artifacts (ABI & bytecode) compiled by Foundry (out/** path)
*/
export const loadContractArtifacts = (
contractName: string,
sourceName?: string
) => {
const sourcePath = sourceName || `${contractName}.sol`;
const artifactPath = path.join(
__dirname,
`../out/${sourcePath}/${contractName}.json`
);

try {
const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8"));
return {
abi: artifact.abi,
bytecode: artifact.bytecode,
} as { abi: any; bytecode: string };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`Unable to load contract artifacts for ${contractName}: ${message}`
);
}
};
Comment on lines +7 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance type safety and add input validation.

The function implementation is functional but could benefit from several improvements:

  1. Type safety: The ABI type is declared as any, reducing type safety
  2. Input validation: No validation of contractName parameter could allow path traversal
  3. Error specificity: Could provide more specific error types
+interface ContractArtifact {
+  abi: any[]; // Could be further typed if ABI structure is known
+  bytecode: string;
+}
+
 export const loadContractArtifacts = (
   contractName: string,
   sourceName?: string
-) => {
+): ContractArtifact => {
+  // Validate contractName to prevent path traversal
+  if (!contractName || contractName.includes('..') || contractName.includes('/')) {
+    throw new Error(`Invalid contract name: ${contractName}`);
+  }
+
   const sourcePath = sourceName || `${contractName}.sol`;
   const artifactPath = path.join(
     __dirname,
     `../out/${sourcePath}/${contractName}.json`
   );

   try {
     const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8"));
     return {
       abi: artifact.abi,
       bytecode: artifact.bytecode,
-    } as { abi: any; bytecode: string };
+    };
   } catch (error) {
     const message = error instanceof Error ? error.message : String(error);
     throw new Error(
       `Unable to load contract artifacts for ${contractName}: ${message}`
     );
   }
 };
🤖 Prompt for AI Agents
In examples/swap/commands/common.ts around lines 7 to 29, improve the
loadContractArtifacts function by replacing the ABI type from 'any' to a more
specific type reflecting the expected ABI structure to enhance type safety. Add
input validation to the contractName parameter to prevent path traversal or
invalid characters by sanitizing or restricting allowed characters. Also, refine
error handling by distinguishing between file read errors and JSON parsing
errors, throwing more specific error types or messages accordingly.

80 changes: 80 additions & 0 deletions examples/swap/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Command } from "commander";
import { ethers } from "ethers";
import { loadContractArtifacts } from "./common";

const main = async (opts: any) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace any type with proper interface.

The function parameter should use a strongly-typed interface instead of any to ensure type safety and better developer experience.

Define an interface for the options:

+interface DeployOptions {
+  rpc: string;
+  privateKey: string;
+  name: string;
+  gateway: string;
+  uniswapRouter: string;
+  gasLimit: number;
+}
+
-const main = async (opts: any) => {
+const main = async (opts: DeployOptions) => {
📝 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
const main = async (opts: any) => {
interface DeployOptions {
rpc: string;
privateKey: string;
name: string;
gateway: string;
uniswapRouter: string;
gasLimit: number;
}
const main = async (opts: DeployOptions) => {
// ...rest of function body...
}
🤖 Prompt for AI Agents
In examples/swap/commands/deploy.ts at line 5, replace the use of the `any` type
for the `opts` parameter with a properly defined interface that describes the
expected shape and properties of the options object. Define this interface above
the function and update the function signature to use it, ensuring type safety
and improved code clarity.

const provider = new ethers.providers.JsonRpcProvider(opts.rpc);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update to ethers v6 API.

The code uses deprecated ethers v5 API. Migrate to ethers v6 for better performance and future support.

Apply these changes:

-  const provider = new ethers.providers.JsonRpcProvider(opts.rpc);
+  const provider = new ethers.JsonRpcProvider(opts.rpc);
-    await implementation.deployed();
+    await implementation.waitForDeployment();
-    await proxy.deployed();
+    await proxy.waitForDeployment();

Also applies to: 16-16, 34-34

🤖 Prompt for AI Agents
In examples/swap/commands/deploy.ts at lines 6, 16, and 34, the code uses the
deprecated ethers v5 API for creating a JsonRpcProvider. Update these lines to
use the ethers v6 API by importing the provider from 'ethers' correctly and
instantiating the JsonRpcProvider according to v6 syntax, ensuring compatibility
with the new version's constructor and method usage.

const signer = new ethers.Wallet(opts.privateKey, provider);

const network = await provider.getNetwork();
const networkInfo = network.name ?? network.chainId;

try {
const { abi, bytecode } = loadContractArtifacts(opts.name);
const factory = new ethers.ContractFactory(abi, bytecode, signer);
const implementation = await factory.deploy();
await implementation.deployed();

const initData = new ethers.utils.Interface(abi).encodeFunctionData(
"initialize",
[opts.gateway, opts.uniswapRouter, opts.gasLimit, signer.address]
);
Comment on lines +18 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate contract interface before encoding function data.

The code assumes the contract has an initialize function with specific parameters without validation.

Add validation:

+    // Validate that the contract has the initialize function
+    const contractInterface = new ethers.utils.Interface(abi);
+    if (!contractInterface.functions['initialize(address,address,uint256,address)']) {
+      throw new Error(`Contract ${opts.name} does not have the expected initialize function`);
+    }
+
-    const initData = new ethers.utils.Interface(abi).encodeFunctionData(
+    const initData = contractInterface.encodeFunctionData(
       "initialize",
       [opts.gateway, opts.uniswapRouter, opts.gasLimit, signer.address]
     );
📝 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
const initData = new ethers.utils.Interface(abi).encodeFunctionData(
"initialize",
[opts.gateway, opts.uniswapRouter, opts.gasLimit, signer.address]
);
// Validate that the contract has the initialize function
const contractInterface = new ethers.utils.Interface(abi);
if (!contractInterface.functions['initialize(address,address,uint256,address)']) {
throw new Error(`Contract ${opts.name} does not have the expected initialize function`);
}
const initData = contractInterface.encodeFunctionData(
"initialize",
[opts.gateway, opts.uniswapRouter, opts.gasLimit, signer.address]
);
🤖 Prompt for AI Agents
In examples/swap/commands/deploy.ts around lines 18 to 21, the code encodes
function data for the "initialize" function without verifying that this function
exists in the contract ABI. To fix this, add a check to confirm the "initialize"
function is present in the Interface before calling encodeFunctionData. If the
function is missing, throw an error or handle it gracefully to prevent runtime
failures.


const { abi: proxyAbi, bytecode: proxyBytecode } = loadContractArtifacts(
"ERC1967Proxy",
"ERC1967Proxy.sol"
);

const proxyFactory = new ethers.ContractFactory(
proxyAbi,
proxyBytecode,
signer
);
const proxy = await proxyFactory.deploy(implementation.address, initData);
await proxy.deployed();

console.log(
JSON.stringify({
contractAddress: proxy.address,
implementationAddress: implementation.address,
deployer: signer.address,
network: networkInfo,
transactionHash: proxy.deployTransaction?.hash,
})
);
} catch (err) {
console.error(
"Deployment failed:",
err instanceof Error ? err.message : err
);
process.exit(1);
}
};

export const deploy = new Command("deploy")
.description("Deploy a swap contract")
.requiredOption(
"-r, --rpc <url>",
"RPC URL (default: testnet)",
"https://zetachain-athens-evm.blockpi.network/v1/rpc/public"
)
.requiredOption("-k, --private-key <key>", "Private key")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Security risk: Private key exposure in command line.

Passing private keys as command-line arguments exposes them in process lists and shell history, creating a significant security vulnerability.

Consider alternative approaches:

-  .requiredOption("-k, --private-key <key>", "Private key")
+  .option("-k, --private-key <key>", "Private key (consider using environment variable PRIVATE_KEY instead)")

And in the main function:

+  const privateKey = opts.privateKey || process.env.PRIVATE_KEY;
+  if (!privateKey) {
+    throw new Error("Private key must be provided via --private-key option or PRIVATE_KEY environment variable");
+  }
-  const signer = new ethers.Wallet(opts.privateKey, provider);
+  const signer = new ethers.Wallet(privateKey, provider);
📝 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
.requiredOption("-k, --private-key <key>", "Private key")
.option("-k, --private-key <key>", "Private key (consider using environment variable PRIVATE_KEY instead)")
🤖 Prompt for AI Agents
In examples/swap/commands/deploy.ts at line 61, avoid accepting the private key
as a command-line argument to prevent exposure in process lists and shell
history. Instead, modify the code to read the private key from a secure source
such as an environment variable or a protected file. Update the option parsing
to remove the private key argument and adjust the main function to retrieve the
key securely from the chosen source.

.option("-n, --name <name>", "Contract name", "Swap")
.requiredOption(
"-u, --uniswap-router <address>",
"Uniswap V2 Router address (default: testnet)",
"0x2ca7d64A7EFE2D62A725E2B35Cf7230D6677FfEe"
)
.option(
"-g, --gateway <address>",
"Gateway address (default: testnet)",
"0x6c533f7fe93fae114d0954697069df33c9b74fd7"
)
.option("--gas-limit <number>", "Gas limit for the transaction", "1000000")
.action((opts) => {
opts.gasLimit = Number(opts.gasLimit);
main(opts).catch((err) => {
console.error("Unhandled error:", err);
process.exit(1);
});
});
8 changes: 8 additions & 0 deletions examples/swap/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env npx tsx

import { Command } from "commander";
import { deploy } from "./deploy";

const program = new Command().addCommand(deploy);

program.parse();
1 change: 1 addition & 0 deletions examples/swap/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ src = 'contracts'
out = 'out'
viaIR = true
libs = ['node_modules', "dependencies"]
evm_version = "paris"
test = 'test'
cache_path = 'cache_forge'
verbosity = 3
Expand Down
1 change: 0 additions & 1 deletion examples/swap/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import "@nomicfoundation/hardhat-toolbox";
import { HardhatUserConfig } from "hardhat/config";
import * as dotenv from "dotenv";

import "./tasks";
import "@zetachain/localnet/tasks";
import "@zetachain/toolkit/tasks";
import { getHardhatConfig } from "@zetachain/toolkit/utils";
Expand Down
2 changes: 1 addition & 1 deletion examples/swap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@
"@zetachain/toolkit": "^16.0.0",
"zetachain": "6.0.1"
}
}
}
150 changes: 45 additions & 105 deletions examples/swap/scripts/localnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,111 +6,51 @@ set -o pipefail

yarn zetachain localnet start --force-kill --exit-on-error &

while [ ! -f "localnet.json" ]; do sleep 1; done

npx hardhat compile --force --quiet

ZRC20_ETHEREUM=$(jq -r '.addresses[] | select(.type=="ZRC-20 ETH on 11155112") | .address' localnet.json)
USDC_ETHEREUM=$(jq -r '.addresses[] | select(.type=="ERC-20 USDC" and .chain=="ethereum") | .address' localnet.json)
ZRC20_USDC=$(jq -r '.addresses[] | select(.type=="ZRC-20 USDC on 98") | .address' localnet.json)
ZRC20_BNB=$(jq -r '.addresses[] | select(.type=="ZRC-20 BNB on 98") | .address' localnet.json)
WZETA=$(jq -r '.addresses[] | select(.type=="wzeta" and .chain=="zetachain") | .address' localnet.json)
GATEWAY_ETHEREUM=$(jq -r '.addresses[] | select(.type=="gateway" and .chain=="ethereum") | .address' localnet.json)
GATEWAY_ZETACHAIN=$(jq -r '.addresses[] | select(.type=="gateway" and .chain=="zetachain") | .address' localnet.json)
UNISWAP_ROUTER=$(jq -r '.addresses[] | select(.type=="uniswapRouterInstance" and .chain=="zetachain") | .address' localnet.json)
SENDER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

CONTRACT_SWAP=$(npx hardhat deploy --name Swap --network localhost --gateway "$GATEWAY_ZETACHAIN" --uniswap-router "$UNISWAP_ROUTER" | jq -r '.contractAddress')
COMPANION=$(npx hardhat deploy-companion --gateway "$GATEWAY_ETHEREUM" --network localhost --json | jq -r '.contractAddress')

npx hardhat evm-swap \
--network localhost \
--receiver "$CONTRACT_SWAP" \
--amount 0.1 \
--target "$WZETA" \
--gateway-evm "$GATEWAY_ETHEREUM" \
--skip-checks \
--withdraw false \
--recipient "$SENDER"

yarn zetachain localnet check

npx hardhat evm-swap \
--network localhost \
--receiver "$CONTRACT_SWAP" \
--amount 0.1 \
--gateway-evm "$GATEWAY_ETHEREUM" \
--target "$ZRC20_USDC" \
--skip-checks \
--recipient "$SENDER"

yarn zetachain localnet check

npx hardhat evm-swap \
--network localhost \
--receiver "$CONTRACT_SWAP" \
--amount 0.1 \
--target "$ZRC20_BNB" \
--gateway-evm "$GATEWAY_ETHEREUM" \
--skip-checks \
--erc20 "$USDC_ETHEREUM" \
--recipient "$SENDER"

yarn zetachain localnet check

npx hardhat evm-swap \
--skip-checks \
--network localhost \
--receiver "$CONTRACT_SWAP" \
--amount 0.1 \
--gateway-evm "$GATEWAY_ETHEREUM" \
--target "$ZRC20_BNB" \
--recipient "$SENDER"

yarn zetachain localnet check

npx hardhat evm-swap \
--skip-checks \
--network localhost \
--receiver "$CONTRACT_SWAP" \
--amount 0.1 \
--target "$ZRC20_BNB" \
--gateway-evm "$GATEWAY_ETHEREUM" \
--recipient "$SENDER" \
--withdraw false

yarn zetachain localnet check

npx hardhat companion-swap \
--network localhost \
--skip-checks \
--contract "$COMPANION" \
--universal-contract "$CONTRACT_SWAP" \
--amount 0.1 \
--target "$ZRC20_BNB" \
--recipient "$SENDER"

yarn zetachain localnet check

npx hardhat companion-swap \
--skip-checks \
--network localhost \
--contract "$COMPANION" \
--universal-contract "$CONTRACT_SWAP" \
--amount 0.1 \
--erc20 "$USDC_ETHEREUM" \
--target "$ZRC20_BNB" \
--recipient "$SENDER"

yarn zetachain localnet check

npx hardhat zetachain-swap \
--network localhost \
--contract "$CONTRACT_SWAP" \
--amount 0.1 \
--zrc20 "$ZRC20_BNB" \
--target "$ZRC20_ETHEREUM" \
--recipient "$SENDER"
while [ ! -f "$HOME/.zetachain/localnet/registry.json" ]; do sleep 1; done

forge build

ZRC20_BNB=$(jq -r '."98".chainInfo.gasZRC20' ~/.zetachain/localnet/registry.json) && echo $ZRC20_BNB
ZRC20_ETHEREUM=$(jq -r '."11155112".chainInfo.gasZRC20' ~/.zetachain/localnet/registry.json) && echo $ZRC20_ETHEREUM
USDC_ETHEREUM=$(jq -r '.["11155112"].contracts[] | select(.contractType == "ERC-20 USDC") | .address' ~/.zetachain/localnet/registry.json) && echo $USDC_ETHEREUM
GATEWAY_ETHEREUM=$(jq -r '.["11155112"].contracts[] | select(.contractType == "gateway") | .address' ~/.zetachain/localnet/registry.json) && echo $GATEWAY_ETHEREUM
GATEWAY_BNB=$(jq -r '."98".contracts[] | select(.contractType == "gateway") | .address' ~/.zetachain/localnet/registry.json) && echo $GATEWAY_BNB
GATEWAY_ZETACHAIN=$(jq -r '.["31337"].contracts[] | select(.contractType == "gateway") | .address' ~/.zetachain/localnet/registry.json) && echo $GATEWAY_ZETACHAIN
UNISWAP_ROUTER=$(jq -r '.["31337"].contracts[] | select(.contractType == "uniswapRouterInstance") | .address' ~/.zetachain/localnet/registry.json) && echo $UNISWAP_ROUTER
WZETA=$(jq -r '.["31337"].contracts[] | select(.contractType == "wzeta") | .address' ~/.zetachain/localnet/registry.json) && echo $WZETA
PRIVATE_KEY=$(jq -r '.private_keys[0]' ~/.zetachain/localnet/anvil.json) && echo $PRIVATE_KEY
RECIPIENT=$(cast wallet address $PRIVATE_KEY) && echo $RECIPIENT
RPC=http://localhost:8545

UNIVERSAL=$(npx ts-node commands/index.ts deploy \
--private-key $PRIVATE_KEY \
--gateway $GATEWAY_ZETACHAIN \
--rpc $RPC \
--uniswap-router $UNISWAP_ROUTER | jq -r .contractAddress)

npx zetachain evm deposit-and-call \
--rpc http://localhost:8545 \
--chain-id 11155112 \
--gateway $GATEWAY_ETHEREUM \
--amount 0.001 \
--types address bytes bool \
--receiver $UNIVERSAL \
--private-key $PRIVATE_KEY \
--values $ZRC20_BNB $RECIPIENT true \
--yes

yarn zetachain localnet check

npx zetachain evm deposit-and-call \
--rpc http://localhost:8545 \
--chain-id 11155112 \
--gateway $GATEWAY_ETHEREUM \
--amount 0.001 \
--types address bytes bool \
--receiver $UNIVERSAL \
--private-key $PRIVATE_KEY \
--values $WZETA $RECIPIENT false \
--yes

yarn zetachain localnet check

Expand Down
Loading