Skip to content
Open
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 packages/commands/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./evm";
export * from "./faucet";
export * from "./query";
export * from "./solana";
export * from "./staking";
export * from "./sui";
export * from "./ton";
export * from "./zetachain";
3 changes: 2 additions & 1 deletion packages/commands/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { evmCommand } from "./evm";
import { faucetCommand } from "./faucet";
import { queryCommand } from "./query";
import { solanaCommand } from "./solana";
import { stakingCommand } from "./staking";
import { suiCommand } from "./sui";
import { tonCommand } from "./ton";
import { zetachainCommand } from "./zetachain";
Expand All @@ -25,7 +26,7 @@ toolkitCommand.addCommand(solanaCommand);
toolkitCommand.addCommand(suiCommand);
toolkitCommand.addCommand(tonCommand);
toolkitCommand.addCommand(zetachainCommand);

toolkitCommand.addCommand(stakingCommand);
showRequiredOptions(toolkitCommand);

toolkitCommand.parse(process.argv);
118 changes: 118 additions & 0 deletions packages/commands/src/staking/delegate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Command, Option } from "commander";
import { ethers } from "ethers";
import { z } from "zod";

import { STAKING_PRECOMPILE } from "../../../../src/constants/addresses";
import { namePkRefineRule } from "../../../../types/shared.schema";
import { handleError, validateAndParseSchema } from "../../../../utils";
import type {
ConfirmTxOptionsSubset,
SetupTxOptionsSubset,
} from "../../../../utils/zetachain.command.helpers";
import {
confirmZetachainTransaction,
setupZetachainTransaction,
} from "../../../../utils/zetachain.command.helpers";
import stakingArtifact from "./staking.json";

const STAKING_ABI = (stakingArtifact as { abi: unknown })
.abi as ethers.InterfaceAbi;

const delegateOptionsSchema = z
.object({
amount: z.string(),
chainId: z.string().optional(),
name: z.string().default("default"),
privateKey: z.string().optional(),
rpc: z.string().optional(),
validator: z.string(),
yes: z.boolean().default(false),
})
.refine(namePkRefineRule);

type DelegateOptions = z.infer<typeof delegateOptionsSchema>;

const main = async (options: DelegateOptions) => {
try {
const { signer } = setupZetachainTransaction({
chainId: options.chainId,
name: options.name,
privateKey: options.privateKey,
rpc: options.rpc,
} satisfies SetupTxOptionsSubset);

const delegatorAddress = await signer.getAddress();
const amountWei = ethers.parseEther(options.amount);

console.log(`Staking delegation details:
Delegator: ${delegatorAddress}
Validator: ${options.validator}
Amount: ${options.amount} ZETA
`);

const isConfirmed = await confirmZetachainTransaction({
yes: options.yes,
} satisfies ConfirmTxOptionsSubset);
if (!isConfirmed) return;

const iface = new ethers.Interface(STAKING_ABI);
const data = iface.encodeFunctionData("delegate", [
delegatorAddress,
options.validator,
amountWei,
]);
Comment on lines +59 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Validator address must be normalized (accept bech32 valoper and 0x).

You pass options.validator as-is. CLI examples use zetavaloper… bech32, while the precompile likely expects a 20‑byte EVM address. Normalize input to 0x… (and checksum) before encoding.

Apply this diff in-place to use a normalized address:

-    const data = iface.encodeFunctionData("delegate", [
-      delegatorAddress,
-      options.validator,
-      amountWei,
-    ]);
+    const validatorHex = normalizeValidatorAddress(options.validator);
+    const data = iface.encodeFunctionData("delegate", [
+      delegatorAddress,
+      validatorHex,
+      amountWei,
+    ]);

Add this helper and imports outside the range:

import { bech32 } from "bech32";
import { Buffer } from "buffer";

const normalizeValidatorAddress = (input: string): string => {
  if (/^0x[0-9a-fA-F]{40}$/.test(input)) return ethers.getAddress(input);
  try {
    const { prefix, words } = bech32.decode(input);
    if (prefix !== "zetavaloper") throw new Error("invalid bech32 prefix");
    const bytes = Buffer.from(bech32.fromWords(words));
    if (bytes.length !== 20) throw new Error("expected 20-byte operator address");
    return ethers.getAddress("0x" + bytes.toString("hex"));
  } catch {
    throw new Error(
      "Validator must be a 0x…20-byte address or a zetavaloper… bech32 string"
    );
  }
};
🤖 Prompt for AI Agents
In packages/commands/src/staking/delegate.ts around lines 59 to 63,
options.validator is passed as-is but the precompile expects a 20-byte EVM
address; add a normalize helper and imports and use it when encoding. Add
imports for bech32 and Buffer and implement normalizeValidatorAddress that
accepts either 0x..40hex or zetavaloper bech32, decodes bech32 to 20 bytes,
validates length and prefix, then returns ethers.getAddress(hex) (throwing a
clear error on invalid input). Replace the current use of options.validator in
iface.encodeFunctionData("delegate", [...]) with
normalizeValidatorAddress(options.validator) so a checksummed 0x address is
encoded.


const tx = await signer.sendTransaction({
data,
to: STAKING_PRECOMPILE,
value: amountWei,
});
Comment on lines +65 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Double-specifying amount (calldata + msg.value) may be wrong for this ABI. Please verify.

You encode amount as a parameter and also send value: amountWei. Depending on the staking precompile ABI, only one may be required.

Run to confirm the delegate signature and mutability:

If inputs do not include an amount (or stateMutability is payable), drop the calldata amount and rely on msg.value; otherwise, keep the calldata amount and set value: 0n.


🏁 Script executed:

#!/bin/bash
# Locate staking.json and inspect the `delegate` function signature.
set -euo pipefail
fd -a 'staking.json' | while read -r f; do
  echo "File: $f"
  jq -r '
    .abi
    | map(select(.type=="function" and .name=="delegate"))
    | .[] | {name, stateMutability, inputs, outputs}
  ' "$f"
done

Length of output: 553


Drop value for delegate transaction
The delegate ABI is nonpayable and already takes amount as a parameter—remove value: amountWei from the transaction to prevent reverts.

🤖 Prompt for AI Agents
In packages/commands/src/staking/delegate.ts around lines 65 to 69, the
transaction is including value: amountWei which causes reverts because the
delegate ABI is nonpayable and already encodes the amount in the calldata;
remove the value field from the signer.sendTransaction call (or set value to 0)
so the transaction doesn't send ETH and leave the amount solely in the encoded
data parameter.


const receipt = await tx.wait();
console.log("Transaction hash:", tx.hash ?? receipt?.hash);
} catch (error) {
handleError({
context: "Error during staking delegation",
error,
shouldThrow: false,
});
process.exit(1);
}
};

export const delegateCommand = new Command("delegate").summary(
"Delegate ZETA to a validator via the staking precompile"
);

delegateCommand
.addOption(
new Option(
"--validator <valoper>",
"Validator operator address (zetavaloper...)"
).makeOptionMandatory()
)
.addOption(
new Option(
"--amount <amount>",
"Amount of ZETA to delegate"
).makeOptionMandatory()
)
.addOption(
new Option("--name <name>", "Account name")
.default("default")
.conflicts(["private-key"])
)
.addOption(
new Option("--private-key <key>", "Private key for signing").conflicts([
"name",
])
)
.addOption(new Option("--rpc <url>", "ZetaChain RPC URL"))
.addOption(new Option("--chain-id <id>", "ZetaChain chain ID"))
.addOption(new Option("--yes", "Skip confirmation").default(false))
.action(async (rawOptions) => {
const options = validateAndParseSchema(rawOptions, delegateOptionsSchema, {
exitOnError: true,
});
await main(options);
});
10 changes: 10 additions & 0 deletions packages/commands/src/staking/delegations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Command } from "commander";

import { listCommand } from "./list";
import { showCommand } from "./show";

export const delegationsCommand = new Command("delegations")
.summary("Delegations-related queries")
.addCommand(listCommand)
.addCommand(showCommand)
.helpCommand(false);
Loading