Back to blogs
Written by
Farouk Elalem
Published on
March 14, 2025

ZKsync Governance Tool: Secure Proposal Verification

Ensure secure ZKsync governance with a powerful CLI tool for verifying proposals, decoding transactions, and preventing front-end spoofing before approval.

Table of Contents

Introduction

For blockchain governance bodies and multi-sig committees, verifying transaction data is not optional but absolutely vital. When approving protocol upgrades or moving large sums, trusting only the front-end interface can be dangerously misleading

As seen in the Bybit exploit, even a reputable wallet’s UI can be compromised to present false information. Without independent verification, signers risk authorizing transactions that differ significantly from what they believe they’re approving. Transaction verification acts as a safeguard: it ensures that the raw transaction details (addresses, calldata, amounts, proposal IDs) are exactly as expected.

Other recent exploits (e.g., the Radiant Capital $50M hack) underscore that failing to double-check the data behind a transaction can be catastrophic. In governance scenarios – where proposals can upgrade contracts or transfer treasury funds – the stakes are especially high. This is why robust verification tools and practices are crucial for protecting protocols and user assets.

Overview of the ZKsync Upgrade Verification Tool

To help address these risks, Cyfrin has introduced the ZKsync Upgrade Verification Tool, a command-line utility purpose-built for secure governance transactions. This tool is essential for participants in the ZKsync governance ecosystem, particularly security council members and guardians. It simplifies the process of verifying and understanding governance proposals and upgrades by delivering three key functionalities:

  • Extracting and displaying proposal IDs from transaction hashes (get-zk-id)
  • Decoding and listing the detailed actions of proposals (targets, values, and calldata), including any bridged Ethereum transactions (get-upgrades)
  • Computing the Keccak256 hash of Ethereum upgrade proposals for verification purposes (get-eth-id).

This tool empowers users to verify proposal details against what’s displayed in governance platforms like Tally (used by ZKsync), understand the exact impact a proposal will have on the protocol, and confirm Ethereum proposal IDs before approval or signing. Whether you're ensuring a proposal aligns with its description or validating its integrity across ZKsync and Ethereum, this tool streamlines the process.

Why is it useful?

In the ZKsync governance process, verifying proposals and upgrades is critical to maintaining the protocol’s security and integrity. This tool addresses several needs:

  • Proposal ID verification: The get-zk-id subcommand extracts the proposal ID from a ZKsync transaction hash, displaying it in both hexadecimal and decimal formats. This allows users to confirm that the ID matches what’s shown on governance platforms, ensuring transparency and accuracy pre-vote.

  • Understanding proposal actions: Using get-upgrades, users can decode a proposal’s input data to see the full list of targets, values, and calldata. If the proposal includes calls to the ETH bridge (e.g., sendToL1), it further decodes the Ethereum transactions being proposed, providing a clear picture of the proposal’s impact—crucial for assessing its validity and intent.

  • Ethereum proposal ID confirmation: get-eth-id computes the Keccak256 hash of Ethereum upgrade proposals embedded within a ZKsync proposal. This hash serves as the Ethereum-side proposal ID, enabling users to verify that it matches expectations post-vote, reducing the risk of approving unintended actions.

These features make the tool indispensable for anyone involved in ZKsync governance, ensuring proposals are thoroughly vetted before execution.

How to use the ZKsync upgrade verification tool

The ZKsync upgrade verification tool is a command-line application that operates with subcommands, a transaction hash, and an RPC URL to connect to the ZKsync network. It is straightforward to use and requires only a few inputs. The governor address is optional and defaults to a predefined value if omitted. Below are examples of how to use each subcommand:

  1. Get the ZKsync proposal ID
    To extract the proposal ID from a transaction:
zkgov-check get-zk-id <tx-hash> --rpc-url $ZKSYNC_RPC_URL

Output: The proposal ID in hex and decimal, e.g., 0xe06945bf... and 101504078....

  1. List proposal actions and Ethereum transactions
    To decode the proposal’s actions:
zkgov-check get-upgrades <tx-hash> --rpc-url $ZKSYNC_RPC_URL

Output: A detailed list of targets, values, and calldata, plus any Ethereum transactions if sendToL1 is called.

zkgov-check get-upgrades <tx-hash> --rpc-url $ZKSYNC_RPC_URL --decode

The --decode option allows us to interpret the calldata intended for execution on Ethereum. This makes it possible to view the function being called and the values of its arguments in a human-readable format.

  1. Compute the Ethereum proposal ID
    To generate the Ethereum-side proposal ID from the transaction hash:

zkgov-check get-eth-id <tx-hash> --rpc-url $ZKSYNC_RPC_URL

To generate the Ethereum-side proposal ID based on JSON file containing the executor, salt, and the set of calls:

zkgov-check get-eth-id --from-file <file-path>

Output: The Keccak256 hash for each Ethereum transaction in the proposal.

Quickstart

The ZKsync upgrade verification tool is available in two implementations: one built with Bash and the other with Rust. You can choose whichever stack you prefer to work with:

Bash implementation
Rust implementation

In this article, we’ll be working with the Rust version.

Option 1: Install the binary

  • Run the install script:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cyfrin/zksync-upgrade-verification-rs/releases/latest/download/zkgov-check-installer.sh | sh

Option 2: Build from source

  1. Clone the repo:
git clone https://github.com/Cyfrin/zksync-upgrade-verification
cd zksync-upgrade-verification

  1. Build the binary:
cargo build --release

  1. Add the binary to the path:
sudo mv target/release/zkgov-check /usr/local/bin/

  1. Verify that the tool was installed:
zkgov-check --help
ZkSync Governance Tool

Usage: zkgov-check [OPTIONS] <COMMAND>

Commands:
  get-zk-id     
  get-upgrades  
  get-eth-id    
  help          Print this message or the help of the given subcommand(s)

Options:
      --rpc-url <RPC_URL>    
      --governor <GOVERNOR>  [default: 0x76705327e682F2d96943280D99464Ab61219e34f]
      --decode               
  -h, --help                 Print help
  -V, --version              Print version

Done!

This tool’s simplicity and robustness make it a powerful companion for navigating ZKsync’s decentralized governance process with confidence.

Example

Using the ZKsync Upgrade Verification Tool can dramatically improve the security of governance operations. The following is a step-by-step guide on how a security council or guardian member might use it to verify a proposal, taking ZIP-7 as an example:

1. Get the hash of the transaction containing the proposal: When a new protocol upgrade is proposed, you’ll need to get the hash of the transaction where the proposal was created. You can get this from Tally or a block explorer.

A screenshot of a governance proposal on Tally for [ZIP-7] Lens Chain inclusion on Elastic Network, showing its status, timeline, and an option to view the transaction on the ZKSync block explorer.


2. Run the tool to get the proposal ID: Use the get_zk_id command on the transaction hash. For instance:

zkgov-check get-zk-id 0x94d49c27617ea2dfd78bb3316e6849bdb1a1dd80ddd22151ecb6c644d3fd86f6 --rpc-url $ZKSYNC_RPC_URL

The tool will output the proposal ID in both hex and decimal formats:

Proposal ID
Hex: 0x7551655cb1bd662c2090a1227ea4eef89a4fdefc83bf33b06ca5b41b53fcadd4
Decimal: 53064417471903525695516096129021600825622830249245179379231067906906888383956

This proposal ID should match the one shown in the proposal in Tally.

A screenshot of an active governance proposal titled [ZIP-7] Lens Chain inclusion on Elastic Network by Cyfrin, showing its proposal ID, governor, and voting status with an option to vote on-chain.


3. Fetch and cross-verify the Ethereum-side ID
: For added security, retrieve the proposal ID as recorded on Ethereum by running the get-eth-id command. Here are two ways to do it:

  • From the transaction hash:
zkgov-check get-eth-id 0x94d49c27617ea2dfd78bb3316e6849bdb1a1dd80ddd22151ecb6c644d3fd86f6 --rpc-url $ZKSYNC_RPC_URL

  • The tool will compute the Ethereum-side proposal ID, for example:
Ethereum proposal ID #1: 0x5ebd899d036aae29b12babe196b11380d8304e98ac86390ac18a56ff51ada9bd

  • You can verify this against the proposal ID shown on verify.zknation.io.

  • From a proposal file:

    You can also obtain the Ethereum proposal ID from a JSON file containing the executor, salt, and the set of calls. For instance, see the example file here.

zkgov-check get-eth-id --from-file test-data/zip5.json

Ethereum Proposal ID
Proposal ID: 0xa34bdc028de549c0fbd0374e64eb5977e78f62331f6a55f4f2211348c4902d13
Encoded Proposal: 0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000060000000000000000000000000ece8e30bfc92c2a8e11e6cb2e17b70868572e3f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000320000000000000000000000000303a465b659cbb0ab36ee643ea362c509eeb521300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba509700000000000000000000000000000000000000000000000000000000000000000000000000000000c2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba509700000000000000000000000000000000000000000000000000000000000000000000000000000000d7f9f54194c633f36ccd5f3da84ad4a1c38cb2cb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba5097000000000000000000000000000000000000000000000000000000000000000000000000000000005d8ba173dc6c3c90c8f7c04c9288bef5fdbad06e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba509700000000000000000000000000000000000000000000000000000000000000000000000000000000f553e6d903aa43420ed7e3bc2313be9286a8f98700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba509700000000000000000000000000000000000000000000000000000000

4. Verify the calldata of ZKsync and Ethereum transactions: A complicated task made easier by the get-upgrades command:

zkgov-check get-upgrades 0x94d49c27617ea2dfd78bb3316e6849bdb1a1dd80ddd22151ecb6c644d3fd86f6 --decode --rpc-url $ZKSYNC_RPC_URL

ZKsync Transactions

ZKsync Transaction #1:
Target Address: 0x0000000000000000000000000000000000008008
Value: 0
Calldata: 0x62f84b24000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000008600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000360000000000000000000000000000000000000000000000000000000000000044000000000000000000000000000000000000000000000000000000000000005c00000000000000000000000009da9f5dad070649811d77c40ccdcab479ce3fa0700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba509700000000000000000000000000000000000000000000000000000000000000000000000000000000590e6587b37dc4152b6b036ff88a835bd2ab892400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba5097000000000000000000000000000000000000000000000000000000000000000000000000000000005c03468829a26981c410a7930bd4853622f0b2e500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba50970000000000000000000000000000000000000000000000000000000000000000000000000000000034899f8b01cf52160c88ddb9e29ec3c26901916500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000479ba509700000000000000000000000000000000000000000000000000000000000000000000000000000000c2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000440d14edf700000000000000000000000000000000000000000000000000000000000000e8000000000000000000000000c29d04a93f893700015138e3e334eb828dac3cef00000000000000000000000000000000000000000000000000000000000000000000000000000000303a465b659cbb0ab36ee643ea362c509eeb52130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e43f58f5b500000000000000000000000000000000000000000000000000000000000000e8000000000000000000000000c2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c0000000000000000000000001ff1dc3cb9eedbc6eb2d99c03b30a05ca625fb5a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000590e6587b37dc4152b6b036ff88a835bd2ab8924000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000164e34a329a00000000000000000000000000000000000000000000000000000000000000e8000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000004d89b79a893ac95eb46e96e452ad21f71144c9180000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640528f3f700000000000000000000000006aa7a7b07108f7c5539645e32dd5c21cbf9eb66000000000000000000000000c2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c0000000000000000000000005d8ba173dc6c3c90c8f7c04c9288bef5fdbad06e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
(ETH transaction)

Ethereum Transaction
  Call:
  Target: 0x9da9f5dad070649811d77c40ccdcab479ce3fa07
  Value: 0
  Calldata:  0x79ba5097
  Function: acceptOwnership()() 

  Call:
  Target: 0x590e6587b37dc4152b6b036ff88a835bd2ab8924
  Value: 0
  Calldata:  0x79ba5097
  Function: acceptOwnership()() 

  Call:
  Target: 0x5c03468829a26981c410a7930bd4853622f0b2e5
  Value: 0
  Calldata:  0x79ba5097
  Function: acceptOwnership()() 

  Call:
  Target: 0x34899f8b01cf52160c88ddb9e29ec3c269019165
  Value: 0
  Calldata:  0x79ba5097
  Function: acceptOwnership()() 

  Call:
  Target: 0xc2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c
  Value: 0
  Calldata:  0x0d14edf700000000000000000000000000000000000000000000000000000000000000e8000000000000000000000000c29d04a93f893700015138e3e334eb828dac3cef
  Function: registerAlreadyDeployedHyperchain(uint256,address)(232, 0xc29d04a93f893700015138e3e334eb828dac3cef) 

  Call:
  Target: 0x303a465b659cbb0ab36ee643ea362c509eeb5213
  Value: 0
  Calldata:  0x3f58f5b500000000000000000000000000000000000000000000000000000000000000e8000000000000000000000000c2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c0000000000000000000000001ff1dc3cb9eedbc6eb2d99c03b30a05ca625fb5a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000
  Function: createNewChain(uint256,address,address,uint256,address,bytes)(232, 0xc2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c, 0x1ff1dc3cb9eedbc6eb2d99c03b30a05ca625fb5a, 0, 0x0000000000000000000000000000000000000000, 0x) 

  Call:
  Target: 0x590e6587b37dc4152b6b036ff88a835bd2ab8924
  Value: 0
  Calldata:  0xe34a329a00000000000000000000000000000000000000000000000000000000000000e8000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000600000000000000000000000004d89b79a893ac95eb46e96e452ad21f71144c9180000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640528f3f700000000000000000000000006aa7a7b07108f7c5539645e32dd5c21cbf9eb66000000000000000000000000c2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c0000000000000000000000005d8ba173dc6c3c90c8f7c04c9288bef5fdbad06e00000000000000000000000000000000000000000000000000000000
  Function: executeUpgrade(uint256,((address,uint8,bool,bytes4[])[],address,bytes))(232, ([], 0x4d89b79a893ac95eb46e96e452ad21f71144c918, 0x0528f3f700000000000000000000000006aa7a7b07108f7c5539645e32dd5c21cbf9eb66000000000000000000000000c2ee6b6af7d616f6e27ce7f4a451aedc2b0f5f5c0000000000000000000000005d8ba173dc6c3c90c8f7c04c9288bef5fdbad06e)) 


Executor: 0x0000000000000000000000000000000000000000
Salt: 0x0000000000000000000000000000000000000000000000000000000000000000

  1. The calladata in the section ZKsync Transactions should be identical to what you can see in Tally.

    The Ethereum Transaction section is basically a decode of the sendToL1 calls, which helps us know what exactly is going on inside the calls sent to Ethereum. These calls should then be verified to make sense compared to the motivation behind the proposal.

Conclusion

With increasingly more sophisticated attacks on blockchain governance, the adage “Don’t trust, verify” has never been more relevant. The ZKsync Upgrade Verification Tool exemplifies the kind of rigorous verification process that should become standard practice for governance committees and multi-sig operators. 

By verifying transaction IDs, cross-checking proposal data, and simulating outcomes, this tool ensures that the approved actions match the original intent—nothing more, nothing less. This reduces the risk that busy signers will approve unintended transactions generated by malicious proposals or deceptive front-end spoofing.

The lessons from the Bybit hack and similar exploits are clear: no matter how secure a contract or multi-sig is, the weakest link is often the human who approves a transaction based on unverified information. By incorporating verification scripts and independent checks, we add a robust layer of security. 

Governance participants are strongly encouraged to adopt such tools and practices – whether it’s ZKsync’s CLI for protocol upgrades or other safe-transaction verifiers for multi-sigs – to protect the integrity of their decisions.

In summary, secure blockchain governance hinges on diligence and the right tools. Verifying each transaction’s details can mean the difference between a routine upgrade and a devastating exploit. Solutions like the ZKsync Upgrade Verification Tool empower the community to conduct thorough due diligence on every proposal, promoting greater trust in the system. 

As decentralized organizations and protocols manage ever larger sums and critical infrastructure, building a culture of “verify before you sign” is the key to resilience. This is how we can confidently step into the future of on-chain governance – secure, transparent, and disaster-proof.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.