{"ends_at":1747062000,"num_competition_issues":438,"judging_stopped_at":1751726863,"rewards":200000,"reserved_auditors_fixed_pays":[{"profile_picture":"https://sherlock-files.ams3.digitaloceanspaces.com/twitter_images/bf2bbbbb-67fc-4fd3-856b-474c4d108a1e.jpg","handle":"berndartmueller","fixed_pay":10000}],"lead_senior_auditor_avatar_url":"https://sherlock-files.ams3.digitaloceanspaces.com/profile_images/0b1f601a-b18a-40b0-bff2-bdc6becc5d55.jpeg","judging_repo_name":"sherlock-audit/2025-04-zetachain-cross-chain-judging","type_label":"Public Best Efforts","title":"ZetaChain Cross-Chain","report":"# Issue H-1: Malicious observer can drain Solana bridge by adding failed deposit transaction to inbound tracker \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/58 \n\n## Found by \n0xalpharush, berndartmueller\n\n### Summary\n\nAny single entity with the admin or observer role can drain the Solana-Zetachain bridge. Deposits can be spoofed by encoding a deposit instruction such that the transaction fails yet is still processed as valid when a malicious observer adds the tx to the inbound CCTX tracker. This is due to the lack of validation on the solana transaction\u2019s meta not containing an error. Normally, observers will not process failing transactions, but this code path fails to perform the same validation unlike its EVM counterpart. While this requires a \"privileged\" role, [every validator is an observer](https://www.zetachain.com/docs/developers/architecture/observers/) and BFT consensus is supposed to be byzantine tolerant i.e. tolerate <1/3 malicious parties. Thus, one malicious observer should not be able to forge a deposit and induce honest validators to vote to mint ZRC20 Sol for failed transactions through the inbound tracker, which lacks validations for its Solana implementation.\n\n### Root Cause\n\nThe `ProcessInboundEvents` [function](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound.go#L147-L157) does not require a transaction to have succeeded unlike the EVM inbound observer which does this correctly [here](https://github.com/zeta-chain/node/blob/f7cd1b57a3e289288671bfd38c41c43248d4491a/zetaclient/chains/evm/observer/inbound.go#L555-L557). Since the instruction is decoded as if it succeeded, a malicious observer can spoof a deposit for the entire ZRC20-SOL balance and then withdraw the SOL locked in the Solana side of the bridge, stealing all lamports in the bridge. This attack could also be used to withdraw SPL tokens or perform arbitrary deposits and calls. For example, [removing the writable property from the gateway\u2019s PDA](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/runner/solana.go#L72) in the deposit instruction results in a failing transaction (see the anchor constraint error in the POC), and it can still be processed once added to the inbound tracker via `MsgAddInboundTracker`.\n\n### Internal Pre-conditions\n\n1. A malicious or negligent observer adds the a failing Solana tx that contains Gateway instructions to the inbound tracker using `MsgAddInboundTracker`, resulting in all validators processing and voting to mint ZRC20 Sol on Zetachain.\n2. The CCTX receives sufficient votes and unbacked ZRC20 Sol on Zetachain is minted.\n\n### External Pre-conditions\n\n1. Any party sends a failing transaction to the gateway with a deposit instruction (or deposit and call).\n2. The recipient of the ZRC20 Sol on Zetachain withdraws it and receives lamports on Solana.\n\n### Attack Path\n\n1. Any party sends a failing transaction to the gateway with a [deposit](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/instructions/deposit.rs#L14) instruction (or deposit and call).\n2. A malicious or negligent observer adds the a failing Solana tx that contains Gateway instructions to the inbound tracker [using](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/msg_server_add_inbound_tracker.go#L15-L36) `MsgAddInboundTracker`, resulting in all validators processing and voting to mint ZRC20 Sol on Zetachain.\n3. The `ProcessInboundTrackers` [task](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound_tracker.go#L13) causes the spoofed CCTX to received sufficient votes, and unbacked ZRC20 Sol on Zetachain is minted.\n4. The recipient of the ZRC20 Sol on Zetachain withdraws it and receives lamports on Solana.\n\n### Impact\n\nAll lamports and SPL tokens deposited in the Solana bridge can be stolen given deposits can be forged for any amount (they are processed despite the Gateway program causing the txs to revert). \n\n### PoC\n\nApply the git diff [here](https://gist.github.com/0xalpharush/a3bfd706019bb3e5df2e964e81b6feb0) to the `node/`.\n\nRun the following tests\n```bash\n$ export E2E_ARGS='--verbose'\n$ make start-solana-test  \n$ docker logs orchestrator --follow\n```\nObserve in the orchestrator's logs that the `solana_deposit ` test passes, and ZRC20 is minted for a failing deposit transaction (fails with `AnchorError caused by account: pda. Error Code: ConstraintMut. Error Number: 2000`).\n\n### Mitigation\n\nDo not process events from failing Solana txs by checking its `Meta.Err` is `nil` as [this patch does](https://gist.github.com/0xalpharush/a3bfd706019bb3e5df2e964e81b6feb0#file-example_fix-patch).\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3914\n\n\n**gjaldon**\n\nFixed by [adding a check](https://github.com/zeta-chain/node/pull/3914/files\\#diff-a7a25c64f1579daa309e07a058f631b8f9867587fc45fc9c531b3625c5f87b28R152-R155) that the transaction succeeded before observing it as an Inbound.\n\n\n\n# Issue H-2: Dirty EVM state changes are not committed before precompile calls, resulting in double-spending or loss of ZETA tokens \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/329 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nberndartmueller, stonejiajia\n\n## Summary\n\n\"Dirty\" EVM state changes before the precompile call are not committed (written) to the Cosmos SDK state. As a result, Cosmos SDK actions (e.g., delegate) operate on old state, and, **after** the precompile call, the [EVM dirty state is committed](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/ethermint/x/evm/keeper/state_transition.go#L434-L439), overwriting the changes made by the Cosmos SDK action.\n\nOverall, this allows double-spending native ZETA tokens, and in other situations leads to loss of ZETA tokens if the state is wrongly overwritten.\n\n## Root Cause\n\nIn ZetaChain's precompiles, the `Run()` function does not commit the current EVM state changes before executing the Cosmos SDK code. If the Cosmos SDK actions involve spending native ZETA tokens, it will either lead to double-spending or loss of tokens.\n\nLet's focus on the most obvious areas that are affected:\n\n### Staking ZETA tokens\n\nPrior to the staking precompile call, if the user transfers native ZETA tokens in Solidity, the Cosmos SDK state is not updated to reflect the spent tokens.\n\n[The `Delegate` call will then succeed](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/staking/method_stake.go#L63-L70), double-spending the tokens.\n\nNote that the explicit `stateDB.SubBalance()` call in the staking precompile is not sufficient to prevent double-spending due to the [lack of checking](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/ethermint/x/evm/statedb/statedb.go#L383-L389) if the [subtraction leads to an underflow](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/ethermint/x/evm/statedb/state_object.go#L133-L136).\n\n[`precompiles/staking/method_stake.go#L82-L88`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/staking/method_stake.go#L82-L88)\n\n```go\n82: \t// if caller is not the same as origin it means call is coming through smart contract,\n83: \t// and because state of smart contract calling precompile might be updated as well\n84: \t// manually reduce amount in stateDB, so it is properly reflected in bank module\n85: \tstateDB := evm.StateDB.(precompiletypes.ExtStateDB)\n86: \tif contract.CallerAddress != evm.Origin {\n87: \t\tstateDB.SubBalance(stakerAddress, amountUint256)\n88: \t}\n```\n\nThis explicit `SubBalance()` call is rather important to ensure that after the delegation, the EVM state is consistent with the just spent ZETA tokens. Otherwise, the ZETA tokens could be double-spent by being transferred after the precompile call.\n\n### Claiming staking rewards\n\nStaking rewards are either explicitly claimed by a user via calling the precompile or implicitly via [Cosmos SDK hooks when delegating/undelegating/re-delegating](https://github.com/cosmos/cosmos-sdk/blob/7b9d2ff98d02bd5a7edd3b153dd577819cc1d777/x/distribution/keeper/hooks.go#L136-L152).\n\nIf prior to the precompile call in Solidity, the withdrawer address is involved in any EVM state changes (e.g., by transferring ZETA token to/from it), the ZETA token balance at this time is cached in the EVM statedb state and not reflected in the Cosmos SDK state.\n\nNow, when claiming and receiving the ZETA coins as staking rewards, the Cosmos SDK state is updated to reflect the balance change.\n\nAt the end of the EVM call (which contains the precompile call), the [EVM state is committed](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/ethermint/x/evm/keeper/state_transition.go#L434-L439), which [overwrites the Cosmos SDK state with the cached EVM state](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/ethermint/x/evm/statedb/statedb.go#L601-L603). As a result, it will overwrite the staking rewards that were just claimed, and the user will lose them.\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\n1. In Solidity, the user transfers their ZETA tokens to another address\n2. Call the staking precompile method to delegate ZETA tokens\n3. The precompile call will succeed, and the ZETA tokens are delegated\n4. The ZETA tokens are double-spent\n\n## Impact\n\n- Double-spending of ZETA tokens\n- Loss of ZETA staking rewards\n\n## PoC\n\nThe following PoC runs the `precompile_contracts_staking_through_contract` e2e test and demonstrates how ZETA tokens can be double-spent by transferring them to the `0xdead` address before the precompile call.\n\nUpdate the `stakeWithStateUpdate` function in [`TestStaking.sol`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/contracts/teststaking/TestStaking.sol):\n\n```solidity\nfunction stakeWithStateUpdate(\n        address staker,\n        string memory validator,\n        uint256 amount\n) external onlyOwner returns (bool) {\n    counter = counter + 1;\n    // transfer full balance\n    (bool success, ) = payable(address(0xdead)).call{value: payable(this).balance }(\"\");\n    require(success, \"transfer to dead address failed\");\n\n    success = staking.stake(staker, validator, amount);\n    counter = counter + 1;\n    return success;\n}\n```\n\nGenerate ABI Go bindings:\n\n```bash\ngo generate ./e2e/contracts/teststaking\n```\n\nReplace the existing test code in [`e2e/e2etests/test_precompiles_staking_through_contract.go`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/e2etests/test_precompiles_staking_through_contract.go) with the following:\n\n```go\npackage e2etests\n\nimport (\n\t\"math/big\"\n\n\tsdk \"github.com/cosmos/cosmos-sdk/types\"\n\tbanktypes \"github.com/cosmos/cosmos-sdk/x/bank/types\"\n\t\"github.com/cosmos/cosmos-sdk/x/staking/types\"\n\t\"github.com/ethereum/go-ethereum/accounts/abi/bind\"\n\t\"github.com/ethereum/go-ethereum/common\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/zeta-chain/node/cmd/zetacored/config\"\n\t\"github.com/zeta-chain/node/e2e/contracts/teststaking\"\n\t\"github.com/zeta-chain/node/e2e/runner\"\n\t\"github.com/zeta-chain/node/e2e/utils\"\n)\n\nfunc TestPrecompilesStakingThroughContract(r *runner.E2ERunner, args []string) {\n\trequire.Len(r, args, 0, \"No arguments expected\")\n\n\ttestStakingAddr, testStakingTx, testStaking, err := teststaking.DeployTestStaking(\n\t\tr.ZEVMAuth,\n\t\tr.ZEVMClient,\n\t\tr.WZetaAddr,\n\t)\n\trequire.NoError(r, err)\n\tutils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, testStakingTx, r.Logger, r.ReceiptTimeout)\n\n\tvalidators, err := testStaking.GetAllValidators(&bind.CallOpts{})\n\trequire.NoError(r, err)\n\trequire.GreaterOrEqual(r, len(validators), 2)\n\n\t// shares are 0 for both validators at the start\n\tsharesBeforeVal1, err := testStaking.GetShares(&bind.CallOpts{}, testStakingAddr, validators[0].OperatorAddress)\n\trequire.NoError(r, err)\n\trequire.Equal(r, int64(0), sharesBeforeVal1.Int64())\n\n\tsharesBeforeVal2, err := testStaking.GetShares(&bind.CallOpts{}, testStakingAddr, validators[1].OperatorAddress)\n\trequire.NoError(r, err)\n\trequire.Equal(r, int64(0), sharesBeforeVal2.Int64())\n\n\tr.ZEVMAuth.Value = big.NewInt(1000000000000)\n\tpreviousGasLimit := r.ZEVMAuth.GasLimit\n\tr.ZEVMAuth.GasLimit = 10000000\n\tdefer func() {\n\t\tr.ZEVMAuth.GasLimit = previousGasLimit\n\t}()\n\n\t// fund testStaking contract with azeta\n\ttx, err := testStaking.DepositWZETA(r.ZEVMAuth)\n\trequire.NoError(r, err)\n\tutils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)\n\tr.ZEVMAuth.Value = big.NewInt(0)\n\n\tstakeAmount := 100000000000\n\ttx, err = testStaking.WithdrawWZETA(r.ZEVMAuth, big.NewInt(int64(stakeAmount)))\n\trequire.NoError(r, err)\n\tutils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)\n\n\texpectedValAddr, err := sdk.ValAddressFromBech32(validators[0].OperatorAddress)\n\trequire.NoError(r, err)\n\n\t// get the balance of the 0xdead evm address\n\tbalanceDeadBeforeStake, err := r.BankClient.Balance(r.Ctx, &banktypes.QueryBalanceRequest{\n\t\tAddress: sdk.AccAddress(common.HexToAddress(\"0xdead\").Bytes()).String(),\n\t\tDenom:   config.BaseDenom,\n\t})\n\trequire.NoError(r, err)\n\n\t// stake 2 more to validator1 using testStaking smart contract with smart contract state update\n\ttx, err = testStaking.StakeWithStateUpdate(\n\t\tr.ZEVMAuth,\n\t\ttestStakingAddr,\n\t\tvalidators[0].OperatorAddress,\n\t\tbig.NewInt(2),\n\t)\n\trequire.NoError(r, err)\n\treceipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout)\n\n\t// check that stake event was emitted\n\tstakeEvent, err := testStaking.ParseStake(*receipt.Logs[0])\n\trequire.NoError(r, err)\n\trequire.Equal(r, big.NewInt(2).Uint64(), stakeEvent.Amount.Uint64())\n\trequire.Equal(r, common.BytesToAddress(expectedValAddr.Bytes()), stakeEvent.Validator)\n\trequire.Equal(r, testStakingAddr, stakeEvent.Staker)\n\n\t// check that bank balance is 0\n\tbalanceAfterStake, err := r.BankClient.Balance(r.Ctx, &banktypes.QueryBalanceRequest{\n\t\tAddress: sdk.AccAddress(testStakingAddr.Bytes()).String(),\n\t\tDenom:   config.BaseDenom,\n\t})\n\trequire.NoError(r, err)\n\trequire.Equal(r, int64(0), balanceAfterStake.Balance.Amount.Int64())\n\n\t// get the zeta balance of the 0xdead address and subtract the balance before the stake to determine the received amount\n\tbalanceDeadAfterStake, err := r.BankClient.Balance(r.Ctx, &banktypes.QueryBalanceRequest{\n\t\tAddress: sdk.AccAddress(common.HexToAddress(\"0xdead\").Bytes()).String(),\n\t\tDenom:   config.BaseDenom,\n\t})\n\trequire.NoError(r, err)\n\trequire.Greater(r, balanceDeadAfterStake.Balance.Amount.Int64()-balanceDeadBeforeStake.Balance.Amount.Int64(), int64(0))\n\n\tr.Logger.Info(\"balanceDeadAfterStake\", \"balance\", balanceDeadAfterStake.Balance.Amount.Int64())\n\n\t// check that counter is updated\n\tcounter, err := testStaking.Counter(&bind.CallOpts{})\n\trequire.NoError(r, err)\n\trequire.Equal(r, int64(2), counter.Int64())\n\n\t// check shares are set to 2\n\tsharesAfterVal1, err := testStaking.GetShares(&bind.CallOpts{}, testStakingAddr, validators[0].OperatorAddress)\n\trequire.NoError(r, err)\n\trequire.Equal(r, big.NewInt(2e18).String(), sharesAfterVal1.String())\n\n\t// check delegation amount using staking keeper query client\n\tdelegationAfterVal1, err := r.StakingClient.Delegation(r.Ctx, &types.QueryDelegationRequest{\n\t\tDelegatorAddr: sdk.AccAddress(testStakingAddr.Bytes()).String(),\n\t\tValidatorAddr: validators[0].OperatorAddress,\n\t})\n\trequire.NoError(r, err)\n\trequire.Equal(r, int64(2), delegationAfterVal1.DelegationResponse.Balance.Amount.Int64())\n}\n```\n\nRun the test:\n\n```bash\nzetae2e run precompile_contracts_staking_through_contract --config cmd/zetae2e/config/local.yml --verbose\n```\n\nResult:\n\nThe test succeeds. ZETA tokens are sent to the `0xdead` address **and** delegated to the validator.\n\n```bash\ne2e          | starting tests\ne2e          | \u2753 balance of spl zrc20: no contract code at given address\ne2e          | \u2753 balance of sui zrc20: nil\ne2e          | \u2753 balance of sui token zrc20: nil\ne2e          | \u23f3 running   - precompile_contracts_staking_through_contract\ne2e          | [INFO] balanceDeadAfterStake%!(EXTRA string=balance, int64=399999999996)\ne2e          | \u2705 completed - precompile_contracts_staking_through_contract (13.136586792s)\ne2e          | \u2753 balance of spl zrc20: no contract code at given address\ne2e          | \u2753 balance of sui zrc20: nil\ne2e          | \u2753 balance of sui token zrc20: nil\ne2e          | tests finished in 18.16416425s\ne2e          |  ---\ud83d\udcc8 E2E Test Report ---\nName                                             Success    Time             Spent\nprecompile_contracts_staking_through_contract    \u2705          18.149263083s    ZETA:298119430000000000\n```\n\n## Mitigation\n\nSimilarly to [Evmos' implementation](https://github.com/evmos/evmos/blob/0402a01c9036b40a7f47dc2fc5fb4cfae019f5f1/precompiles/common/precompile.go#L95-L99), commit the EVM state changes before executing the Cosmos SDK precompile code to ensure both states are synced.\n\n\n# Issue H-3: A Malicious Observer can use TON's Outbound Tracker to steal funds from Zetachain \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/373 \n\n## Found by \ng\n\n### Summary\n\nThe [`processOutboundTracker()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L102-L134) does not check that the TON transaction's nonce matches the CCTX nonce. This allows a malicious Observer to add a tx hash to the tracker that points to a TON withdrawal transaction with a different nonce and has failed, even though their withdrawal succeeded. This allows them to extract funds from Zetachain twice instead of once. Once through the successful withdrawal in TON and another through the refund for the reverted outbound CCTX in Zetachain. \n\n### Root Cause\n\nThere are no checks in [`processOutboundTracker()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L102-L134) that the TON transaction's nonce does not match the nonce of the outbound CCTX. This enables the passing of withdrawal transaction hashes for different outbound CCTXs (those with different nonces). \n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. A malicious Observer withdraws TON coins from Zetachain to TON, creating an outbound CCTX with nonce 42.\n2. Their outbound CCTX is [processed](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/signer/signer.go#L93-L156), and they receive the withdrawn TON.\n3.  Before the outbound voting for the outbound CCTX 42 starts, the malicious Observer [adds](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/msg_server_add_outbound_tracker.go#L20-L107) a transaction hash to the outbound tracker for CCTX 42. The hash points to a different withdrawal transaction that failed.\n4. Every Observer will [process](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L102-L134) the outbound tracker for 42 and set its [receiveStatus](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L122) to failed.\n5. Every Observer will post a [vote outbound](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L31-L52) for the outbound CCTX 42. They will vote that they observed a failure.\n6. Once the consensus is reached and the vote has been finalized, the failed outbound CCTX 42 will [trigger](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go#L188-L190) trigger a revert and refund the TON coins to the sender in Zetachain.\n\n\n### Impact\n\nZetachain will permanently lose TON coins to the attacker. The attacker gains TON coins equal to their withdrawal amount on top of the amount withdrawn.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nConsider checking that the outbound CCTX nonce matches the nonce used in the TON transaction and that the transaction has incremented the nonce.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3977\n\n\n\n\n# Issue H-4: TON Observer processes each hash in the Outbound tracker and stores the result of the last nonce successfully processed \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/375 \n\n## Found by \ng\n\n### Summary\n\nWhen the TON Observer processes an Outbound tracker, it will process [each hash](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L86-L94) without stopping if an error is encountered. The last tx hash successfully processed will overwrite any previous successful processing when it calls [`setOutboundByNonce()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L130-L131). This allows any malicious Observer to add the last tx hash to an Outbound Tracker to mess with the results intentionally. \n\n### Root Cause\n\nUnlike the Bitcoin Observer, which will iterate every tx hash to [find](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/bitcoin/observer/outbound.go#L56-L68) the truly included outbound, the TON Observer will only [store](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L130-L131) the results of the last transaction it successfully processed as long as:\n1. It's a withdrawal transaction.\n2. The signer of the payload hash is the TSS address.\n\nThere is also [no check](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L136-L148) that the transaction has incremented the nonce.\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. Outbound 42 is a successful withdrawal where the sender has received their withdrawn amount.\n2. A malicious Observer can add the tx hash to the Outbound Tracker of Outbound 42 with a failed withdrawal transaction with the same nonce. This can naturally occur when multiple Observers try to broadcast the same Outbound transaction. They do this at the last moment to ensure their tx hash will dictate the observed status.\n3. When the Outbound Tracker for Outbound 42 is processed, their tx hash's results will be used, and it will be a receive_status of [failed](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/outbound.go#L143-L145). This means the attacker can make a successful observation into a failed observation.\n4. Every Observer will vote on outbound 42 with a failed observation, marking it as a Pending Revert.\n5. The sender of the withdrawal ends up claiming both the withdrawal and the refund on their withdrawal, doubling their funds at the expense of Zetachain.\n\n### Impact\n\nThe protocol permanently loses funds equal to the withdrawal amount. The attacker/target will gain funds equal to their withdrawal amount, doubling their withdrawal amount.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\n_No response_\n\n# Issue H-5: Token Mismatch in SPL Token Deposits \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/421 \n\n## Found by \n0xkmr\\_, OhmOudits, adeolu, berndartmueller, chinepun, g, mahdiRostami\n\n### Summary\n\nThe Gateway Solana program contains a critical vulnerability in its `handle_spl` function within the deposit.rs file. While the program requires a whitelist entry account for the SPL token being deposited, it does not verify that the token being transferred from the user's account actually matches the whitelisted token specified in the transaction context. This mismatch allows an attacker to deposit a non-whitelisted token while claiming it's a different, whitelisted token. The ZetaChain node, which processes these deposits, will incorrectly identify the deposited token based on the mint address in the transaction context rather than the actual token being transferred, potentially leading to incorrect cross-chain asset transfers and economic damage.\n\n\nsnippet of the DepositSplToken context below \n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/contexts.rs#L76C1-L89C1\n```rust \n#[derive(Accounts)]\npub struct DepositSplToken<'info> {\n    /// The account of the signer making the deposit.\n    #[account(mut)]\n    pub signer: Signer<'info>,\n\n    /// Gateway PDA.\n    #[account(mut, seeds = [b\"meta\"], bump)]\n    pub pda: Account<'info, Pda>,\n\n    /// The whitelist entry account for the SPL token.\n    #[account(seeds = [b\"whitelist\", mint_account.key().as_ref()], bump)]\n    pub whitelist_entry: Account<'info, WhitelistEntry>,\n\n    /// The mint account of the SPL token being deposited.\n    pub mint_account: Account<'info, Mint>,\n\n    /// The token program.\n    pub token_program: Program<'info, Token>,\n\n    /// The source token account owned by the signer.\n    #[account(mut)]\n    pub from: Account<'info, TokenAccount>,\n\n    /// The destination token account owned by the PDA.\n    #[account(mut)]\n    pub to: Account<'info, TokenAccount>,\n\n    /// The system program.\n    pub system_program: Program<'info, System>,\n}\n```\n\n`mint_account` which is used in the `whitelist_entry` can be different from the token_program and to.mint or from.mint account. \n\n### Root Cause\n\nThe deposit process for SPL tokens involves multiple components that interact as follows:\n\n1. The process begins when a user calls the `deposit_spl_token` function in lib.rs:\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/lib.rs#L236-L242\n```rust\npub fn deposit_spl_token(\n    ctx: Context<DepositSplToken>,\n    amount: u64,\n    receiver: [u8; 20],\n    revert_options: Option<RevertOptions>,\n) -> Result<()> {\n    instructions::deposit::handle_spl(ctx, amount, receiver, revert_options, DEPOSIT_FEE)\n}\n```\n\n2. This function calls `handle_spl` in deposit.rs, which performs the actual deposit operation:\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/instructions/deposit.rs#L68-L77\n```rust\npub fn handle_spl(\n    ctx: Context<DepositSplToken>,\n    amount: u64,\n    receiver: [u8; 20],\n    revert_options: Option<RevertOptions>,\n    deposit_fee: u64,\n) -> Result<()> {\n    verify_payload_size(None, &revert_options)?;\n    let token = &ctx.accounts.token_program; //@audit no check for if token is whitelisted, or is for same one represented by ctx.accounts.mint_account\n    //this disparity can affect the final messaqge emitted, and can affect node processing. i.e if two whitelisted tokens usdc and weth, attacker can ust mint_account for weth and send in 10 usdc\n    //msg willl emit weth as mint_account and node will process deposit on the other side as weth deposit\n    let from = &ctx.accounts.from;\n    \n    // ... rest of the function ...\n}\n```\n\n3. The `DepositSplToken` context includes a `whitelist_entry` account that is derived from the mint address:\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/contexts.rs#L76C1-L105\n```rust\n#[derive(Accounts)]\npub struct DepositSplToken<'info> {\n    // ... other fields ...\n\n    /// The whitelist entry account for the SPL token.\n    #[account(seeds = [b\"whitelist\", mint_account.key().as_ref()], bump)]\n    pub whitelist_entry: Account<'info, WhitelistEntry>,\n\n    /// The mint account of the SPL token being deposited.\n    pub mint_account: Account<'info, Mint>,\n\n    /// The token program.\n    pub token_program: Program<'info, Token>,\n\n    /// The source token account owned by the signer.\n    #[account(mut)]\n    pub from: Account<'info, TokenAccount>,\n    \n    // ... other fields ...\n}\n```\n\nAnd there is no check in the logic to verify that token_program is the valid one belonging to the mint_account provided and the same for to.mint and from.mint. Full function logic below \n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/instructions/deposit.rs#L68-L119\n```rust \npub fn handle_spl(\n    ctx: Context<DepositSplToken>,\n    amount: u64,\n    receiver: [u8; 20],\n    revert_options: Option<RevertOptions>,\n    deposit_fee: u64,\n) -> Result<()> {\n    verify_payload_size(None, &revert_options)?;\n    let token = &ctx.accounts.token_program; \n    let from = &ctx.accounts.from;\n\n    let pda = &mut ctx.accounts.pda;\n    require!(!pda.deposit_paused, Errors::DepositPaused);\n    require!(receiver != [0u8; 20], Errors::EmptyReceiver);\n\n    let cpi_context = CpiContext::new(\n        ctx.accounts.system_program.to_account_info(),\n        system_program::Transfer {\n            from: ctx.accounts.signer.to_account_info().clone(),\n            to: pda.to_account_info().clone(),\n        },\n    );\n    system_program::transfer(cpi_context, deposit_fee)?;\n\n    let pda_ata = get_associated_token_address(&ctx.accounts.pda.key(), &from.mint);\n    require!(\n        pda_ata == ctx.accounts.to.to_account_info().key(), \n        Errors::DepositToAddressMismatch\n    );\n\n    let xfer_ctx = CpiContext::new(\n        token.to_account_info(),\n        anchor_spl::token::Transfer {\n            from: ctx.accounts.from.to_account_info(),\n            to: ctx.accounts.to.to_account_info(), \n            authority: ctx.accounts.signer.to_account_info(),\n        },  \n    );\n    transfer(xfer_ctx, amount)?;\n\n    msg!(\n            \"Deposit SPL executed: amount = {}, fee = {}, receiver = {:?}, pda = {}, mint = {}, revert options = {:?}\",\n            amount,\n            deposit_fee,\n            receiver,\n            ctx.accounts.pda.key(),\n            ctx.accounts.mint_account.key(),\n            revert_options\n        );\n\n    Ok(())\n}\n```\n\nThe logic sets `let token = &ctx.accounts.token_program;`, then fetches the pda_ata via `    let pda_ata = get_associated_token_address(&ctx.accounts.pda.key(), &from.mint);` after which it does a cip_contrxt transfer like below \n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/instructions/deposit.rs#L98-L106\n```rust \n    let xfer_ctx = CpiContext::new(\n        token.to_account_info(),\n        anchor_spl::token::Transfer {\n            from: ctx.accounts.from.to_account_info(),\n            to: ctx.accounts.to.to_account_info(), \n            authority: ctx.accounts.signer.to_account_info(),\n        }, //@audit no freeze authority check too for if pda_ata is frozen before transfer. \n    );\n    transfer(xfer_ctx, amount)?;\n```\nhere the token being transferred is `token` or `ctx.accounts.token_program`. the pda_ata of the program is checked to be same as one generated for the from.mint account representing the token. \n\ntoken_program, from.mint and to.mint can actually represent the same token while mint_account may not. if token_program, from.mint and to.mint represent same token the token transfer will be sucessfull. if mint_account represents another whitelisted token, the whitelist check will succeed too. \n\n4. After the deposit is processed, the ZetaChain node observes the transaction and extracts the deposit information:\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound.go#L224-L251\n```go\n// In FilterInboundEvents function (node/zetaclient/chains/solana/observer/inbound.go)\ndeposit, err := solanacontracts.ParseInboundAsDepositSPL(tx, i, txResult.Slot)\nif err != nil {\n    return nil, errors.Wrap(err, \"error ParseInboundAsDepositSPL\")\n} else if deposit != nil {\n    seenDepositSPL = true\n    events = append(events, &clienttypes.InboundEvent{\n        // ... other fields ...\n        CoinType: coin.CoinType_ERC20,\n        Asset:    deposit.Asset,  // This is the mint address from the transaction\n        // ... other fields ...\n    })\n}\n```\n\nAfter this, the parsing is done here \n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L134-L146\n\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L149-L173\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L312-L333\n\n5. The node extracts the SPL token mint address from the instruction accounts:\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L329-L338\n```go\n// In getSignerAndSPLFromDepositSPLAccounts function (node/pkg/contracts/solana/inbound.go)\n// the accounts are [signer, pda, whitelist_entry, mint_account, token_program, from, to]\nsigner := instructionAccounts[0].PublicKey.String()\nspl := instructionAccounts[3].PublicKey.String()  // This is the mint_account from the transaction\n\nreturn signer, spl, nil\n```\n\nThe issue is that the `handle_spl` function does not verify that the token in the user's `from` account matches the mint address specified in the transaction context. The Anchor framework ensures that a whitelist entry exists for the provided `mint_account`, but there's no check to ensure that the token being transferred has the same mint.\n\n\nThe mint_account is THEN taken as the spl token account and is returned by [getSignerAndSPLFromDepositSPLAccounts](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L338). this is then returned as the asset deposited in [parseAsDepositSPL()](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L186)\n\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L170-L186\n```rust \n\tsender, spl, err := getSignerAndSPLFromDepositSPLAccounts(tx, &instruction)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\n\treceiver, err := parseReceiver(inst.Receiver)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\n\treturn &Inbound{\n\t\tSender:           sender,\n\t\tReceiver:         receiver,\n\t\tAmount:           inst.Amount,\n\t\tMemo:             []byte{},\n\t\tSlot:             slot,\n\t\tAsset:            spl, // <-- see here this is mint_account, may not be the token program or from.mint transferred\n\n```\n\n### Internal Pre-conditions\n\n\n1. The Gateway program has a whitelist mechanism for SPL tokens, implemented through the `whitelist_entry` account in the `DepositSplToken` context.\n\n2. Multiple tokens have been whitelisted in the system (e.g., USDC and WETH).\n\n3. The `handle_spl` function is called with a token program and mint account that should be checked for consistency.\n\n4. The node processes SPL token deposits by extracting the mint address from the transaction context, not from the actual token being transferred.\n\n### External Pre-conditions\n\nnone\n\n### Attack Path\n\n\n1. An attacker identifies two whitelisted tokens in the system, for example, USDC and WETH.\n\n2. The attacker creates a transaction that includes:\n   - A legitimate whitelisted token's mint address (e.g., WETH) in the `mint_account` field\n   - The corresponding whitelist entry for WETH\n   - But actually transfers a different token (e.g., USDC) from their `from` account to the `to` account which is the program pda_ata. already created since USDC is whitelisted \n\n3. The `handle_spl` function does not verify that the token being transferred matches the mint address specified in the transaction.\n\n4. The transaction is processed successfully, and the log message emits information that a WETH deposit was made, even though USDC was actually transferred.\n\n5. The ZetaChain node observes this transaction and processes it as a WETH deposit based on the mint address in the transaction context.\n\n6. This results in a mismatch between the actual token deposited (USDC) and what the system records and processes (WETH), potentially leading to incorrect asset transfers and accounting.\n\n### Impact\n\n1. **Token Spoofing**: Attackers can deposit one token (e.g., USDC) while making the system believe they deposited another token (e.g., WETH).\n\n2. **Cross-Chain Asset Mismatch**: The ZetaChain node will process the deposit based on the mint address in the transaction, not the actual token being transferred, potentially leading to incorrect asset transfers on the ZetaChain side.\n\n3. **Economic Damage**: If the value of the spoofed token is different from the actual token being transferred, this could lead to economic damage or exploitation of the protocol.\n\n4. **Protocol Inconsistency**: The mismatch between the actual token deposited and what the system records could lead to inconsistencies in the protocol state.\n\n\n\n### Mitigation\n\n1. **Implement Token Validation**: Add explicit validation in the `handle_spl` function to ensure the token being deposited matches the mint address specified in the transaction:\n\n```rust\npub fn handle_spl(\n    ctx: Context<DepositSplToken>,\n    amount: u64,\n    receiver: [u8; 20],\n    revert_options: Option<RevertOptions>,\n    deposit_fee: u64,\n) -> Result<()> {\n    verify_payload_size(None, &revert_options)?;\n    \n    // Ensure the token being deposited matches the mint address in the transaction\n    require!(\n        ctx.accounts.from.mint == ctx.accounts.mint_account.key(),\n        Errors::TokenMintMismatch\n    );\n    \n    let token = &ctx.accounts.token_program;\n    let from = &ctx.accounts.from;\n    \n    // ... rest of the function ...\n}\n```\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/protocol-contracts-solana/pull/114\n\n\n**gjaldon**\n\nThe mint of the from token account is now [validated](https://github.com/zeta-chain/protocol-contracts-solana/pull/114/files\\#diff-e9aa90c11c8df3130c0cc027f1d5fd6f429d44a4c1ec35eaeaa40cc280841fc2R97) against the mint_account in the signed payload.\n\n\n\n# Issue M-1: User is able to DOS and grief node by repeatedly calling deposit \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/45 \n\n## Found by \nBrothersInCode, g\n\n### Summary\n\n### Description\n\nAny user can initiate a deposit on the SUI network by calling the `gateway.move::deposit` function:\n\n```javascript\npublic entry fun deposit<T>(\n    gateway: &mut Gateway,\n    coin: Coin<T>,\n    receiver: String,\n    ctx: &mut TxContext,\n) {\n    let amount = coin.value();\n    let coin_name = coin_name<T>();\n\n    check_receiver_and_deposit_to_vault(gateway, coin, receiver);\n\n    // Emit deposit event\n    event::emit(DepositEvent {\n        coin_type: coin_name,\n        amount: amount,\n        sender: tx_context::sender(ctx),\n        receiver: receiver,\n    });\n}\n```\n\nThis function in turn calls`check_receiver_and_deposit_to_vault`:\n\n```javascript\n// Validates receiver and deposits the coin into the vault\nfun check_receiver_and_deposit_to_vault<T>(gateway: &mut Gateway, coin: Coin<T>, receiver: String) {\n    assert!(receiver.length() == ReceiverAddressLength, EInvalidReceiverAddress);\n    assert!(is_whitelisted<T>(gateway), ENotWhitelisted);\n    assert!(!gateway.deposit_paused, EDepositPaused);\n\n    // Perform the deposit\n    let coin_name = coin_name<T>();\n    let vault = bag::borrow_mut<String, Vault<T>>(&mut gateway.vaults, coin_name);\n    balance::join(&mut vault.balance, coin.into_balance());\n}\n```\n\nThis helper function performs a series of checks, including:\n\n- Verifying the receiver address format\n- Checking that the coin type is whitelisted\n- Ensuring that deposits are not currently paused\n\nHowever, SUI's deposit flow does not perform a minimum deposit amount/fee check.\n\nNote that in all of the other implementations (EVM, Solana, TON) these checks are correctly performed.  \n\n\n### Root Cause\n\nThe root cause of this issue is that there is no minimum amount/fee check enforced inside the following [function](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/e3caacef006bcef90af3035ed79d95075e31264d/protocol-contracts-sui/sources/gateway.move#L181-L199)\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. Any user can call `gateway.move::deposit` with `coin` amount set to 0 or a neglible amount\n2. User repeats this as many times as he wants\n\n\n\n### Impact\n\nThere are several impacts here:\n\n- Without a minimum amount or fee requirement, users can submit empty or negligible-value deposits, which will overwhelm the node\u2019s processing logic which in turn will drain the nodes\u2019 funds and cause a potential DOS.\n- SUI's nodes have a maximum event processing limit of 50. A malicious user could exploit this by spamming deposit transactions, quickly hitting the limit with their own events and easily causing DOS for other users\n\n### PoC\n- Paste the following test-function inside `gateway_tests.move`:\n```javascript\n#[test]\nfun test_deposit_zero_amount() {\n    let mut scenario = ts::begin(@0xA);\n    setup(&mut scenario);\n\n    ts::next_tx(&mut scenario, @0xA);\n    {\n        let mut gateway = scenario.take_shared<Gateway>();\n        let admin_cap = ts::take_from_address<AdminCap>(&scenario, @0xA);\n\n        let coin = coin::mint_for_testing<SUI>(0, scenario.ctx()); // deposit 0 amount\n        let eth_addr = ValidEthAddr.to_string().to_ascii();\n\n        deposit(&mut gateway, coin, eth_addr, scenario.ctx());\n\n        ts::return_to_address(@0xA, admin_cap);\n        ts::return_shared(gateway);\n    };\n    ts::end(scenario);\n}\n```\n- run `sui move test test_deposit_zero_amount`\n- Logs:\n```diff\nRunning Move unit tests\n[ PASS    ] gateway::gateway_tests::test_deposit_zero_amount\nTest result: OK. Total tests: 1; passed: 1; failed: 0\n```\n\n### Mitigation\n\nEnsure that users are not able to perform empty deposit calls. \n\n# Issue M-2: Jailed validators are able to participate in voting \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/46 \n\n## Found by \nBrothersInCode, forgebyola\n\n### Description\n\nWhen voting on an inbound or outbound ballot, the following check is performed:\n\n```go\nfunc (k Keeper) VoteOnInboundBallot(\n\tctx sdk.Context,\n\tsenderChainID int64,\n\treceiverChainID int64,\n\tcoinType coin.CoinType,\n\tvoter string,\n\tballotIndex string,\n\tinboundHash string,\n) (isFinalized bool, isNew bool, err error) {\n\t// ... code\n\n\t// Ensure the voter is an active (non-tombstoned) observer\n\tif ok := k.IsNonTombstonedObserver(ctx, voter); !ok {\n\t\treturn false, false, types.ErrNotObserver\n\t}\n\n\t// ... code\n}\n```\n\nThis prevents tombstoned validators from voting on the observation chain. However, the current logic does **not** prevent jailed validators from voting.\n\nIn contrast, a jailed check is correctly performed during observer updates:\n\n```go\nfunc (k msgServer) UpdateObserver(\n\tgoCtx context.Context,\n\tmsg *types.MsgUpdateObserver,\n) (*types.MsgUpdateObserverResponse, error) {\n\t// ... code\n\n\t// New observer must be a bonded, non-jailed validator\n\terr = k.IsValidator(ctx, msg.NewObserverAddress)\n\tif err != nil {\n\t\treturn nil, errorsmod.Wrap(types.ErrUpdateObserver, err.Error())\n\t}\n}\n```\n\nWithin [`IsValidator`](https://github.com/zeta-chain/node/blob/a32ffd88ea9957c8f3424466bb358b4197d41889/x/observer/keeper/voting.go#L94-L108):\n\n```go\nif validator.Jailed || !validator.IsBonded() {\n\treturn types.ErrValidatorStatus\n}\n```\n\nThis ensures that only active, non-jailed validators can be assigned as observers.\n\n### Impact\n\nSince no such check exists during voting, jailed validators are still able to vote, even though they may have misbehaved.\n\n### Recommendation\n\nAdd a jailed validator check during the voting process to ensure that only bonded and non-jailed validators are allowed to vote.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3917\n\n\n**gjaldon**\n\nThe issue has been fixed by [replacing all instances](https://github.com/zeta-chain/node/pull/3917/files\\#diff-38f29b114abcc1da10dbf7a503b0f5fce243866acdbc442034eec4b0b8c8c706R105-R106) of `IsNonTombstonedObserver()` with `CheckObserverCanVote()`, which also checks if a validator is jailed.\n\n\n\n# Issue M-3: TON transactions risk being lost due to flawed transaction handling \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/73 \n\n## Found by \n0x73696d616f, BrothersInCode\n\n### Description\n\nInside the [`ton::inbound.go::ObserveInbound`](https://github.com/zeta-chain/node/blob/fe3505ffc8a55343c68f682dff92c18ecb8e8fc4/zetaclient/chains/ton/observer/inbound.go#L33) function, transactions are fetched using the following code:\n\n```go\ntxs, err := ob.client.GetTransactionsSince(ctx, ob.gateway.AccountID(), lt, hashBits)\nif err != nil {\n\treturn errors.Wrap(err, \"unable to get transactions\")\n}\n\n```\n\nThe `GetTransactionsSince` method retrieves transactions, populating the `txs`array, beginning with **most recent to oldest,** which aligns with how the TON Lite API operates:\n\n```go\n// Note: TON Lite API returns transactions from latest to oldest.\n// We retrieve up to `pageSize` transactions per call.\ntxs, err := c.GetTransactions(ctx, pageSize, acc, lt, hashBits)\nif err != nil {\n\treturn nil, errors.Wrapf(err, \"unable to get transactions [lt %d, hash %s]\", lt, hashBits.Hex())\n}\n\n```\n\nHere, `pageSize` is set to 200.\n\n```jsx\n\tpageSize       = 200\n```\n\nIf the last scanned transaction is found in the retrieved `txs`, the function exits early without reversing the list. As a result, the transactions are processed starting from the **most recent** to **oldest**.\n\n```jsx\n\tfor i := range txs {\n\t\t\tfound := txs[i].Lt == oldestLT && txs[i].Hash() == tlb.Bits256(oldestHash)\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// early exit\n\t\t\tresult = append(result, txs[:i]...)\n\n\t\t\treturn result, nil\n\t\t}\n\n```\n\nWhen exiting early and processing without reversing, `ObserveInbound` continues with this logic:\n\n```go\ncase len(txs) > maxTransactionsPerTick:\n\tob.Logger().Inbound.Info().\n\t\tMsgf(\"observeGateway: got %d transactions. Taking first %d\", len(txs), maxTransactionsPerTick)\n\n\ttxs = txs[:maxTransactionsPerTick]\n\n```\n\n```jsx\n\tmaxTransactionsPerTick = 100\n```\n\nIf more than `maxTransactionsPerTick`  transactions are returned\u2014which is possible given the `pageSize` is set to 200\u2014only the **first 100 transactions** are processed. Because of the way the transactions are ordered, only the 100 most recent ones will be processed.\n\n### Impact\n\nAs a result, The next time `ObserveInbound` runs, it will **skip over the older transactions** that were previously unprocessed due to the `maxTransactionsPerTick` limit because `setLastScannedTx` points to a newer transaction, causing the previously skipped older ones to be bypassed entirely and never be processed causing loss of funds.\n\n### POC\n\n- `LastScannedTx` is set to `tx=0`\n- `GetTransactionsSince` retrieves 200 transactions, ordered from **newest to oldest**.\n- During processing, the last scanned transaction (`tx=0`) is found at the **130th position**, so the function returns early without reversing the list.\n- Since 130 > `maxTransactionPerTick`, only the **first 100 transactions** (i.e., the most recent ones) are processed.\n    - In this example, it will be transaction `30..130`\n- At the end of `ObserveInbound`, the pointer will be set to `130`\n- This means that, in the next round of `ObserveInbound`, transactions will be fetched from `130..` and thus, the transactions `0..29` will never be executed.\n- This is a total loss of funds for the users that submitted those transactions.\n### Recommendation\n\nEnsure that users do not risk losing their funds when interacting with the TON implemenation.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\n https://github.com/zeta-chain/node/pull/3958\n\n\n**gjaldon**\n\nThe early exit case in GetTransactionsSince() now [reverses the order](https://github.com/zeta-chain/node/blob/2c81997da374b8390d9d2fa48f3780d4433d3c07/zetaclient/chains/ton/rpc/client.go\\#L289-L296) of the results and fixes the issue.\n\n\n\n# Issue M-4: Restricted addresses are still able to use Zetachain due to faulty update \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/79 \n\n## Found by \n0x73696d616f\n\n### Summary\n\nZetachain has a restricted address list that stops inbounds from going through, [link](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/sui/observer/inbound.go#L190-L202).\n\nHowever, the restricted addresses book is not correctly set when it is updated later in a config file. [Here](https://github.com/zeta-chain/node/blob/29f26d469602094d8bf686b7e41275195b7b0b8d/cmd/zetaclientd/start.go#L67) a watcher is set to update the list when the file is updated. `config.go::loadRestrictedAddressesConfig()` is [called](https://github.com/zeta-chain/node/blob/29f26d469602094d8bf686b7e41275195b7b0b8d/zetaclient/config/config.go#L180) with the **old** config and the file to update the addresses list from.\n\n`config.go::loadRestrictedAddressesConfig()` shows that the new `addresses` [fetched](https://github.com/zeta-chain/node/blob/29f26d469602094d8bf686b7e41275195b7b0b8d/zetaclient/config/config.go#L111) from the file are never used, and the old config is used to set the restricted addresses, confirming that these addresses are not updated.\n```go\nfunc loadRestrictedAddressesConfig(cfg Config, file string) error {\n\tinput, err := os.ReadFile(file) // #nosec G304\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"reading file %s\", file)\n\t}\n\taddresses := []string{}\n\terr = json.Unmarshal(input, &addresses)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"invalid json\")\n\t}\n\n\trestrictedAddressBookLock.Lock()\n\tdefer restrictedAddressBookLock.Unlock()\n\n\t// Clear the existing map, load addresses from main config, then load addresses\n\t// from dedicated config file\n\tSetRestrictedAddressesFromConfig(cfg)\n\tfor _, addr := range cfg.ComplianceConfig.RestrictedAddresses {\n\t\trestrictedAddressBook[strings.ToLower(addr)] = true\n\t}\n\treturn nil\n}\n```\n\nThis will lead to catastrophic consequences as an attacker will not be blacklisted when they should and is able to exploit Zetachain how much they want. Essentially, this renders the blacklist mechanism useless and access control can be circumvented.\n\n### Root Cause\n\nIn `node/zetaclient/config/config.go:111`, the new restricted `addresses` from the file are not used.\n\n### Internal Pre-conditions\n\nNone.\n\n### External Pre-conditions\n\nNone.\n\n### Attack Path\n\n1. User is restricted by changing the config file by the nodes, but it won't apply and the user will not be blacklisted.\n\n### Impact\n\nAttacker can circumvent restricted address access control and keep using the SUI/EVM gateways.\n\n### PoC\n\nSee above.\n\n### Mitigation\n\nIterate through the new addresses fetched from the file to set the `restrictedAddressBook` map instead of using the old, stale config.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3971\n\n\n**gjaldon**\n\nThe [restricted addresses](https://github.com/zeta-chain/node/pull/3971/files\\#diff-1a5a944dc645a1e3bb2ab1785316638efc545f0d1fc0cd93778abc1c45588b6cR97-R134) from the loaded file are now being used.\n\n\n\n# Issue M-5: SUI TSS drained due to lack of refund when the `withdraw/withdraw_and_call()` PTB fails \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/80 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \n0x73696d616f, BrothersInCode\n\n### Summary\n\nFirstly, SUI [PTBs](https://docs.sui.io/concepts/transactions/prog-txn-blocks) or 'Programmable Transaction Blocks' are atomic, that is, whenever a transaction in the bundle reverts, the whole bundle fails\n> If one transaction command fails, the entire block fails and no effects from the commands are applied.\n\nThe `withdraw_and_call()` mechanism on SUI, relies on a PTB consisting of a withdrawal and a `on_call()` function bundled together, [link](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/sui/signer/signer_tx.go#L130)\n> a withdrawAndCall is a PTB transaction that contains a withdraw_impl call and a on_call call\n\nThe `withdraw()` function, used to refund the gas paid to the gateway, directly transfers it as the first tx of the PTB:\n```move\nentry fun withdraw<T>(\n    gateway: &mut Gateway,\n    amount: u64,\n    nonce: u64,\n    receiver: address,\n    gas_budget: u64,\n    cap: &WithdrawCap,\n    ctx: &mut TxContext,\n) {\n    let (coins, coins_gas_budget) = withdraw_impl<T>(gateway, amount, nonce, gas_budget, cap, ctx);\n\n    transfer::public_transfer(coins, receiver);\n    transfer::public_transfer(coins_gas_budget, tx_context::sender(ctx));\n\n    // Emit event\n    event::emit(WithdrawEvent {\n        coin_type: coin_name<T>(),\n        amount: amount,\n        sender: tx_context::sender(ctx),\n        receiver: receiver,\n        nonce: nonce,\n    });\n}\n```\n\nGiven these statements, it becomes clear that whenever the PTB fails on the `on_call()` step, the first `withdraw()` tx will also fail, and the gateway (TSS signer) doesn't get refunded, while having paid for the gas fees. This may happen without the failure on the node being caught when simulating the tx because on chain conditions on simulation and when it settles may be different (and can forcibly be different by an attacker frontrunning).\n\n\n\n### Root Cause\n\nIn `node/zetaclient/chains/sui/signer/signer_tx.go`, withdraw_and_call PTB will make the gateway run out of funds.\n\n### Internal Pre-conditions\n\nNone.\n\n### External Pre-conditions\n\nNone.\n\n### Attack Path\n\n1. User sends a cross chain withdrawal to SUI, triggering the `on_call()` tx that reverts.\n2. The node signs and sends the PTB, and at this point the tx in 1 doesn't revert (diff on chain conditions).\n3. The withdraw() tx goes through, but `on_call()` reverts, so the whole PTB is aborted and the gateway loses gas fees without getting refunded.\n\n### Impact\n\nTSS funds leak leading to insolvency and DoSed nodes.\n\n### PoC\n\nSee above.\n\n### Mitigation\n\nGas refund should be performed in a separate PTB so the TSS signer in the node always gets their gas refund.\n\n# Issue M-6: Wrong postGasPrice() implementation can lead to loss of funds \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/84 \n\n## Found by \n0x73696d616f, BrothersInCode\n\n## Description\n\nIn Sui\u2019s implementation, the gas price is committed to memory before posting it and before running validation on the gas price.\n\nBefore showing the code of Sui\u2019s implementation, it will make more sense if we look at the implementation of Ton, which is the correct way of implementing it.\n\nIn `ton/observer/observer.go::PostGasPrice()` :\nhttps://github.com/zeta-chain/node/blob/835341df556faefeb450770cceb6f959be4f32cc/zetaclient/chains/ton/observer/observer.go#L75\n```go\nfunc (ob *Observer) PostGasPrice(ctx context.Context) error {\n//.. ommitted code\n\t_, err = ob.ZetacoreClient().PostVoteGasPrice(ctx, ob.Chain(), gasPrice, priorityFee, blockNum)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to post gas price\")\n\t}\n\n\tob.setLatestGasPrice(gasPrice)\n//.. ommitted code\n}\n```\n\nWe can see that the gas price is **first** validated and voted on in `PostVoteGasPrice()` . If an error occurs in `PostVoteGasPrice()`, the function returns.\n\nIf it does not error, `gasPrice` will be set in memory using `ob.setLatestGasPrice()`\n\nThis is the correct way of implementing it.\n\nNow, let\u2019s take a look at Sui\u2019s implementation. If we take a look at `sui/observer/observer.go::PostGasPrice()` :\n\n```go\nfunc (ob *Observer) PostGasPrice(ctx context.Context) error {\n//.. ommitted code\n\tob.setLatestGasPrice(gasPrice)\n\n\t_, err = ob.ZetacoreClient().PostVoteGasPrice(ctx, ob.Chain(), gasPrice, priorityFee, epochNum)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to post vote for gas price\")\n\t}\n//.. ommitted code\n}\n```\n\nAs we can see, the `gasPrice` is set in memory **before** `PostVoteGasPrice()`. This means that when `PostVoteGasPrice()` fails, the memory will be pointing to an invalid `gasPrice`.\n\nThis invalid `gasPrice` will be used in Sui\u2019s implementation`VoteOutbound`:\n\n```go\n\nfunc (ob *Observer) VoteOutbound(ctx context.Context, cctx *cctypes.CrossChainTx) error {\n//.. ommitted code\n\toutboundGasPrice := math.NewInt(int64(ob.getLatestGasPrice()))\n//.. ommitted code\n}\n```\n\nAs such, outbound votes can be made with invalid gas parameters, leading to the honest validator performing slashable behavior which in turns can lead to loss of funds.\n\n## Proof of Concept\n\n- Alice calls the periodic `PostGasPrice()` function.\n- Alice sets the `current_gas_price` to `X`\n- When passing `X` to `PostVoteGasPrice()`, it errors and does not post the gas price, returning the `PostGrasPrice()`\n- However, the `current_gas_price` is set to `X` , which has not been voted on by other observers.\n- Alice will use this gas price in `VoteOutbound` , which leads to slashable behavior since these are parameters not voted on, which can lead to funds lost.\n- Furthermore, this can lead undefined behavior depending on the gas price used. Issues like these trickle down and can impact other facets of the zetaclient node.\n- The gas price must match across the active observer set for the vote to count toward consensus. This is why gas prices are pre-agreed upon through `MsgVoteGasPrice`  so when observers report the outbound, they\u2019re all expected to use the same gas price, ensuring determinism in fee logic, swaps, and minting.\n\n## Recommendation\n\n**Only** set the gas price after it has been posted & validated, just like done on the Ton implementation.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3967\n\n\n**gjaldon**\n\nThe [gas price is set](https://github.com/zeta-chain/node/pull/3967/files\\#diff-db16d9e80ec024a4a600eb8e2f95eae8f63dcf1de93f372251f046aaf07cae79R113-R114) only after voting on the gas price to fix the issue.\n\n\n\n# Issue M-7: Add/remove authorization messages are not usable due to missing registration \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/89 \n\n## Found by \n0xAlix2\n\n\n\n### Summary\n\nIn Cosmos SDK modules, `codec.go` is responsible for registering all message and interface types with both the Amino codec and the Protobuf interface registry. Every module message must be registered in both `RegisterInterfaces` and `RegisterCodec` (if legacy Amino support is required). This registration enables the Cosmos SDK to correctly decode incoming messages and route them to the appropriate `MsgServer` handlers.\n\nThe `node/x/authority` module defines five message handlers in its `MsgServer`: `UpdatePolicies`, `UpdateChainInfo`, `RemoveChainInfo`, `AddAuthorization`, and `RemoveAuthorization`. However, the `AddAuthorization` and `RemoveAuthorization` messages are missing from both `RegisterInterfaces` and `RegisterCodec`.\n\nAs a result, these messages are unusable.\n\n[`codec.go`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/authority/types/codec.go#L10-L24):\n\n```go\nfunc RegisterCodec(cdc *codec.LegacyAmino) {\n\tcdc.RegisterConcrete(&MsgUpdatePolicies{}, \"authority/UpdatePolicies\", nil)\n\tcdc.RegisterConcrete(&MsgUpdateChainInfo{}, \"authority/UpdateChainInfo\", nil)\n\tcdc.RegisterConcrete(&MsgRemoveChainInfo{}, \"authority/RemoveChainInfo\", nil)\n}\n\nfunc RegisterInterfaces(registry cdctypes.InterfaceRegistry) {\n\tregistry.RegisterImplementations((*sdk.Msg)(nil),\n\t\t&MsgUpdatePolicies{},\n\t\t&MsgUpdateChainInfo{},\n\t\t&MsgRemoveChainInfo{},\n\t)\n\n\tmsgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)\n}\n```\n\n### Root Cause\n\nThe `AddAuthorization` and `RemoveAuthorization` message types are missing from both registration functions in `codec.go`.\n\n### Internal Pre-conditions\n\nN/A\n\n### External Pre-conditions\n\nN/A\n\n### Attack Path\n\nN/A\n\n### Impact\n\nAny transaction attempting to call `MsgAddAuthorization` or `MsgRemoveAuthorization` will fail to be decoded or routed, rendering these messages completely unusable via CLI, gRPC, or transactions.\n\n\n### Proof of Concept\n\nN/A\n\n### Mitigation\n\nAdd the missing message types to `RegisterCodec` and `RegisterInterfaces` in `node/x/authority/types/codec.go`:\n\n```diff\nfunc RegisterCodec(cdc *codec.LegacyAmino) {\n\tcdc.RegisterConcrete(&MsgUpdatePolicies{}, \"authority/UpdatePolicies\", nil)\n\tcdc.RegisterConcrete(&MsgUpdateChainInfo{}, \"authority/UpdateChainInfo\", nil)\n\tcdc.RegisterConcrete(&MsgRemoveChainInfo{}, \"authority/RemoveChainInfo\", nil)\n+\tcdc.RegisterConcrete(&MsgAddAuthorization{}, \"authority/AddAuthorization\", nil)\n+\tcdc.RegisterConcrete(&MsgRemoveAuthorization{}, \"authority/RemoveAuthorization\", nil)\n}\n\nfunc RegisterInterfaces(registry cdctypes.InterfaceRegistry) {\n\tregistry.RegisterImplementations((*sdk.Msg)(nil),\n\t\t&MsgUpdatePolicies{},\n\t\t&MsgUpdateChainInfo{},\n\t\t&MsgRemoveChainInfo{},\n+\t\t&MsgAddAuthorization{},\n+\t\t&MsgRemoveAuthorization{},\n\t)\n\n\tmsgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)\n}\n```\n\nThis ensures the messages can be decoded, routed, and executed properly within the Cosmos SDK framework.\n\n\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3959\n\n\n**gjaldon**\n\n[Added](https://github.com/zeta-chain/node/pull/3959/files\\#diff-8ded7639d258e46b4943e2f1026e9241ad8c44e702a64ea89571e852944782bcR14-R15) the missing codecs to `RegisterCodec()` and `RegisterInterfaces()`.\n\n\n\n# Issue M-8: Malicious observer can block messages added through the inbound tracker \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/100 \n\n## Found by \n0xalpharush\n\n### Summary\n\nZetaclient stops processing the inbound tracker indefinitely after a panic caused by parsing transaction signatures greater than 64 bytes with [MustSignatureFromBase58](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound_tracker.go#L22). This causes following inbound messages to not be processed, leaving deposits that are processed via the inbound tracker blocked indefinitely. While this requires a \"privileged\" role, [every validator is an observer](https://www.zetachain.com/docs/developers/architecture/observers/) and BFT consensus is supposed to be byzantine tolerant i.e. tolerate <1/3 malicious parties. Thus, one malicious observer should not be able to cause panics that prevent processing inbound Solana cross-chain transactions via the inbound tracker, trapping user founds.\n\n### Root Cause\n\nThe Zetaclient task, [process_inbound_trackers](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/solana.go#L100), stops indefinitely after panicking due to parsing a transaction signature greater than 64 bytes with [MustSignatureFromBase58](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound_tracker.go#L22). This can be caused by sending an invalid `MsgAddInboundTracker` as its `ValidateBasic` implementation does not ensure Solana tx hashes are exactly 64 bytes. This blocks all future `MsgAddInboundTracker` messages, and, even upon restarting Zetaclient, a malicious observer can immediately send another `MsgAddInboundTracker` message to crash Zetaclient. Patching `ValidateBasic` is app-hash breaking and funds will remain locked in the bridge without the corresponding mint if using `MsgAddInboundTracker` is required to process the cross-chain transaction. Thus, this qualifies as a denial of service vector.\n\n### Internal Pre-conditions\n\n1. The Zetaclient task, [process_inbound_trackers](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/solana.go#L100), processes the malicious message from the [inbound tracker](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound_tracker.go#L15C2-L15C79) and crashes when it parses the tx has with [MustSignatureFromBase58](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound_tracker.go#L22).\n2. The panic recovery does not keep the task running following a panic as `defer t.setStopState()` also runs when a panic occurs, permanently stopping the task. This is clear by the [log](https://gist.github.com/0xalpharush/9704faa8edf5311c4a485036cc3c2d1b#file-zetaclient1-log-L1) `\"Ticker stopped\"` which indicates [`defer t.setStopState`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/ticker/ticker.go#L135) ran and will not restart the task until the Zetaclient process is restarted.\n\n### External Pre-conditions\n\n1. A malicious observer sends a [MsgAddInboundTracker with an invalid tx hash for the Solana chain](https://gist.github.com/0xalpharush/9704faa8edf5311c4a485036cc3c2d1b#file-node-diff-L19-L24).\n2. Another observer tries to add a Solana, cross-chain transaction to the inbound tracker and it's unable to be processed by other validators.\n\n### Attack Path\n\n1. A malicious observer sends a [MsgAddInboundTracker with an invalid tx hash for the Solana chain](https://gist.github.com/0xalpharush/9704faa8edf5311c4a485036cc3c2d1b#file-node-diff-L19-L24).\n2. The Zetaclient task, [process_inbound_trackers](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/solana.go#L100), process the malicious message from the [inbound tracker](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound_tracker.go#L15C2-L15C79) and crashes when it parses the tx has with [MustSignatureFromBase58](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound_tracker.go#L22).\n3. Pending deposits that need to be processed via inbound tracker are effectively blocked as validating and voting on inbound CCTX received via the inbound tracker has stopped.\n\n### Impact\n\nNew messages cannot be process through the inbound tracker without patching the issue. This means deposited funds would be stuck and users would not receive refunds or the assets they expected to mint indefinitely.\n\n### PoC\n\nTo exercise the inbound tracker,  we must first patch the solana observer\u2019s [ObserveInbound](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound.go#L29-L30) method [as is done to test the inbound tracker for the EVM](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/evm/observer/inbound.go#L109-L113). This simulates the functionality of adding a tx hash that was missed by the observers as the feature is intended to be used.\n\n1) Apply the [diff](https://gist.github.com/0xalpharush/9704faa8edf5311c4a485036cc3c2d1b#file-node-diff) containing the POC test to `node/`.\n2) Run `E2E_ARGS='--verbose --test-filter solana' make start-solana-test`\n3) Observe the logs of the zetaclients and orchestrator: `docker logs orchestrator --follow`, `docker logs zetaclient0 --follow`, `docker logs zetaclient1 --follow`. These are attached in the [gist](https://gist.github.com/0xalpharush/9704faa8edf5311c4a485036cc3c2d1b) for reference.\n4) We will notice the zetalcients panics and shuts down the inbound processor task, and, as a result, the test will be unable to process new messages via the inbound tracker with the error, `Error getting cctx by inboundHash: rpc error: code = NotFound desc = not found`. This indicates that inbound messages cannot be processed until the validators are upgraded to be robust against this DOS vector.\n\n### Mitigation\n\nModify `MsgAddInboundTracker`'s `ValidateBasic` implementation to ensure Solana tx hashes are exactly 64 bytes and that they only contain base58 characters. One reason to prefer this change is that a malicious observer could send very large based58 encoded strings as the tx hash currently, and that would stall zetaclient as base58 decoding is very slow. Enforcing the length to be valid addresses both DOS vectors.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3895\n\n\n\n\n# Issue M-9: Incorrect Formate for transaction building, and transaction executions In Solana will lead to revert all execute Operations. \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/113 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nAl-Qa-qa\n\n## Affected Code\n- https://github.com/zeta-chain/node/blob/a32ffd88ea9957c8f3424466bb358b4197d41889/zetaclient/chains/solana/signer/execute.go#L88-L89\n\n## Description\nWhen creating Execute Function in Solana, The Account structure us making `destination_program` with no `mut` flag (non-writable), and `destination_program_pda` with `mut` flag (writable)\n\n> programs/gateway/src/contexts.rs#Execute\n```rust\npub struct Execute<'info> {\n    ...\n    pub signer: Signer<'info>,\n    ...\n    pub pda: Account<'info, Pda>,\n\n    /// The destination program.\n    /// CHECK: This is arbitrary program.\n>>  pub destination_program: AccountInfo<'info>,\n    ...\n    #[account(\n>>      mut,\n        ....\n    )]\n>>  pub destination_program_pda: UncheckedAccount<'info>,\n}\n```\n\nBut when constructing the tx by Solana Nodes, they are doing the reverse, by making the `destination_program` as writable and `destination_program_pda` as non-writable\n\n> node/zetaclient/chains/solana/signer/execute.go#createExecuteInstruction\n```go\n\tpredefinedAccounts := []*solana.AccountMeta{\n\t\tsolana.Meta(signer.relayerKey.PublicKey()).WRITE().SIGNER(),\n\t\tsolana.Meta(signer.pda).WRITE(),\n\t\tsolana.Meta(msg.To()).WRITE(), <<---------\n\t\tsolana.Meta(destinationProgramPda), <<---------\n\t}\n```\n\nThis will result in All transactions gets reverted, as the `pda` account is the account designed to be changed in the execution of `on_call` and this can be seen cealy in `example` folders\n\n> programs/examples/connected/src/lib.rs#on_call\n```rust\n    pub fn on_call( ... ) -> Result<()> {\n>>      let pda = &mut ctx.accounts.pda;\n\n        // Store the sender's public key\n>>      pda.last_sender = sender;\n\n        // Convert data to a string and store it\n        let message = String::from_utf8(data).map_err(|_| ErrorCode::InvalidDataFormat)?;\n>>      pda.last_message = message;\n\n        // Transfer some portion of lamports transferred from gateway to another account\n>>      pda.sub_lamports(amount / 2)?;\n    }\n```\n\nDestination_PDA is the trusted account, that Cross Chain Apps depend on changing it. But as all Relayer txs are signed either in execute function, do not make it as writable, most of execute signed txs by the Node Relayers will get reverted as it will try to write on unwritable account leading to revert in execution\n\n## Recommendations\n- in `execute.go` remove WRITE from `msg.To`, which is the `destination_program`, and it it to `destinationProgramPda`\n```diff\ndiff --git a/node/zetaclient/chains/solana/signer/execute.go b/node/zetaclient/chains/solana/signer/execute.go\nindex daf4009..5fa0b31 100644\n--- a/node/zetaclient/chains/solana/signer/execute.go\n+++ b/node/zetaclient/chains/solana/signer/execute.go\n@@ -85,8 +85,8 @@ func (signer *Signer) createExecuteInstruction(msg contracts.MsgExecute) (*solan\n        predefinedAccounts := []*solana.AccountMeta{\n                solana.Meta(signer.relayerKey.PublicKey()).WRITE().SIGNER(),\n                solana.Meta(signer.pda).WRITE(),\n-               solana.Meta(msg.To()).WRITE(),\n-               solana.Meta(destinationProgramPda),\n+               solana.Meta(msg.To()),\n+               solana.Meta(destinationProgramPda).WRITE(),\n        }\n        allAccounts := append(predefinedAccounts, msg.RemainingAccounts()...)\n```\n\n\n\n# Issue M-10: removed observers are still able to vote \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/144 \n\n## Found by \n0xAlix2, BrothersInCode\n\n### Description\n\nAmong the various voting entry points, [`VoteTSS`](https://github.com/zeta-chain/node/blob/4c51775d71c8e313303dc4db125ac57acf793aad/x/observer/keeper/msg_server_vote_tss.go#L28) stands out by replacing the `IsNonTombstonedObserver` check and instead relying on the following validation:\n\n```go\n// Verifies that the signer is authorized by ensuring they have a node account.\n_, found := k.GetNodeAccount(ctx, msg.Creator)\nif !found {\n\treturn nil, errorsmod.Wrapf(\n\t\tsdkerrors.ErrorInvalidSigner,\n\t\t\"%s, signer %s does not have a node account set\", voteTSSid, msg.Creator)\n}\n```\n\n`GetNodeAccount` simply checks for the `nodeAccount` to be in store:\n\n```jsx\nfunc (k Keeper) GetNodeAccount(ctx sdk.Context, index string) (val types.NodeAccount, found bool) {\n\tstore := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.NodeAccountKey))\n\n\tb := store.Get(types.KeyPrefix(index))\n\tif b == nil {\n\t\treturn val, false\n\t}\n\n\tk.cdc.MustUnmarshal(b, &val)\n\treturn val, true\n}\n\n```\n\nThis ensures that only observers with valid node accounts can participate.\n\nCurrently, the **only** way a node account can be removed is through the `updateObserver` function which calls `removeNodeAccount`:\n\n```go\nfunc (k msgServer) UpdateObserver(\n\tgoCtx context.Context,\n\tmsg *types.MsgUpdateObserver,\n) (*types.MsgUpdateObserverResponse, error) {\n\t// ...\n\t// Remove the old node account to maintain consistency between observers and node accounts\n\tk.RemoveNodeAccount(ctx, msg.OldObserverAddress)\n\t// ...\n}\n\n```\n\nIt's important to note that `UpdateObserver` is invoked to replace an old observer when they are tombstoned. Therefore, the presence of the node account check inside `VoteTSS` effectively serves as an implicit tombstone check\u2014since tombstoned validators have their node accounts removed.\n\nThere is a flaw to this however.\n\nWhen an observer is removed from the observer set, `UpdateObserver` will always revert due to the following check:\n\n```go\n// We do not use IsNonTombstonedObserver here because we want to allow tombstoned observers to be updated\nif !k.IsAddressPartOfObserverSet(ctx, msg.OldObserverAddress) {\n\treturn nil, errorsmod.Wrapf(\n\t\ttypes.ErrNotObserver,\n\t\t\"Observer address is not authorized : %s\", msg.OldObserverAddress)\n}\n\n```\n\nThis prevents the `updateObserver` function from executing, which in turn means the observer\u2019s node account cannot be removed.\n\nObservers can be removed from the observer set through various hooks\u2014for example, in `CheckAndCleanObserver`, in case the observer does not meet the minimum required threshold it will be removed from the observer set:\n\n```go\ntokens := validator.TokensFromShares(delegation.Shares)\nif tokens.LT(minDelegation) {\n\tk.RemoveObserverFromSet(ctx, accAddress)\n}\n\n```\n\nIf this logic is triggered for an observer with a valid node account, they are removed from the observer set. However, since the `VoteTSS` function only checks for the presence of a node account\u2014and that account can no longer be removed via `updateObserver`\u2014the validator remains able to vote, even though they are no longer an authorized observer.\n\nNote that all other voting functions such as `VoteBlame` perform the `IsNonTombstonedObserver` check which correctly verifies whether the observer is still part of the observer set and therefore would have handled this case appropriately.\n\n```jsx\nfunc (k Keeper) IsNonTombstonedObserver(ctx sdk.Context, address string) bool {\n\tisPresentInMapper := k.IsAddressPartOfObserverSet(ctx, address)\n\tif !isPresentInMapper {\n\t\treturn false\n\t}\n//..code\n\n```\n\nThis contrasts with `VoteTSS` which does not perform this check at all.\n\n### Impact\n\nUltimately, if an observer is removed from the observer set they can perform `VoteTSS`.\n\n### POC\n\n- Bob, an active observer, currently holds a valid node account.\n- However, Bob fails to meet the minimum staking requirement and is subsequently removed from the observer set.\n- Despite this, Bob\u2019s node account remains intact, allowing him to continue submitting `VoteTSS` votes\u2014even though he is no longer a valid observer.\n\n### Recommendation\n\nEnsure that removed/tombstoned validators are unable to perform votes.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3917\n\n\n**gjaldon**\n\nThe issue has been fixed by [replacing all instances](https://github.com/zeta-chain/node/pull/3917/files\\#diff-38f29b114abcc1da10dbf7a503b0f5fce243866acdbc442034eec4b0b8c8c706R105-R106) of `IsNonTombstonedObserver()` with `CheckObserverCanVote()`, which also checks if a validator is jailed.\n\n\n\n# Issue M-11: DoSed SUI node on withdrawals to SUI due to lack of payload validation \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/177 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \n0x73696d616f\n\n### Summary\n\nSUI withdrawals do not handle incorrect payloads, only return with an error:\n```go\nfunc (s *Signer) buildWithdrawAndCallTx(\n\tctx context.Context,\n\tparams *cctypes.OutboundParams,\n\tcoinType,\n\tgasBudget,\n\twithdrawCapID,\n\tpayload string,\n) (models.TxnMetaData, error) {\n\t// decode and parse the payload to object the on_call arguments\n\tpayloadBytes, err := hex.DecodeString(payload)\n\tif err != nil {\n\t\treturn models.TxnMetaData{}, errors.Wrap(err, \"unable to decode payload hex bytes\")\n\t}\n\n\tvar cp sui.CallPayload\n\tif err := cp.UnpackABI(payloadBytes); err != nil {\n\t\treturn models.TxnMetaData{}, errors.Wrap(err, \"unable to parse withdrawAndCall payload\")\n\t}\n    ...\n}\n```\nWhen this happens, it will get stuck trying to schedule this cctx over and over again, without being able to process further txs as the nonce will be stale. Thus, withdrawals will be stuck and there is no way to recover.\nIn order to trigger this, an attacker simply has to send to the gateway on ZEVM, `r.GatewayZEVM.WithdrawAndCall0()`, an incorrect payload.\n\n### Root Cause\n\nIn `node/zetaclient/chains/sui/signer/signer_tx.go`, an incorrect payload is not handled and all txs will get stuck. It returns an error without further processing.\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/sui/signer/signer_tx.go#L140-L148\n\n\n### Internal Pre-conditions\n\nNone.\n\n### External Pre-conditions\n\nNone.\n\n### Attack Path\n\n1. Attacker calls `gatewayZEVM::withdrawAndCall()` with a `message` with incorrect payload to the SUI network.\n\n### Impact\n\nDoSed withdrawals on SUI.\n\n### PoC\n\nChange the `sui.go::SuiWithdrawAndCallSUI()` function to send an incorrect payload.\n```go\nfunc (r *E2ERunner) SuiWithdrawAndCallSUI(\n\treceiver string,\n\tamount *big.Int,\n\tpayload sui.CallPayload,\n) *ethtypes.Transaction {\n\treceiverBytes, err := hex.DecodeString(receiver[2:])\n\trequire.NoError(r, err, \"receiver: \"+receiver[2:])\n\n\t// ACT\n\t//payloadBytes, err := payload.PackABI()\n\t//require.NoError(r, err)\n\n\tvar fakePayload []byte = []byte(\"::::\")\n\n\ttx, err := r.GatewayZEVM.WithdrawAndCall0(\n\t\tr.ZEVMAuth,\n\t\treceiverBytes,\n\t\tamount,\n\t\tr.SUIZRC20Addr,\n\t\tfakePayload,\n\t\tgatewayzevm.CallOptions{\n\t\t\tIsArbitraryCall: false,\n\t\t\tGasLimit:        big.NewInt(20000),\n\t\t},\n\t\tgatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)},\n\t)\n\trequire.NoError(r, err)\n\n\treturn tx\n}\n```\n\nUncomment the withdraw and call sui test in `node/cmd/zetae2e/local/local.go`.\n\nAdd verbose to the makefile:\n```makefile\nstart-sui-test: e2e-images\n\t@echo \"--> Starting sui test\"\n\texport E2E_ARGS=\"${E2E_ARGS} --skip-regular --test-sui --verbose\" && \\\n\tcd contrib/localnet/ && $(DOCKER_COMPOSE) --profile sui up -d\n\n```\n\nRun `make start-sui-test` and watch the zetaclient0 and zetaclient1 being stuck on this withdrawal tx.\n\n### Mitigation\n\nRevert the tx back or increase the nonce.\n\n# Issue M-12: SUI receiver lacks validation \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/178 \n\n## Found by \n0x73696d616f, BrothersInCode, Laksmana\n\n### Summary\n\nThe SUI receiver is not validated in `node/pkg/chains/chain.go`, only encoded, leading to the message being stuck when it tries to withdraw in the zetaclient on the SUI chain. \n\nMore specifically, an attacker may send a receiver that is incorrectly encoded, such as using 33 bytes instead of 32, and it will keep trying to send the withdrawal in the Zetaclient but failing. \n\nAs the nonce is not increased because it errors, it will make all withdrawals stuck. Nodes may be slashed and not get rewards.\n\n### Root Cause\n\nIn `node/pkg/chains/chain.go:95`, '\"0x\" + hex.EncodeToString(b)' lacks validation.\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/chains/chain.go#L95\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. Attacker calls `GatewayZEVM::withdraw()` with an incorrect receiver (wrong length, for example).\n2. Zetaclient on SUI gets stuck on this withdrawal as `node/zetaclient/chains/sui/signer/signer_tx.go::buildWithdrawTx()` errors, returning an error of \"unable to create withdrawal tx builder\".\n\n### Impact\n\nDoSed withdrawals on SUI and loss of rewards for Zetaclient nodes.\n\n### PoC\n\nIn `node/e2e/e2etests/test_sui_withdraw.go::TestSuiWithdraw()`, replace \"signer.Address()\" with some random string that is not a correct SUI address (for example, add 1 byte to `signer.address()`. Then, run `make start-sui-test` in the node folder. Observe the logs in the orchestrator and zetaclients, they will be stuck.\n\n### Mitigation\n\nCancel the tx in case of receiver error of revert back to Zetachain to refund.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3945\n\n\n**gjaldon**\n\nThe [receiver address](https://github.com/zeta-chain/node/pull/3945/files\\#diff-3edf1bd777d0671d6eb9d5538582ea96db70ba99c9812c00af9305f4f6051faaR110-R236) is now properly validated and any invalid receiver will trigger the cancel transaction broadcasting.\n\n\n\n# Issue M-13: Removing an observer doesn't update an active ballot's voter list, leading to deadlocks \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/190 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \n0xAlix2, BrothersInCode\n\n### Summary\n\nWhen a ballot is initialized for voting, it takes a snapshot of the active observers and adds them to the voter list, along with recording the chain threshold, meaning X% of the voter list must vote to finalize the ballot.  \nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/keeper/voting.go#L62-L92:\n```go\nballot = types.Ballot{\n\tIndex:                \"\",\n\tBallotIdentifier:     index,\n\tVoterList:            observerSet.ObserverList,\n\tVotes:                types.CreateVotes(len(observerSet.ObserverList)),\n\tObservationType:      observationType,\n\tBallotThreshold:      cp.BallotThreshold,\n\tBallotStatus:         types.BallotStatus_BallotInProgress,\n\tBallotCreationHeight: ctx.BlockHeight(),\n}\n```\nAny observers who are not included in the above voter list are not allowed to vote on the ballot, as the vote will be rejected in `AddVote`.\n\nLater, when observers are removed due to slashing or unbonding (via hooks in `node/x/observer/keeper/hooks.go`), they are only removed from the observer set but **not** from the voter lists of active ballots.  \nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/keeper/hooks.go#L104-L106:\n```go\nfunc (k Keeper) CleanSlashedValidator(\n\tctx context.Context,\n\tvalAddress sdk.ValAddress,\n\tfraction sdkmath.LegacyDec,\n) error {\n\t// ... snip ...\n\tif resultingTokens.LT(mindelegation) {\n\t\tk.RemoveObserverFromSet(sdkCtx, accAddress.String())\n\t}\n\treturn nil\n}\n```\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/keeper/hooks.go#L117:\n```go\nfunc (k Keeper) CleanObservers(ctx context.Context, valAddress sdk.ValAddress) error {\n\t// ... snip ...\n\tk.RemoveObserverFromSet(sdkCtx, accAddress.String())\n\treturn nil\n}\n```\n\nIf enough observers are removed, the number of available voters might drop below the threshold needed to finalize the ballot, making it impossible to ever reach the required vote count, effectively deadlocking the active ballots.\n\n**NB:**  \nThis issue also exists for TSS votes: a TSS ballot expects 100% of the node accounts to vote.  \nIf any node account is updated after the ballot has been created, the ballot cannot be finalized anymore, similarly getting stuck forever.\n\n\n### Root Cause\n\nWhen an observer is removed from the observer set during an active ballot, the ballot's voter list is not updated accordingly.\nSince ballots expect a threshold of successful votes based on the original voter set size, removing observers (due to slashing or undelegation) can make it mathematically impossible to meet the threshold, causing the ballot to remain stuck and never finalize.\n\n### Internal Pre-conditions\n\n- A ballot is in progress (BallotStatus = `BallotInProgress`).\n- One or more voters in the ballot are later removed from the observer set (e.g., delegation drops below minimum).\n- Ballot threshold is based on the original number of voters.\n\n### External Pre-conditions\n\n- Delegators or slashing mechanisms cause an observer's self-delegation to fall below the minimum delegation threshold.\n- The observer set gets updated, but ballots referencing old observers are not updated.\n\n### Attack Path\n\n1. A ballot is created with an initial set of N observers.\n2. Some observers lose their self-delegation or are slashed and removed from the observer set.\n3. Because the ballot's `VoterList` is static and not updated, votes from the removed observers are still required.\n4. Remaining active observers can never reach the ballot's quorum.\n5. The ballot remains stuck indefinitely in `BallotInProgress`.\n\n### Impact\n\nBallots can get stuck forever, causing operational deadlocks, and all protocols depending on successful ballot finalization (e.g., blame handling, inbound/outbound tx validation) could freeze.\n\n### PoC\n\nAdd the following in `node/x/observer/keeper/voting_test.go`:\n```go\nfunc TestRemovedObserverDeadlock(t *testing.T) {\n\tk, ctx, sdkk, _ := keepertest.ObserverKeeper(t)\n\n\t// Create 5 observers\n\tr := rand.New(rand.NewSource(10))\n\tobservers := make([]stakingtypes.Validator, 5)\n\taccAddrs := make([]string, 5)\n\n\tfor i := range 5 {\n\t\tvalidator := sample.Validator(t, r)\n\t\tvalidator.DelegatorShares = sdkmath.LegacyNewDec(1)\n\t\tvalidator.Tokens = sdkmath.NewInt(1)\n\t\tsdkk.StakingKeeper.SetValidator(ctx, validator)\n\n\t\taccAddr, err := types.GetAccAddressFromOperatorAddress(validator.OperatorAddress)\n\t\trequire.NoError(t, err)\n\n\t\tminDelegation, err := types.GetMinObserverDelegationDec()\n\t\trequire.NoError(t, err)\n\n\t\tsdkk.StakingKeeper.SetDelegation(ctx, stakingtypes.Delegation{\n\t\t\tDelegatorAddress: accAddr.String(),\n\t\t\tValidatorAddress: validator.GetOperator(),\n\t\t\tShares:           minDelegation,\n\t\t})\n\n\t\tobservers[i] = validator\n\t\taccAddrs[i] = accAddr.String()\n\t}\n\n\t// Set all 5 as observers\n\tk.SetObserverSet(ctx, types.ObserverSet{ObserverList: accAddrs})\n\n\tthreshold, err := sdkmath.LegacyNewDecFromStr(\"0.75\")\n\trequire.NoError(t, err)\n\n\t// Set ChainParams\n\tchainID := getValidEthChainIDWithIndex(t, 0)\n\tk.SetChainParamsList(ctx, types.ChainParamsList{\n\t\tChainParams: []*types.ChainParams{\n\t\t\t{\n\t\t\t\tChainId:         chainID,\n\t\t\t\tIsSupported:     true,\n\t\t\t\tBallotThreshold: threshold,\n\t\t\t},\n\t\t},\n\t})\n\tindex := sample.ZetaIndex(t)\n\n\tmsgServer := keeper.NewMsgServerImpl(*k)\n\n\t// Observer 0 initiates a vote blame\n\tmsg := &types.MsgVoteBlame{\n\t\tCreator:   accAddrs[0],\n\t\tChainId:   chainID,\n\t\tBlameInfo: types.Blame{Index: index},\n\t}\n\t_, err = msgServer.VoteBlame(ctx, msg)\n\trequire.NoError(t, err)\n\n\t// Simulate observer1 losing delegation \u2014 set delegation to 0\n\tsdkk.StakingKeeper.SetDelegation(ctx, stakingtypes.Delegation{\n\t\tDelegatorAddress: accAddrs[1],\n\t\tValidatorAddress: observers[1].GetOperator(),\n\t\tShares:           sdkmath.LegacyZeroDec(),\n\t})\n\t// Remove observer1 from ObserverSet\n\terr = k.CheckObserverSelfDelegation(ctx, accAddrs[1])\n\trequire.NoError(t, err)\n\n\t// Simulate observer2 losing delegation \u2014 set delegation to 0\n\tsdkk.StakingKeeper.SetDelegation(ctx, stakingtypes.Delegation{\n\t\tDelegatorAddress: accAddrs[2],\n\t\tValidatorAddress: observers[2].GetOperator(),\n\t\tShares:           sdkmath.LegacyZeroDec(),\n\t})\n\t// Remove observer2 from ObserverSet\n\terr = k.CheckObserverSelfDelegation(ctx, accAddrs[2])\n\trequire.NoError(t, err)\n\n\t// Observer 3 and 4 can't vote blame\n\t_, err = msgServer.VoteBlame(ctx, &types.MsgVoteBlame{\n\t\tCreator:   accAddrs[3], // active observers\n\t\tChainId:   chainID,\n\t\tBlameInfo: types.Blame{Index: index},\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = msgServer.VoteBlame(ctx, &types.MsgVoteBlame{\n\t\tCreator:   accAddrs[4], // active observers\n\t\tChainId:   chainID,\n\t\tBlameInfo: types.Blame{Index: index},\n\t})\n\trequire.NoError(t, err)\n\n\t// Observers 1 and 2 can't vote blame\n\t_, err = msgServer.VoteBlame(ctx, &types.MsgVoteBlame{\n\t\tCreator:   accAddrs[1], // non-active observers\n\t\tChainId:   chainID,\n\t\tBlameInfo: types.Blame{Index: index},\n\t})\n\trequire.Error(t, err)\n\n\t_, err = msgServer.VoteBlame(ctx, &types.MsgVoteBlame{\n\t\tCreator:   accAddrs[2], // non-active observers\n\t\tChainId:   chainID,\n\t\tBlameInfo: types.Blame{Index: index},\n\t})\n\trequire.Error(t, err)\n\n\t// At this point the success percentage is 3/5 = 0.6\n\t// While the threshold is 0.75, so the threshold will never be reached\n\n\tballot, found := k.GetBallot(ctx, msg.Digest())\n\trequire.True(t, found)\n\trequire.Equal(t, types.BallotStatus_BallotInProgress, ballot.BallotStatus)\n}\n```\n\nThis PoC shows that after removing two observers mid-ballot, the threshold can no longer be reached, and the ballot remains stuck in `BallotInProgress`.\n\n### Mitigation\n\nWhen removing an observer from the observer set (e.g., during self-delegation checks or slashing events), the system should update all active ballots to also remove the observer from their `VoterList`.\n\n\n\n# Issue M-14: Abort processing after call originating from Solana will cause loss of funds \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/221 \n\n## Found by \nAl-Qa-qa, Goran\n\n### Summary\n\nSolana implementation of the gateway assumes abort address is a Solana account, when in reality abort is always processed on ZetaChain (EVM chain). That means user can only provide Solana account for the abort address. When node processes abort it will convert provided value into a 20-byte EVM address and send the funds there. Effectively funds are then permanently lost since user (or no-one else) has no private key for the resulting EVM address.\n\n### Root Cause\n\nZetaChain introduced `onAbort`, a mechanism to prevent funds getting stuck after failed cross-chain transactions. Abort mechanism is a final fail-safe, in which protocol will move funds to a user-defined abort address in a case that both cross-chain action and the revert processing fail. \n\nIn case call originates from Connected chain flow is: Connected::depositAndCall -> ZetaChain::onCall fails -> Connected::onRevert fails (ie. out-of-gas) -> ZetaChain::onAbort \n\nIn case call originates from ZetaChain flow is: ZetaChain::depositAndCall -> Connected::onCall fails -> ZetaChain::onRevert fails (ie. out-of-gas) -> ZetaChain::onAbort\n\n(Reference [here](https://www.zetachain.com/blog/zetachains-new-revert-handling-for-cross-chain))\n\nImportant to notice - `onAbort` is always processed on ZetaChain. So the `onAbort` target needs to be EVM address.\n\nLet's look at one of Solana gateway's entrypoints, ie. [deposit_and_call](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/lib.rs#L213):\n```rust\n    pub fn deposit_and_call(\n        ctx: Context<Deposit>,\n        amount: u64,\n        receiver: [u8; 20],\n        message: Vec<u8>,\n        revert_options: Option<RevertOptions>,\n    ) -> Result<()> {\n        instructions::deposit::handle_sol_with_call(\n            ctx,\n            amount,\n            receiver,\n            message,\n            revert_options,\n            DEPOSIT_FEE,\n        )\n    }\n```\n\nThe last parameter is revert_options. The structure is [defined](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/state.rs#L77) like this:\n```rust\npub struct RevertOptions {\n    pub revert_address: Pubkey,\n    pub abort_address: Pubkey,\n    pub call_on_revert: bool,\n    pub revert_message: Vec<u8>,\n    pub on_revert_gas_limit: u64,\n}\n```\n\nThe main thing to notice here is that `abort_address ` is defined as `Pubkey` (a Solana account) instead of `[u8; 20]` (a 20-bytes EVM address). So even though abort is always processed on ZetaChain, user can only provide Solana account as an abort address. As a result, protocol will send aborted funds to the wrong EVM address (the one resulting from the implicit conversion from the Solana account format), where they are permanently lost.\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n-\n\n### Impact\n\nInstead of securing the funds when abort is processed, funds get permanently lost.\n\n### PoC\n\nBug is demonstrated by modifiying the existing e2e test [SolanaDepositAndCallRevertWithDust](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/e2etests/test_solana_deposit_and_call_revert_with_dust.go):\n```diff\nfunc TestSolanaDepositAndCallRevertWithDust(r *runner.E2ERunner, args []string) {\n\trequire.Len(r, args, 0)\n\n\t// deposit the rent exempt amount which will result in a dust amount (after fee deduction) in the revert outbound\n\tdepositAmount := big.NewInt(constant.SolanaWalletRentExempt)\n\t\n+\t// @audit convert random EVM address to Solana format and use it as the abort address\n+\tevmAddress := common.HexToAddress(\"0x0397eE691dBf0f390DDeB7Edd328594e241CA9CB\")\n+       var abortPubkey solana.PublicKey\n+       copy(abortPubkey[:20], evmAddress[:])\n\n\t// ACT\n\t// execute the deposit and call transaction\n\tnonExistReceiver := sample.EthAddress()\n\tdata := []byte(\"dust lamports should abort cctx\")\n+\t// @audit add the abort address to the RevertOptions\n-       sig := r.SOLDepositAndCall(nil, nonExistReceiver, depositAmount, data, nil)\n+\tsig := r.SOLDepositAndCall(nil, nonExistReceiver, depositAmount, data,  &solanacontracts.RevertOptions{\n+\t\tAbortAddress: abortPubkey,\n+\t})\n\n\t// ASSERT\n\t// Now we want to make sure cctx is aborted.\n\tcctx := utils.WaitCctxAbortedByInboundHash(r.Ctx, r, sig.String(), r.CctxClient)\n\trequire.True(r, cctx.GetCurrentOutboundParam().Amount.Uint64() < constant.SolanaWalletRentExempt)\n\trequire.True(\n\t\tr,\n\t\tstrings.Contains(cctx.CctxStatus.ErrorMessageRevert, crosschaintypes.ErrInvalidWithdrawalAmount.Error()),\n\t)\n\t\n+\t// @audit check if evmAddress got the aborted funds\n+\tbalance, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, evmAddress)\n+\trequire.NoError(r, err)\n+\trequire.Equal(r, balance, depositAmount)\n}\n```\n\nNow let's run the modified test in the localnet:\n```bash\n\u276f make solana\n\u276f E2E_ARGS='--verbose --test-filter solana_deposit_and_call_revert_with_dust' make start-solana-test\n```\n\nAnd check logs:\n```bash\n\u276f docker logs -f orchestrator\n...\nsolana       | \ud83c\udfc3 starting Solana tests\nsolana       | [INFO] SolanaDeployerAddress: 37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ\nsolana       | \u23f3 running   - solana_deposit_and_call_revert_with_dust\nsolana       | [INFO] computed pda for gateway program: 9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik, bump 253\n\nsolana       | [INFO] Latest valid block height for tx 581\nsolana       | [INFO] Broadcast start\nsolana       | [INFO] Broadcast once start\nsolana       | [INFO] Broadcast success! tx sig wApP7UV6QFnjUbHb7RWi6RbEEGmJFGG5TGdEAHQJc5Y3MXVm9zPCkXU43Uq9fPHKL41UKRATYobT7wscuWTtgBE; waiting for confirmation...\nsolana       | [INFO] Current block height 444\nsolana       | [INFO] Broadcast once finished, tx: wApP7UV6QFnjUbHb7RWi6RbEEGmJFGG5TGdEAHQJc5Y3MXVm9zPCkXU43Uq9fPHKL41UKRATYobT7wscuWTtgBE, confirmed: true\nsolana       | [INFO] Tx broadcasted and confirmed\nsolana       | [INFO] deposit logs: [Program ComputeBudget111111111111111111111111111111 invoke [1] Program ComputeBudget111111111111111111111111111111 success Program ComputeBudget111111111111111111111111111111 invoke [1] Program ComputeBudget111111111111111111111111111111 success Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1] Program log: Instruction: DepositAndCall Program 11111111111111111111111111111111 invoke [2] Program 11111111111111111111111111111111 success Program log: Deposit executed: amount = 1000000, fee = 2000000, receiver = [53, 18, 47, 131, 192, 214, 203, 187, 121, 186, 136, 71, 61, 194, 162, 232, 14, 2, 212, 168], pda = 9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik, revert options = Some(RevertOptions { revert_address: 11111111111111111111111111111111, abort_address: F2bL9LEHkt7jRA75ajJKtBxvLz9LsaWGFFBAz4CN4m5, call_on_revert: false, revert_message: [], on_revert_gas_limit: 0 }) Program log: Deposit and call executed with message = [100, 117, 115, 116, 32, 108, 97, 109, 112, 111, 114, 116, 115, 32, 115, 104, 111, 117, 108, 100, 32, 97, 98, 111, 114, 116, 32, 99, 99, 116, 120] Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 50037 of 99700 compute units Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success]\nsolana       | [ERROR]\n\tError Trace:\t/go/delivery/zeta-node/e2e/e2etests/test_solana_deposit_and_call_revert_with_dust.go:53\n\tError:      \tNot equal:\n\t            \texpected: 0\n\t            \tactual  : 1000000\n\n\t            \tDiff:\n\t            \t--- Expected\n\t            \t+++ Actual\n\t            \t@@ -2,3 +2,4 @@\n\t            \t  neg: (bool) false,\n\t            \t- abs: (big.nat) {\n\t            \t+ abs: (big.nat) (len=1) {\n\t            \t+  (big.Word) 1000000\n\t            \t  }\n\nsolana       | [ERROR]Failure: (*E2ERunner).FailNow for runner \"solana\". Exiting\nsetup        | \u274c solana tests failed: context cancelled in solana_deposit_and_call_revert_with_dust after 17.940023758s\nsetup        | \u274c e2e tests failed after 23.218043511s\n```\n\nSo test failed as expected. We can use protocol's API to see errors reported with regards to abort processing:\n```bash\n\u276f curl http://localhost:1317/zeta-chain/crosschain/inboundHashToCctxData/wApP7UV6QFnjUbHb7RWi6RbEEGmJFGG5TGdEAHQJc5Y3MXVm9zPCkXU43Uq9fPHKL41UKRATYobT7wscuWTtgBE | jq '.CrossChainTxs[0].cctx_status.error_message_abort'\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n100  3693    0  3693    0     0  1476k      0 --:--:-- --:--:-- --:--:-- 1803k\n\"failed to process abort: {\\\"type\\\":\\\"contract_call_error\\\",\\\"message\\\":\\\"contract call failed when calling EVM with data\\\",\\\"error\\\":\\\"execution reverted: ret 0x: evm transaction execution failed\\\",\\\"method\\\":\\\"executeAbort\\\",\\\"contract\\\":\\\"0x733aB8b06DDDEf27Eaa72294B0d7c9cEF7f12db9\\\",\\\"args\\\":\\\"[0x0000000011111111111111111111111111111111 {[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 55] 0x48f80608B672DC30DC7e3dbBd0343c5F02C738Eb 900000 false 902 []}]\\\"}\"\n```\nFrom this output it can be deduced that the target of the `onAbort` call was `0x0000000011111111111111111111111111111111`, and not the actual address which was converted from EVM format to Solana format in the Go test.\n\nFinally, to confirm that aborted funds ended up in the incorrect address, permanently lost:\n```bash\n\u276f SOLZRC20=0x48f80608B672DC30DC7e3dbBd0343c5F02C738Eb\n\u276f ZETA=\"http://0.0.0.0:9545\"\n\u276f\n\u276f cast call -r $ZETA $SOLZRC20 \"balanceOf(address) (uint256)\" 0x0000000011111111111111111111111111111111\n900000\n```\n\n\n### Mitigation\n\nUse EVM compatible format for specifiying the abort address in the Solana gateway\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/protocol-contracts-solana/pull/110\n\n\n**gjaldon**\n\nThe [abort address](https://github.com/zeta-chain/protocol-contracts-solana/pull/110/files\\#diff-a6f54cba8e8d30b9684a829cad551b355ade6c66eb73183879d9c2d447adb407R86) format has been changed to a 20-element u8 array, which fixes the issue.\n\n\n\n# Issue M-15: The withdraw method in the Solana node doesn't increment the nonce when the withdrawal transaction fails \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/275 \n\n## Found by \nLaksmana\n\n### Summary\n\nThe missing `increment nonce` in the `withdraw` method can cause the nonce to get stuck when the `withdraw` transaction fails.\n\n### Root Cause\n\nIn the [signer.go](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/signer/signer.go#L341) file, both the `prepareWithdrawTx` and `prepareWithdrawSPLTx` functions do not include a `nonce increment` instruction.\n```go\nfunc (signer *Signer) prepareWithdrawTx(\n\tctx context.Context,\n\tcctx *types.CrossChainTx,\n\theight uint64,\n\tlogger zerolog.Logger,\n) (outboundGetter, error) {\n\tparams := cctx.GetCurrentOutboundParam()\n\t// compliance check\n\tcancelTx := compliance.IsCCTXRestricted(cctx)\n\tif cancelTx {\n\t\tcompliance.PrintComplianceLog(\n\t\t\tlogger,\n\t\t\tsigner.Logger().Compliance,\n\t\t\ttrue,\n\t\t\tsigner.Chain().ChainId,\n\t\t\tcctx.Index,\n\t\t\tcctx.InboundParams.Sender,\n\t\t\tparams.Receiver,\n\t\t\t\"SOL\",\n\t\t)\n\t}\n\n\t// sign gateway withdraw message by TSS\n\tmsg, err := signer.createAndSignMsgWithdraw(ctx, params, height, cancelTx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"createAndSignMsgWithdraw error\")\n\t}\n\n\treturn func() (*Outbound, error) {\n\t\t// sign the withdraw transaction by relayer key\n\t\tinst, err := signer.createWithdrawInstruction(*msg)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error creating withdraw instruction\")\n\t\t}\n\n\t\ttx, err := signer.signTx(ctx, inst, 0)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error signing withdraw instruction\")\n\t\t}\n\t\treturn &Outbound{Tx: tx}, nil\n\t}, nil\n}\n\nfunc (signer *Signer) prepareWithdrawSPLTx(\n\tctx context.Context,\n\tcctx *types.CrossChainTx,\n\theight uint64,\n\tlogger zerolog.Logger,\n) (outboundGetter, error) {\n\tparams := cctx.GetCurrentOutboundParam()\n\t// compliance check\n\tcancelTx := compliance.IsCCTXRestricted(cctx)\n\tif cancelTx {\n\t\tcompliance.PrintComplianceLog(\n\t\t\tlogger,\n\t\t\tsigner.Logger().Compliance,\n\t\t\ttrue,\n\t\t\tsigner.Chain().ChainId,\n\t\t\tcctx.Index,\n\t\t\tcctx.InboundParams.Sender,\n\t\t\tparams.Receiver,\n\t\t\t\"SPL\",\n\t\t)\n\t}\n\n\t// get mint details to get decimals\n\tmint, err := signer.decodeMintAccountDetails(ctx, cctx.InboundParams.Asset)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"decodeMintAccountDetails error\")\n\t}\n\n\t// sign gateway withdraw spl message by TSS\n\tmsg, err := signer.createAndSignMsgWithdrawSPL(\n\t\tctx,\n\t\tparams,\n\t\theight,\n\t\tcctx.InboundParams.Asset,\n\t\tmint.Decimals,\n\t\tcancelTx,\n\t)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"createAndSignMsgWithdrawSPL error\")\n\t}\n\n\treturn func() (*Outbound, error) {\n\t\t// sign the withdraw transaction by relayer key\n\t\tinst, err := signer.createWithdrawSPLInstruction(*msg)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error creating withdraw SPL instruction\")\n\t\t}\n\n\t\ttx, err := signer.signTx(ctx, inst, 0)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error signing withdraw SPL instruction\")\n\t\t}\n\n\t\treturn &Outbound{Tx: tx}, nil\n\t}, nil\n}\n\n```\n\nAs a result, when a withdrawal transaction fails, the nonce in the Solana program remains unchanged.\n\n\n\n### Internal Pre-conditions\n\n- \n\n### External Pre-conditions\n\nThe withdraw method transaction on Solana failed.\n\n### Attack Path\n\n- \n\n### Impact\n\nWhen a withdrawal transaction on Solana fails, the nonce will remain unchanged.\n\n### PoC\n\n- \n\n### Mitigation\n\nEnsure `withdraw` method on node include a `increment nonce`\n\n# Issue M-16: Protocol fee  is stuck in the `crosschain` module \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/280 \n\n## Found by \nAudinarey, Flashloan44\n\n### Summary\n\nWhen `PayGasInZetaAndUpdateCctx()` is called, the Zeta amount is swapped for `gasZRC20` and the protocolfees are added to this amount to determine the amount of Zeta tokens to mint to the `crosschain` module. However after the swap the protocol fees are left in the `crosschain` module.\n\n### Root Cause\n\nAs shown below, the `feeInZeta` is the sum of the protocol fees and the amount of zeta nedeed for the swap to `gasZRC20` which in turn is burned on L428 below, \n\nThe `feeInZeta` is minted on L399 to cover swapping and protocol fees, however after the swap, the protocol fee is left in the `crosschain` module and there is no way to retrieve it as there is no function to do this causing the fees accrued over time to be stuck in the module.\n\nThe only withdrawal that is implemented if `WithdrawFromGasStabilityPool()`\n\n```go\nFile: node/x/crosschain/keeper/gas_payment.go\n317: func (k Keeper) PayGasInZetaAndUpdateCctx(\n318: \tctx sdk.Context,\n319: \tchainID int64,\n320: \tcctx *types.CrossChainTx,\n321: \tzetaBurnt sdkmath.Uint,\n322: \tnoEthereumTxEvent bool,\n323: ) error {\n\n////SNIP \n374: \t// get the gas fee in Zeta using system uniswapv2 pool wzeta/gasZRC20 and adding the protocol fee\n375: \toutTxGasFeeInZeta, err := k.fungibleKeeper.QueryUniswapV2RouterGetZetaAmountsIn(ctx, outTxGasFee.BigInt(), gasZRC20)\n376: \tif err != nil {\n377: \t\treturn cosmoserrors.Wrap(err, \"PayGasInZetaAndUpdateCctx: unable to QueryUniswapV2RouterGetZetaAmountsIn\")\n378: \t}\n379: @>\tfeeInZeta := types.GetProtocolFee().Add(sdkmath.NewUintFromBigInt(outTxGasFeeInZeta))\n\n////SNIP\n\n398: \t{\n399: \t@>\tcoins := sdk.NewCoins(sdk.NewCoin(config.BaseDenom, sdkmath.NewIntFromBigInt(feeInZeta.BigInt()))) // amount iof zeta actualy needed for this tx\n400: \t\terr := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins)\n401: \t\tif err != nil {\n402: \t\t\treturn cosmoserrors.Wrap(err, \"PayGasInZetaAndUpdateCctx: unable to mint coins\")\n403: \t\t}\n404: \n405: \t@>\tamounts, err := k.fungibleKeeper.CallUniswapV2RouterSwapExactETHForToken(\n406: \t\t\tctx,\n407: \t\t\ttypes.ModuleAddressEVM,\n408: \t\t\ttypes.ModuleAddressEVM,\n409: \t\t\toutTxGasFeeInZeta,\n410: \t\t\tgasZRC20,\n411: \t\t\tnoEthereumTxEvent,\n412: \t\t)\n413: \t\tif err != nil {\n414: \t\t\treturn cosmoserrors.Wrap(\n415: \t\t\t\terr,\n416: \t\t\t\t\"PayGasInZetaAndUpdateCctx: unable to CallUniswapv2RouterSwapExactETHForToken\",\n417: \t\t\t)\n418: \t\t}\n419: \n420: \t\tctx.Logger().Info(\"gas fee\", \"outTxGasFee\", outTxGasFee, \"outTxGasFeeInZeta\", outTxGasFeeInZeta)\n421: \t\tctx.Logger().Info(\"CallUniswapv2RouterSwapExactETHForToken\",\n422: \t\t\t\"zetaAmountIn\", amounts[0],\n423: \t\t\t\"zrc20AmountOut\", amounts[1],\n424: \t\t)\n425: \n426: \t\t// FIXME: investigate small mismatches between amounts[1] and outTxGasFee\n427: \t\t// https://github.com/zeta-chain/node/issues/1303\n428: \t@>\terr = k.fungibleKeeper.CallZRC20Burn(ctx, types.ModuleAddressEVM, gasZRC20, amounts[1], noEthereumTxEvent)\n\n\n```\n\nAs shown below, the protocol fee is 2e18\n\n```go\nFile: node/x/crosschain/types/keys.go\n26: @>\tProtocolFee = 2000000000000000000\n\n///SNIP\n32: \n33: func GetProtocolFee() math.Uint {\n34: \treturn math.NewUint(ProtocolFee)\n35: }\n\n```\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/gas_payment.go#L379\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/gas_payment.go#L399\n\n### Internal Pre-conditions\n\nNIL\n\n### External Pre-conditions\n\nNIL\n\n### Attack Path\n\nThis does not need an attack path as it is a logic flaw explained in the root cause section\n\n### Impact\n\nThis leads to funds getting stuck in the module leading to a loss for the protocol\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nConsider implementing a function for the protocol fees to be retrieved from the `crosschain` module or better still transfer it to the protocl address in the execution flow still\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3734\n\n\n\n\n# Issue M-17: Observer rewards are less than expected due to not properly accounting for negative rewards \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/309 \n\n## Found by \nberndartmueller\n\n## Summary\n\nObservers receive slightly fewer rewards than they should due to not correctly accounting for negative rewards in the `BuildRewardsDistribution()` function.\n\n## Root Cause\n\nIn [`BuildRewardsDistribution()#124`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/types/ballot.go#L124), if an observer votes against the majority vote, it decrements the reward for that observer. It can even become negative, representing a penalty.\n\nHowever, the `totalRewardUnits` is not decremented in this case. This means that the total rewards are calculated incorrectly, as it does not account for the negative rewards accumulated from previous ballots.\n\n```go\n094: // BuildRewardsDistribution builds the rewards distribution map for the ballot\n095: // It returns the total rewards units which account for the observer block rewards\n096: func (m Ballot) BuildRewardsDistribution(rewardsMap map[string]int64) int64 {\n...   // [...]\n117:\n118: \tfor _, address := range m.VoterList {\n119: \t\tvote := m.Votes[m.GetVoterIndex(address)]\n120: \t\tif vote == majorityVote {\n121: \t\t\trewardsMap[address]++\n122: \t\t\ttotalRewardUnits++\n123: \t\t} else {\n124: \u274c\t\t    rewardsMap[address]--\n125: \t\t}\n126: \t}\n```\n\nAs a result, `totalRewardsUnits` will be larger than the actual votes that will be rewarded. Thus, the [`rewardPerUnit` will be smaller than it should be](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/emissions/abci.go#L191-L193). This means that observers will receive fewer rewards.\n\n## Internal Pre-conditions\n\n- Multiple matured ballots per block height\n- Some observers who cast the opposite vote to the majority vote, which results in a negative reward for them\n\n## External Pre-conditions\n\nN/A\n\n## Attack Path\n\nN/A\n\n## Impact\n\nObservers receive slightly fewer rewards than they should.\n\n## PoC\n\nAdd the following test to the end of the file [`ballot_test.go`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/types/ballot_test.go):\n\n```go\n// This test is for the following scenario:\n// Two ballots are voted on on the same block height. 4 voters/observers\n// Ballot 1: Successful. All voters voted for success.\n// Ballot 2: Successful. 3 voters voted for success, 1 voter voted for failure.\nfunc Test_BuildRewardsDistribution_MultipleBallots(t *testing.T) {\n\tballot1 := Ballot{\n\t\tVoterList: []string{\"Observer1\", \"Observer2\", \"Observer3\", \"Observer4\"},\n\t\tVotes: []VoteType{\n\t\t\tVoteType_SuccessObservation,\n\t\t\tVoteType_SuccessObservation,\n\t\t\tVoteType_SuccessObservation,\n\t\t\tVoteType_SuccessObservation,\n\t\t},\n\t\tBallotStatus: BallotStatus_BallotFinalized_SuccessObservation,\n\t}\n\tballot2 := Ballot{\n\t\tVoterList: []string{\"Observer1\", \"Observer2\", \"Observer3\", \"Observer4\"},\n\t\tVotes: []VoteType{\n\t\t\tVoteType_SuccessObservation,\n\t\t\tVoteType_SuccessObservation,\n\t\t\tVoteType_SuccessObservation,\n\t\t\tVoteType_FailureObservation,\n\t\t},\n\t\tBallotStatus: BallotStatus_BallotFinalized_SuccessObservation,\n\t}\n\n\tvar (\n\t\trewardsDistributeMap = map[string]int64{}\n\t\ttotalRewardsUnits    = int64(0)\n\t)\n\n\ttotalRewardsUnits = ballot1.BuildRewardsDistribution(rewardsDistributeMap)\n\ttotalRewardsUnits += ballot2.BuildRewardsDistribution(rewardsDistributeMap)\n\n\trequire.Equal(t, rewardsDistributeMap, map[string]int64{\n\t\t\"Observer1\": 2,\n\t\t\"Observer2\": 2,\n\t\t\"Observer3\": 2,\n\t\t\"Observer4\": 0,\n\t})\n\n\t// sum of votes that will be rewarded\n\tsum := int64(0)\n\tfor _, v := range rewardsDistributeMap {\n\t\tsum += v\n\t}\n\n\trequire.Equal(t, sum, totalRewardsUnits) // @audit-info Fails. Expected 6, got 7\n}\n```\n\nRun the test with the following command:\n\n```bash\ngo test -timeout 30s -run ^Test_BuildRewardsDistribution_MultipleBallots$ github.com/zeta-chain/node/x/observer/types\n```\n\nIt will fail with the following output:\n\n```bash\nError:      \tNot equal:\n                expected: 6\n                actual  : 7\nTest:       \tTest_BuildRewardsDistribution_MultipleBallots\n```\n\nThe test shows that across those two ballots, 6 successful votes were cast, but the total rewards were calculated as 7. This leads to a smaller `rewardPerUnit` and thus a smaller reward for each observer.\n\n## Mitigation\n\nReconsider the logic in `BuildRewardsDistribution()` and ensure the total rewards are correctly calculated across multiple ballots.\n\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3904\n\n\n**gjaldon**\n\nThe total rewards units is now summed [separately](https://github.com/zeta-chain/node/pull/3904/files), and the issue is fixed.\n\n\n\n# Issue M-18: Setting `RevertGasLimit` to a value larger than `uint64` in the Solidity gateway contract will cause a panic that prevents finalizing the inbound CCTX and observers from receiving block rewards \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/310 \n\n## Found by \nberndartmueller\n\n## Summary\n\nThe `RevertGasLimit` is a `uint256` in Solidity. When calling `cctx.RevertOptions.RevertGasLimit.Uint64()` in `GetRevertGasLimit()`, it will panic if the value does not fit into a `uint64`.\n\nAs a result, the finalizing vote that executes the `MsgVoteInbound` message will fail. The ballet will remain pending, and observers will not receive block rewards for those pending ballots.\n\n## Root Cause\n\nIn [`GetRevertGasLimit()#L72`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_utils.go#L72), the `RevertGasLimit` is a [`cosmossdk_io_math.Uint`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/types/cross_chain_tx.pb.go#L702) and possibly [set to a value of size `uint256` in the Solidity gateway contract](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/Revert.sol#L15).\n\n```go\n66: // GetRevertGasLimit returns the gas limit for the revert transaction in a CCTX\n67: // It returns 0 if there is no error but the gas limit can't be determined from the CCTX data\n68: func (k Keeper) GetRevertGasLimit(ctx sdk.Context, cctx types.CrossChainTx) (uint64, error) {\n69: \t// with V2 protocol, reverts on connected chains can eventually call a onRevert function which can require a higher gas limit\n70: \tif cctx.ProtocolContractVersion == types.ProtocolContractVersion_V2 && cctx.RevertOptions.CallOnRevert &&\n71: \t\t!cctx.RevertOptions.RevertGasLimit.IsZero() {\n72: \u274c\t\treturn cctx.RevertOptions.RevertGasLimit.Uint64(), nil\n73: \t}\n```\n\nHowever, `Uint64()` will panic if the value does not fit into a `uint64`.\n\n```go\nFile: cosmossdk.io/math@v1.4.0/uint.go\n72: // Uint64 converts Uint to uint64\n73: // Panics if the value is out of range\n74: func (u Uint) Uint64() uint64 {\n75: \tif !u.i.IsUint64() {\n76: \t\tpanic(\"Uint64() out of bound\")\n77: \t}\n78: \treturn u.i.Uint64()\n79: }\n```\n\nThis panic will cause the whole Cosmos SDK `MsgVoteInbound` message to fail. As a result, the finalizing vote that will trigger this code path will have the message fail. Previous votes on the same ballot will be processed and stored.\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\n1. On Ethereum, call the `GatewayEVM.depositAndCall()` function with `onRevertGasLimit` set to a value larger than `uint64`. The target contract on ZEVM should revert for this inbound.\n2. Observers vote on this inbound\n3. The observer with the final vote will execute the inbound CCTX on ZEVM\n4. On ZEVM, the target function reverts so that a revert outbound is attempted to be created.\n5. `GetRevertGasLimit()` panics\n6. The inbound and hence the corresponding ballot remains pending\n7. Observers who voted on the inbound will not receive block rewards\n\n## Impact\n\nObservers will vote on the inbound, and the ballot will track the votes. Finalizing the ballot is not possible. It remains pending. Hence, observers will [not receive block rewards for those pending ballots](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/types/ballot.go#L97-L100), even though they have voted and thus spent gas.\n\n## PoC\n\nAdapt the existing E2E test [`e2e/e2etests/test_eth_deposit_and_call_revert_with_call.go`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/e2etests/test_eth_deposit_and_call_revert_with_call.go):\n\n```diff\ndiff --git a/node/e2e/e2etests/test_eth_deposit_and_call_revert_with_call.go b/node/e2e/e2etests/test_eth_deposit_and_call_revert_with_call.go\nindex db7fd26ca..2868d4dda 100644\n--- a/node/e2e/e2etests/test_eth_deposit_and_call_revert_with_call.go\n+++ b/node/e2e/e2etests/test_eth_deposit_and_call_revert_with_call.go\n@@ -3,6 +3,7 @@ package e2etests\n import (\n \t\"math/big\"\n\n+\tsdkmath \"cosmossdk.io/math\"\n \t\"github.com/ethereum/go-ethereum/accounts/abi/bind\"\n \t\"github.com/stretchr/testify/require\"\n \t\"github.com/zeta-chain/protocol-contracts/pkg/gatewayevm.sol\"\n@@ -29,7 +30,7 @@ func TestETHDepositAndCallRevertWithCall(r *runner.E2ERunner, args []string) {\n \t\t\tRevertAddress:    r.TestDAppV2EVMAddr,\n \t\t\tCallOnRevert:     true,\n \t\t\tRevertMessage:    []byte(payload),\n-\t\t\tOnRevertGasLimit: big.NewInt(200000),\n+\t\t\tOnRevertGasLimit: sdkmath.NewUintFromString(\"184467440737095516160\").BigIntMut(),\n \t\t},\n \t)\n```\n\nRun the test:\n\n```bash\nzetae2e run eth_deposit_and_call_revert_with_call --config cmd/zetae2e/config/local.yml --verbose\n```\n\nDocker container logs show that the Cosmos SDK message panics! This demonstrates that the inbound CCTX (initiated on Ethereum) cannot be finalized on ZetaChain.\n\n## Mitigation\n\nConsider using `cctx.RevertOptions.RevertGasLimit.BigInt().Uint64()`, which safely truncates the value to 64 bits. However, it would be better to update the Solidity code to use `uint64` instead of `uint256` for the revert gas limit.\n\n```diff\nfunc (k Keeper) GetRevertGasLimit(ctx sdk.Context, cctx types.CrossChainTx) (uint64, error) {\n\t// with V2 protocol, reverts on connected chains can eventually call a onRevert function which can require a higher gas limit\n\tif cctx.ProtocolContractVersion == types.ProtocolContractVersion_V2 && cctx.RevertOptions.CallOnRevert &&\n\t\t!cctx.RevertOptions.RevertGasLimit.IsZero() {\n\t\tfmt.Println(\"cctx.RevertOptions.RevertGasLimit\", cctx.RevertOptions.RevertGasLimit)\n-\t\treturn cctx.RevertOptions.RevertGasLimit.Uint64(), nil\n+\t\treturn cctx.RevertOptions.RevertGasLimit.BigInt().Uint64(), nil\n\t}\n```\n\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/protocol-contracts/pull/510\n\n\n**gjaldon**\n\nThis [PR](https://github.com/zeta-chain/protocol-contracts/pull/510/files) addresses the issue by setting a MAX_REVERT_GAS_LIMIT of 2M for all calls that support reverts in GatewayEVM and GatewayZEVM.\n\n\n\n# Issue M-19: The `sender` argument for the Solana `OnCall` instruction can be impersonated \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/312 \n\n## Found by \n0x15, 0xkmr\\_, OhmOudits, berndartmueller, ch13fd357r0y3r, shaflow01\n\n## Summary\n\nThe `sender` argument for the Solana `OnCall` instruction can be impersonated due to not being included in the message signed by the observers. This allows an attacker to call the destination program with a different `sender` address than the one originally initiated the CCTX.\n\n## Root Cause\n\nThe `sender` argument for [`handle_spl_token()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/instructions/execute.rs#L100) and [`handle_sol()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/instructions/execute.rs#L40) is provided to the called destination program as part of the `OnCall` instruction. For example, authorization checks can be implemented to verify the original CCTX sender.\n\nHowever, the `sender` argument is not included in the message signed by the observers. This means the `sender` can be exchanged to any other address when calling the gateway's `execute()` or `execute_spl_token()` function. As a result, the `sender` can be impersonated by an attacker, allowing them to call the destination program with a different `sender` address than the one originally initiated the CCTX.\n\nAs Solana does not have a public mempool, exploiting these issues is difficult. However, a single observer could exploit this by frontrunning the outbound Solana transaction and replacing the `sender`. Or, the Solana RPC operator or the validator could also exploit it.\n\n## Internal Pre-conditions\n\n- A single malicious observer\n\n## External Pre-conditions\n\n- The destination Solana program has authorization checks that depend on the `sender` argument and distribute funds based on it.\n\n## Attack Path\n\n1. Attacker sends an outbound CCTX from ZetaChain to Solana to call a specific destination program. The attacker's address will be used as the CCTX sender.\n2. Observers vote on the outbound CCTX.\n3. The observer colludes with the attacker and replaces the `sender` (the attacker's address) with their target address that should be impersonated.\n4. The Solana transaction is executed, and the destination program is called with the impersonated `sender` address.\n5. Destination program checks the `sender` address and authorizes funds transfer to the impersonated address.\n\n## Impact\n\nThe `sender` argument for the `OnCall` instruction can be impersonated.\n\n## PoC\n\nN/A\n\n## Mitigation\n\nThe `sender` argument should be included in the message signed by the observers.\n\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3896\n\n\n**gjaldon**\n\nThe issue has been fixed by [including](https://github.com/zeta-chain/node/pull/3896/files\\#diff-367bfa6b34b8ec2e1f482eafb11e80a929d53795c766224f7b36df40396a3c1fR342-R346) the CCTX sender in the message hash.\n\n\n\n# Issue M-20: Claiming delegation rewards via the precompile can result in a loss of ZETA rewards due to state synchronization issues \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/316 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nberndartmueller\n\n## Summary\n\nWhen claiming Cosmos delegation rewards, both explicitly and implicitly, the ZETA rewards are not added to the EVM statedb of the caller contract. This leads to a loss of ZETA rewards when the EVM state is committed to the Cosmos state.\n\n## Root Cause\n\nWhen withdrawing Cosmos delegation rewards via the `claimRewards` precompile method by [calling `c.distributionKeeper.WithdrawDelegationRewards()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/staking/method_claim_rewards.go#L54-L61), ZETA rewards [will be skipped](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/staking/method_claim_rewards.go#L68-L70) from the ZRC20 unlocking process.\n\nHowever, the received ZETA bank coins are not added to the EVM statedb, which is then out-of-sync with the Cosmos state. And as the EVM state is [committed to the Cosmos state at the end of the EVM execution](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/ethermint/x/evm/keeper/state_transition.go#L434-L439), it will overwrite the ZETA bank balance in the Cosmos state with the EVM state, resulting in a loss of ZETA reward coins.\n\nThis is similar to the mitigation in the `Stake()` precompile method that [subtracts the staked ZETA coins from the EVM statedb](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/staking/method_stake.go#L87) to ensure that the EVM state is in sync with the Cosmos state and to prevent double-spending.\n\n## Internal Pre-conditions\n\nN/A\n\n## External Pre-conditions\n\n- A ZEVM contract interacts with the staking precompile\n\n## Attack Path\n\nN/A\n\n## Impact\n\nThe claimed ZETA bank coin rewards are lost.\n\n## PoC\n\n1. A ZEVM contract delegates ZETA coins to a validator\n2. After a while, when delegation rewards are accumulated, the contract\n   - updates its storage variable (so that a state object is added to the EVM statedb). This leads to the ZETA balance at the time of the state change being cached.\n   - calls the `claimRewards` precompile method.\n3. ZETA coin rewards are received in the Cosmos state\n4. The EVM execution finishes, and the EVM state is committed to the Cosmos state\n5. The EVM state overwrites the Cosmos ZETA bank balance of the contract with the previously cached balance\n6. ZETA rewards are lost\n\n## Mitigation\n\n[Similar to Evmos](https://github.com/evmos/evmos/blob/0402a01c9036b40a7f47dc2fc5fb4cfae019f5f1/precompiles/distribution/tx.go#L90-L99), explicitly add the claimed ZETA coin rewards to the withdraw address balance in the EVM statedb.\n\nSimilarly, this is also required for the stake and unstake precompile functions, as they will [implicitly auto-withdraw delegation rewards](https://github.com/cosmos/cosmos-sdk/blob/7b9d2ff98d02bd5a7edd3b153dd577819cc1d777/x/distribution/keeper/hooks.go#L136-L152).\n\n\n# Issue M-21: V1 ZEVM logs incorrectly use the `emittingContract` address as the CCTX `sender` address \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/318 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nberndartmueller\n\n## Summary\n\nThe `ProcessZRC20WithdrawalEvent()` and `ProcessZetaSentEvent()` functions use the `emittingContract` address as the `to` address as the CCTX `sender` when processing v1 ZEVM logs.\n\nThis is not necessarily the contract that initiated the CCTX on ZEVM, which can lead to revert refunds going to the wrong address and access protection issues on the receiving contract.\n\n## Root Cause\n\nWhen processing ZEVM v1 logs, [`ProcessZRC20WithdrawalEvent()#L195`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_hooks.go#L195) uses the `emittingContract` address as the `to` address as the CCTX `sender`.\n\n```go\n156: func (k Keeper) ProcessZRC20WithdrawalEvent(\n157: \tctx sdk.Context,\n158: \tevent *zrc20.ZRC20Withdrawal,\n159: \temittingContract ethcommon.Address,\n160: \ttxOrigin string,\n161: ) error {\n...   // [...]\n193: \tmsg := types.NewMsgVoteInbound(\n194: \t\t\"\",\n195: \u274c\t\temittingContract.Hex(),\n```\n\n[Same as `ProcessZetaSentEvent()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_hooks.go#L279).\n\nThis `sender` address will be used in a few places. For example,\n\n- as the [`zetaTxSenderAddress`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/evm/signer/sign.go#L35) address that is [passed to the external chain when calling `ZetaConnector.onReceive()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/evm/legacy/ZetaConnector.eth.sol#L56), used for authentication.\n- and in V2 also as the [revert address](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/types/cctx.go#L145) that receives the funds in case of the revert process.\n\nThe issue is that the `emittingContract` is not necessarily the contract that initiated the CCTX on ZEVM.\n\nIt will be either set to\n\n- [`msg.To`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_hooks.go#L55) when regularly processing ZEVM logs. Which is the first contract that was called in the ZEVM transaction.\n- [the inbound receiver address](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_deposit.go#L141) in `HandleEVMDeposit`\n- the [outbound revert receiver address](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go#L439) in `processFailedOutboundV2` when reverting a CCTX on ZEVM\n\nThose addresses would be the CCTX sender. However, it could be a nested contract call that originated the CCTX, which is the rightful sender.\n\n### Tl;dr\n\nUsing an intermediary contract on the zEVM that initiates outbound CCTXs causes refund and access protection issues on the receiving contract.\n\n## Internal Pre-conditions\n\nN/A\n\n## External Pre-conditions\n\nN/A\n\n## Attack Path\n\nN/A\n\n## Impact\n\n- Revert refunds go to the wrong address\n- Access protection issues on the receiving contract when checking `zetaTxSenderAddress`\n\n## PoC\n\nN/A\n\n## Mitigation\n\n- `ProcessZRC20WithdrawalEvent()`: Use `ZRC20Withdrawal.From` as the `sender` address\n- `ProcessZetaSentEvent()`: Use [`zetaTxSenderAddress`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/zevm/legacy/ZetaConnectorZEVM.sol#L30) as the `sender` address\n\n\n# Issue M-22: Lost funds on ZetaChain due to unprocessed ZEVM logs \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/319 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nberndartmueller\n\n## Summary\n\nIn some instances, ZEVM logs that are emitted to signal the creation of outbound CCTXs are not processed, burning the user's funds on ZetaChain while not withdrawing them on the external chain.\n\n## Root Cause\n\nIn the following two instances,\n\n- [`processFailedZETAOutboundOnZEVM()#L335-344`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go#L335-L344) - Used to revert a failed ZETA outbound that errored on an external chain and which was initiated on ZetaChain (ZEVM)\n- [`HandleEVMDeposit()#L58-71`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_deposit.go#L58-L71) - Handles a ZETA deposit from an external EVM chain to ZetaChain (ZEVM)\n\nthe ZEVM on ZetaChain is invoked, and a target contract is called.\n\nThis contract might as well interact with ZetaChain's ZEVM gateway contract to initiate subsequent outbound CCTXs. For example, calling [`GatewayZEVM.withdraw()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/zevm/GatewayZEVM.sol#L183-L211), which burns the user's ZRC-20 tokens and emits `Withdrawn`.\n\nHowever, logs are not processed in those two instances, meaning no outbound CCTX is created, and the burned funds are not withdrawn on the external chain; they remain burned on ZetaChain. The user loses their funds.\n\nThis is in contrast to other situations where ZEVM logs are processed. [For example, when depositing gas or ERC-20 tokens from an external chain to ZetaChain](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_deposit.go#L108-L158).\n\n## Internal Pre-conditions\n\nN/A\n\n## External Pre-conditions\n\nN/A\n\n## Attack Path\n\n1. [User sends ZETA tokens from an external chain (e.g., Ethereum) to ZetaChain (ZEVM) with a contract call on ZetaChain](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/evm/legacy/ZetaConnector.eth.sol#L30-L48)\n2. On ZetaChain, the user's target contract interacts with the ZEVM gateway contract to initiate an outbound CCTX. ZRC-20 tokens are burned\n3. The emitted log is not processed, preventing the outbound CCTX from being created\n4. The user/contract on ZEVM loses their funds\n\n## Impact\n\nFunds are burned on ZetaChain while not creating an outbound CCTX.\n\n## PoC\n\nN/A\n\n## Mitigation\n\nConsider always processing ZEVM logs to create outbound CCTXs.\n\n\n# Issue M-23: Impartially aborted CCTXs are incorrectly marked as refunded, resulting in a loss of funds \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/320 \n\n## Found by \n0xAlix2, BrothersInCode, berndartmueller\n\n## Summary\n\nInbound CCTXs for either paused ZRC20 tokens or those that exceed the ZRC20 liquidity cap during the abort process will not be refunded to the user, manifesting as a loss of funds.\n\n## Root Cause\n\nIn the case of a CCTX that gets aborted on ZetaChain, the parent caller function `ProcessAbort()` [sets `cctx.CctxStatus.IsAbortRefunded = true`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/abort.go#L102), regardless of whether the [abort deposit succeeded or not](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/deposits.go#L203-L234).\n\nFor example, if [`getAndCheckZRC20()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/deposits.go#L202-L212) errors, due to\n\n- temporarily paused ZRC20 token, or, more likely,\n- ZRC20 liquidity cap reached (especially problematic if a user sent an inbound with a large ZRC20 amount, which exceeds the liquidity cap)\n\nthe CCTX amount is not refunded.\n\nAdditionally, a ZetaChain admin is also unable to manually refund such an aborted CCTX via the `MsgRefundAbort` message [due to the `CctxStatus.IsAbortRefunded` flag being set to `true`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/msg_server_refund_aborted_tx.go#L43-L46).\n\n## Internal Pre-conditions\n\n- CCTX needs to be aborted on ZetaChain\n- Inbound with a large ZRC20 amount is sent to ZetaChain, which errors during the [ZEVM deposit](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_deposit.go#L27) and the amount exceeds the liquidity cap\n\n## External Pre-conditions\n\nN/A\n\n## Attack Path\n\nN/A\n\n## Impact\n\nCCTX is incorrectly marked as aborted without refunding the user\n\n## PoC\n\n## Mitigation\n\nEnsure that the CCTX is only marked as `IsAbortRefunded = true` if the refund was successful.\n\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3901\n\n\n**gjaldon**\n\nThe issue has been [addressed](https://github.com/zeta-chain/node/pull/3901/files\\#diff-6c6b704f695ab2e9a14e02b6250c74d01482566e72f7afaf1c25bfecda5bbda6R97-R100) by marking the CCTX as AbortRefunded only when there was no error during the abort process or when only the OnAbort callback failed.\n\n\n\n# Issue M-24: ZETA coin type outbound CCTXs can be sent to unsupported external chains, which results in a DoS of the outbound queue \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/321 \n\n## Found by \nberndartmueller\n\n## Summary\n\nChecking whether an external chain supports ZETA by checking if the `zeta_token_contract_address` is set to the empty string (`\"\"`) is ineffective, as the actual value is set to the string `\"0x0000000000000000000000000000000000000000\"`.\n\nThis allows sending ZETA to external chains that cannot process it and error, blocking subsequent CCTXs.\n\n## Root Cause\n\nWhen processing an inbound CCTX (`MsgVoteInbound`), in [`VoteOnInboundBallot()#L58-L60`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/keeper/vote_inbound.go#L58-L60), it prevents outbound CCTX with the ZETA cointype to an external chain that does not have a ZETA token (e.g., Bitcoin, Solana, etc.):\n\n```go\n58: if coreParams.ZetaTokenContractAddress == \"\" && coinType == coin.CoinType_Zeta {\n59: \treturn false, false, types.ErrInvalidZetaCoinTypes\n60: }\n```\n\nSimilarly, this [check](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_hooks.go#L263-L265) is also done when sending an outbound CCTX from ZEVM to an external chain.\n\n**Notice that it checks for the empty string.**\n\nRetrieving the current production chain params via https://zetachain.blockpi.network/lcd/v1/public/zeta-chain/observer/get_chain_params, we can see that, for example, the config for Bitcoin (chain id 8332) has `zeta_token_contract_address` set to the **string** `\"0x0000000000000000000000000000000000000000\"`:\n\n```json\n{\n  \"chain_id\": \"8332\",\n  \"confirmation_count\": \"2\",\n  \"gas_price_ticker\": \"300\",\n  \"inbound_ticker\": \"20\",\n  \"outbound_ticker\": \"60\",\n  \"watch_utxo_ticker\": \"30\",\n  \"zeta_token_contract_address\": \"0x0000000000000000000000000000000000000000\",\n  \"connector_contract_address\": \"0x0000000000000000000000000000000000000000\",\n  \"erc20_custody_contract_address\": \"0x0000000000000000000000000000000000000000\",\n  \"outbound_schedule_interval\": \"10\",\n  \"outbound_schedule_lookahead\": \"30\",\n  \"ballot_threshold\": \"0.660000000000000000\",\n  \"min_observer_delegation\": \"10000000000000000000.000000000000000000\",\n  \"is_supported\": true,\n  \"gateway_address\": \"\",\n  \"confirmation_params\": {\n    \"safe_inbound_count\": \"2\",\n    \"fast_inbound_count\": \"2\",\n    \"safe_outbound_count\": \"2\",\n    \"fast_outbound_count\": \"2\"\n  },\n  \"disable_tss_block_scan\": false\n}\n```\n\nThe same applies to other chains, such as Solana (chain ID 900).\n\nThis allows sending ZETA to external chains that cannot process it and error, blocking subsequent CCTXs.\n\n- The TON zetaclient will [error in `ProcessOutbound()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/signer/signer.go#L101-L103) due to `cctx.InboundParams.CoinType != coin.CoinType_Gas`.\n- Bitcoin outbound CCTXs error in [`NewOutboundData()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/bitcoin/signer/outbound_data.go#L61-L63),\n- Solana in [`TryProcessOutbound()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/signer/signer.go#L201-L205), and\n- SUI in [`buildWithdrawal()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/sui/signer/signer_tx.go#L79-L81).\n\nNote that there is a specific unit test for this scenario where a CCTX with the ZETA coin type is sent to an unsupported chain -> `\"fail if inbound contain ZETA but receiver chain doesn't support ZETA\"` in [`node/x/observer/keeper/vote_inbound_test.go`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/observer/keeper/vote_inbound_test.go#L179-L222). However, by setting `ZetaTokenContractAddress` to the **empty string**, it will detect that the chain does not support ZETA.\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\n1. Send a V1 CCTX with the ZETA coin type to an external chain that does not support it (e.g., Bitcoin, Solana, etc.).\n2. The zetaclient attempts to process the outbound CCTX, but errors when checking the coin type.\n3. The CCTX cannot be processed, the nonce on the external chain is not increased\n4. The CCTX queue is blocked, no further outbound CCTXs can be processed\n\n## Impact\n\nSending outbound CCTX with the cointype ZETA to an external chain that does not support it will cause the CCTX to error in the zetaclient, blocking subsequent CCTXs. It will indefinitely DoS the external chain's outbound CCTX processing.\n\nNote that while such a \"stuck\" CCTX can be aborted by an admin via `MsgAbortStuckCCTX`, it will still block the outbound queue because the nonce is still \"blocked\" (unused).\n\n## PoC\n\nWe can re-use and update the [`TestMessagePassingEVMtoZEVM`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/e2etests/legacy/test_message_passing_evm_to_zevm.go) E2E test case to use the Bitcoin chain ID when sending the outbound ZETA CCTX.\n\n```diff\ndiff --git a/node/e2e/e2etests/legacy/test_message_passing_evm_to_zevm.go b/node/e2e/e2etests/legacy/test_message_passing_evm_to_zevm.go\nindex a88812581..b0c52a416 100644\n--- a/node/e2e/e2etests/legacy/test_message_passing_evm_to_zevm.go\n+++ b/node/e2e/e2etests/legacy/test_message_passing_evm_to_zevm.go\n@@ -20,8 +20,10 @@ func TestMessagePassingEVMtoZEVM(r *runner.E2ERunner, args []string) {\n \tamount := utils.ParseBigInt(r, args[0])\n\n \t// Set destination details\n-\tzEVMChainID, err := r.ZEVMClient.ChainID(r.Ctx)\n-\trequire.NoError(r, err)\n+\t// zEVMChainID, err := r.ZEVMClient.ChainID(r.Ctx)\n+\t// require.NoError(r, err)\n+\n+\tbitcoinChainID := r.GetBitcoinChainID()\n\n \tdestinationAddress := r.ZevmTestDAppAddr\n\n@@ -45,7 +47,7 @@ func TestMessagePassingEVMtoZEVM(r *runner.E2ERunner, args []string) {\n\n \t// Call the SendHelloWorld function on the EVM dapp Contract which would in turn create a new send, to be picked up by the zeta-clients\n \t// set Do revert to false which adds a message to signal the ZEVM zetaReceiver to not revert the transaction\n-\ttx, err = testDAppEVM.SendHelloWorld(r.EVMAuth, destinationAddress, zEVMChainID, amount, false)\n+\ttx, err = testDAppEVM.SendHelloWorld(r.EVMAuth, destinationAddress, big.NewInt(bitcoinChainID), amount, false)\n \trequire.NoError(r, err)\n \tr.Logger.Info(\"TestDApp.SendHello tx hash: %s\", tx.Hash().Hex())\n\n```\n\nRun the E2E test with:\n\n```bash\nmake start-localnet\nmake install-zetae2e\nzetae2e run legacy_message_passing_evm_to_zevm --config cmd/zetae2e/config/local.yml --verbose\n```\n\nThe Docker logs of one of the zetaclients, specifically, the Bitcoin signer, will error when processing the CCTX due to the invalid ZETA coin type:\n\n```bash\n2025-05-11T12:13:56Z ERR failed to setup Bitcoin outbound data error=\"invalid coin type Zeta\" cctx=0x76676ad20454f0ce974f00b092241b48648c41aee26f38a5174bc7981b9de0b9 chain=18444 method=TryProcessOutbound module=signer nonce=0 signer=zeta1erxfztr9nzf72g6jum0vx9p5j23ry6nztz7e2k\n```\n\n## Mitigation\n\nConvert `ZetaTokenContractAddress` to `common.Address` and then use the already existing [`IsEmptyAddress()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/crypto/evm_address.go#L12-L14) function,\n\n```go\n11: // IsEmptyAddress returns true if the address is empty\n12: func IsEmptyAddress(address common.Address) bool {\n13: \treturn address == (common.Address{}) || address.Hex() == constant.EVMZeroAddress\n14: }\n```\n\nto check if the ZETA token contract address is set.\n\n\n# Issue M-25: Outbound CCTXs to external chains are not properly validated, preventing subsequent outbounds to the external chain from being processed \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/322 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nberndartmueller\n\n## Summary\n\nOutbound CCTXs to external chains are not properly validated. This can lead to invalid and unprocessable outbound CCTXs that cannot be relayed and executed on the external chain. Due to the strict nonce handling, a \"blocked\" nonce will block all subsequent CCTXs from being processed.\n\n## Root Cause\n\nThe [`validateZRC20Withdrawal()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_hooks.go#L324-L366) function is essential to validate the parameters of an outbound ZRC20 withdrawal, a CCTX that is sent from ZetaChain (ZEVM) to external chains, e.g., EVM chains, Bitcoin, Solana, SUI, and TON.\n\nHowever, the current implementation only checks Bitcoin and Solana, SUI, and TON are completely missing.\n\nFollowing is a (non-exhaustive) list of missing checks:\n\n### Solana\n\nThe Solana zetaclient signer errors in\n\n- [`prepareExecuteTx()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/signer/signer.go#L410-L413) (and other instances where `DecodeExecuteMsg()` is called) when `DecodeExecuteMsg()` is unable to decode the execute message (e.g., due to incorrect payload)\n- [`TryProcessOutbound()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/signer/signer.go#L201-L205) when the `CoinType` is `CoinType_NoAssetCall` (using `GatewayZEVM.call()`)\n\n**Impact:**\n\nSolana signer's `TryProcessOutbound()` will not fully process the CCTX, nor will it send a fallback (cancel) transaction to the Solana network to increase the nonce. This will block further CCTXs from being processed.\n\n### SUI\n\n- Valid receiver SUI address\n- The SUI zetaclient signer [errors for V1 ZRC20 withdrawals](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/sui/signer/signer_tx.go#L70-L71), which is possible via [`ZRC20.withdraw()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/zevm/ZRC20.sol#L300-L308) on ZEVM. Add a check to ensure V1 withdrawals are not possible.\n\n### TON\n\nIn the TON zetaclient's [`ProcessOutbound()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/signer/signer.go#L101-L120):\n\n```go\n101: \tif cctx.InboundParams.CoinType != coin.CoinType_Gas {\n102: \t\treturn Invalid, errors.New(\"only gas coin outbounds are supported\")\n103: \t}\n```\n\nCurrently, it is possible to use `GatewayZEVM.call()` with TON as the destination chain, which sets the coin type to `CoinType_NoAssetCall`. This errors in the TON signer, causes subsequent CCTXs to be stuck due to the nonce not being incremented.\n\n```go\n110: receiver, err := ton.ParseAccountID(params.Receiver)\n111: if err != nil {\n112: \treturn Invalid, errors.Wrapf(err, \"unable to parse recipient %q\", params.Receiver)\n113: }\n```\n\nIn line `110`, parsing an invalid receiver account ID also errors, so that further CCTXs are blocked.\n\n[`Lines 295-297`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L295-L297), the TON gateway contract assert a `workchain = 0` address. E.g., if `0f8yaVdkvWSuax8luWhDXY_0X9Am1ASWlJz4OI7M-jqcM2NC` (encode with https://ton.org/address/) is used, with workchain -1, the TON transaction will fail.\n\n[The recipient must not be the TON gateway address](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L298).\n\nTON withdrawals with `amount = 0` [revert](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L300). This is possible when withdrawing from ZEVM with v1 ZERC20 `withdraw`, which [allows 0 amounts](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/zevm/ZRC20.sol#L300-L308).\n\nAll of this will cause the TON transaction to revert, preventing the nonce from being increased, blocking subsequent CCTXs.\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\nUse any of the above-mentioned missing checks to create an outbound CCTX that will error in the respective chain's zetaclient so that the nonce is not being used and thus all subsequent CCTXs are blocked.\n\n## Impact\n\nInvalid and unprocessable outbound CCTXs that cannot be relayed and executed on the external chain. Due to the strict nonce handling, a \"blocked\" nonce will block all subsequent CCTXs from being processed.\n\n## PoC\n\nN/A\n\n## Mitigation\n\nConsider adding the missing checks for the mentioned chains to ensure only valid and actually processable outbound CCTX are put into the queue for the zetaclients to process.\n\n\n# Issue M-26: ZETA token supply keeps growing on failed `onReceive()` contract calls \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/323 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nberndartmueller\n\n## Summary\n\nZETA tokens minted to the fungible module account are not burned/reverted if the `onReceive()` ZEVM call reverts, which keeps increasing the total supply of ZETA tokens. This can lead to a denial of service (DoS) for ZETA deposits on ZetaChain, as the cap of 1.85 billion ZETA tokens is reached.\n\n## Root Cause\n\nIn `CallOnReceiveZevmConnector()`, ZETA tokens are first [minted to the fungible module](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/evm.go#L380) by calling `DepositCoinsToFungibleModule()`, so that the subsequent EVM call has sufficient [native tokens (ZETA) to attach](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/evm.go#L390).\n\nIf we follow the code path up,\n\n- [`InitiateOutbound()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_gateway_zevm.go#L26-L46)\n  - [`HandleEVMDeposit()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/evm_deposit.go#L27-L66)\n    - [`ZETADepositAndCallContract()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/zevm_msg_passing.go#L17-L46)\n      - `CallOnReceiveZevmConnector()`\n\nwe notice that in `InitiateOutbound()` a [temporary context `tmpCtx` is used and passed down](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_gateway_zevm.go#L45-L46) to the chain of function calls.\n\n[This temporary context is committed](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_gateway_zevm.go#L60), i.e., the state is written, if the CCTX's outbound is either successfully mined or if the CCTX will get reverted:\n\n```go\n58: newCCTXStatus = c.crosschainKeeper.ValidateOutboundZEVM(ctx, config.CCTX, err, isContractReverted)\n59: if newCCTXStatus == types.CctxStatus_OutboundMined || newCCTXStatus == types.CctxStatus_PendingRevert {\n60: \tcommit()\n61: }\n```\n\nHowever, if the [`onReceive()` EVM call in `CallOnReceiveZevmConnector()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/evm.go#L385-L401) reverts (e.g., the target contract reverts), the ZETA tokens remain minted to the fungible module while reverting the CCTX and thus refunding the ZETA tokens to the sender on the external chain (e.g., Ethereum).\n\nAs a result, ZETA tokens are minted out of thin air to the fungible module account, which increases the total supply of ZETA tokens. This is a problem as the ZETA token has a [**cap of 1.85 billion ZETA tokens**](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/zeta.go#L16), checked by [`validateZetaSupply()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/zeta.go#L45-L58). If the cap is reached, [minting ZETA tokens will fail](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/zeta.go#L18-L43) and thus prevents ZETA deposits on ZetaChain. This is effectively a DoS.\n\nSame also in case of `processFailedZETAOutboundOnZEVM()` (which internally calls `CallOnRevertZevmConnector()` that also mints ZETA tokens to the fungible module without a temporary context). However, due to [aborting the CCTX in case of the failed revert](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go#L254-L261), without refunding the funds immediately, it does not invite for an attack. Reclaiming the funds would require an admin to refund them via the `MsgRefundAbortedCCTX` message.\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\nGiven ZETA at the time of the review is valued at ~$0.25 USD. And an attacker has a ZEVM contract deployed on ZetaChain that reverts in `onReceive()`.\n\nAn attacker can purposefully trigger the 1.85B ZETA supply cap by repeating the following steps:\n\n1. On Ethereum, initiate a ZETA token inbound via `ZetaConnectorEth.send()` to ZetaChain with 10M ZETA tokens, worth $2.5M USD.\n2. CCTX inbound errors on ZetaChain when executing the `onReceive()` call on the target contract. The 10M ZETA tokens have been minted to the fungible module account.\n3. A revert outbound is created to refund the 10M ZETA tokens to the sender on Ethereum. 4. Attacker receives the 10M ZETA tokens back on Ethereum.\n\nRepeat 185 times until the cap is reached.\n\nThe gas fees are negligible. Also, the capital is returned to the attacker after each iteration, reducing the risk of failure.\n\n## Impact\n\nZETA deposits and message passing via ZetaChain are halted. A network upgrade is required to burn the fungible module account ZETA tokens.\n\n## PoC\n\nAdd the following test case to [`node/x/fungible/keeper/evm_test.go`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/fungible/keeper/evm_test.go) and run with\n\n```bash\ngo test -timeout 30s -run ^TestKeeper_CallOnReceiveZevmConnector$/^AUDIT_should_error_in_contract_call_reverts_but_ZETA_is_minted_to_fungible_module_account$ github.com/zeta-chain/node/x/fungible/keeper\n```\n\n```diff\ndiff --git a/node/x/fungible/keeper/evm_test.go b/node/x/fungible/keeper/evm_test.go\n--- node/x/fungible/keeper/evm_test.go\n+++ node/x/fungible/keeper/evm_test.go\n@@ -19,8 +19,9 @@\n \t\"github.com/zeta-chain/protocol-contracts/pkg/systemcontract.sol\"\n \t\"github.com/zeta-chain/protocol-contracts/pkg/wzeta.sol\"\n \t\"github.com/zeta-chain/protocol-contracts/pkg/zrc20.sol\"\n\n+\tzetaconfig \"github.com/zeta-chain/node/cmd/zetacored/config\"\n \t\"github.com/zeta-chain/node/e2e/contracts/dapp\"\n \t\"github.com/zeta-chain/node/e2e/contracts/dappreverter\"\n \t\"github.com/zeta-chain/node/e2e/contracts/example\"\n \t\"github.com/zeta-chain/node/e2e/contracts/reverter\"\n@@ -1562,8 +1563,34 @@\n \t\t\tdAppContract,\n \t\t\tbig.NewInt(45), []byte(\"message\"), [32]byte{})\n \t\trequire.ErrorContains(t, err, \"execution reverted\")\n \t})\n+\n+\tt.Run(\"AUDIT should error in contract call reverts but ZETA is minted to fungible module account\", func(t *testing.T) {\n+\t\t// add import \"zetaconfig \"github.com/zeta-chain/node/cmd/zetacored/config\"\"\n+\t\tk, ctx, sdkk, _ := keepertest.FungibleKeeper(t)\n+\t\t_ = k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName)\n+\n+\t\tdeploySystemContracts(t, ctx, k, sdkk.EvmKeeper)\n+\t\tdAppContract, err := k.DeployContract(ctx, dappreverter.DappReverterMetaData)\n+\t\trequire.NoError(t, err)\n+\t\tassertContractDeployment(t, sdkk.EvmKeeper, ctx, dAppContract)\n+\n+\t\t// check fungible module address cosmos bank coin ZETA balance before the call\n+\t\tbeforeBalance := sdkk.BankKeeper.GetBalance(ctx, types.ModuleAddress, zetaconfig.BaseDenom)\n+\t\trequire.Equal(t, \"0\", beforeBalance.Amount.String())\n+\n+\t\t_, err = k.CallOnReceiveZevmConnector(ctx,\n+\t\t\tsample.EthAddress().Bytes(),\n+\t\t\tbig.NewInt(1),\n+\t\t\tdAppContract,\n+\t\t\tbig.NewInt(45), []byte(\"message\"), [32]byte{})\n+\t\trequire.ErrorContains(t, err, \"execution reverted\")\n+\n+\t\t// check fungible module address cosmos bank coin ZETA balance after the call\n+\t\tafterBalance := sdkk.BankKeeper.GetBalance(ctx, types.ModuleAddress, zetaconfig.BaseDenom)\n+\t\trequire.Equal(t, \"45\", afterBalance.Amount.String()) // 45 ZETA is minted to fungible module account\n+\t})\n }\n\n func TestKeeper_CallOnRevertZevmConnector(t *testing.T) {\n \tt.Run(\"should call on revert on connector which calls onZetaRevert on sample DAPP\", func(t *testing.T) {\n```\n\nThe test demonstrates that ZETA tokens are minted to the fungible module account even if the contract call reverts.\n\n## Mitigation\n\nConsider using another temporary context for both the ZETA token mint and the `onReceive()` EVM call to ensure atomicity. Same for the `processFailedZETAOutboundOnZEVM()` function.\n\n\n# Issue M-27: EVM outbound transaction gas limit can be set lower than the intrinsic gas limit, which prevents sending the transaction and blocks all other outbound transactions to this chain \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/324 \n\n## Found by \nberndartmueller, g\n\n## Summary\n\nThe gas limit for an outbound EVM (e.g., Ethereum) transaction can be set lower than the intrinsic gas limit of the EVM (min. ~21k, growing based on transaction size). This prevents sending the transaction, blocking the nonce, and causing a Denial of Service (DoS) for all subsequent outbound transactions to the EVM chain.\n\n## Root Cause\n\nIn `gasFromCCTX()`, a recent [PR #3680](https://github.com/zeta-chain/node/pull/3680) removed the `limit < minGasLimit` check for a lower bound gas limit, to ensure an EVM outbound transaction never has less than 100k gas.\n\nNow, it only enforces a maximum gas limit, and overwrites the gas limit if it is set to 21k.\n\n[`gasFromCCTX()#L74-L91`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/evm/signer/gas.go#L74-L91)\n\n```go\n74: if limit > maxGasLimit {\n75: \tlimit = maxGasLimit\n76: \tlogger.Warn().\n77: \t\tUint64(\"cctx.initial_gas_limit\", params.CallOptions.GasLimit).\n78: \t\tUint64(\"cctx.gas_limit\", limit).\n79: \t\tMsgf(\"Gas limit is too high; Setting to the maximum (%d)\", maxGasLimit)\n80: } else if limit == gasTransferGasLimit && (params.CoinType != coin.CoinType_Gas || (cctx.IsCurrentOutboundRevert() && cctx.RevertOptions.CallOnRevert)) {\n81: \t// in some context, 21k might currently be used for an outbound that involves a contract call\n82: \t// this includes erc20 withdraw and onRevert calls whe gas deposit revert\n83: \t// we fix this minimum to ensure the transaction has the minimum required gas limit\n84: \t// TODO: fix the gas limit used for these cctx outbound gas limit\n85: \t// https://github.com/zeta-chain/node/issues/3723\n86: \tlimit = contractCallMinGasLimit\n87: \tlogger.Warn().\n88: \t\tUint64(\"cctx.initial_gas_limit\", params.CallOptions.GasLimit).\n89: \t\tUint64(\"cctx.gas_limit\", limit).\n90: \t\tMsgf(\"Gas limit is too low for contract call; Setting to the minimum (%d)\", contractCallMinGasLimit)\n91: }\n```\n\nThis means that the gas limit can be set to a very low value, so that the [Ethereum RPC intrinsic gas error is returned](https://github.com/ethereum/go-ethereum/blob/0db99f4e409b05c109cffac26c15dd641757a3ab/core/txpool/validation.go#L113-L121). As a result, [broadcasting the outbound EVM transaction](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/evm/signer/signer.go#L322) with `BroadcastOutbound()` will fail repeatedly. As this does not increase the nonce, any subsequent outbound transactions cannot be sent.\n\nThis is non-recoverable, even trying to abort this \"stuck\" CCTX will not work as the nonce will not be re-used by another CCTX.\n\nFor further details, please see the same issue in the Code4rena ZetaChain contest:\n\n[Outbound transactions that can not be broadcasted to an external EVM chain cause a Denial of Service of all outgoing transactions to this chain](https://github.com/code-423n4/2023-11-zetachain-findings/issues/412)\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\n1. Attacker sends an outbound CCTX from ZetaChain to Ethereum and sets the gas limit to a very low value (e.g., 1337).\n2. The EVM zetaclients processes the CCTX and uses 1337 as the transaction gas limit\n3. Broadcasting the Ethereum transaction fails with the intrinsic gas error\n4. Zetaclient retries a couple of times, without success\n5. The CCTX is stuck in the outbound queue, and the TSS nonce on Ethereum is not incremented\n6. Subsequent outbound CCTXs to Ethereum are stuck in the outbound queue\n\n## Impact\n\nThe EVM outbound zetaclient queue will be stuck as the CCTX with the next expected nonce will never be sent and executed. Subsequent CCTXs will be stuck in the queue, representing a DoS.\n\n## PoC\n\nN/A\n\n## Mitigation\n\nConsider re-adding the minimum gas limit check.\n\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3848\n\n\n**gjaldon**\n\nIf the [gas limit](https://github.com/zeta-chain/node/pull/3848/files\\#diff-d2ae83a3d3c2ee567e6aa6f6849a4bf9b0fa112406b243714039c5bea2d78d82R83-R89) is less than 100,000 for any EVM CCTX with contract calls, the gas is set to 100,000 to ensure that there is enough gas to broadcast the transaction and prevent CCTXs from getting blocked.\n\n\n\n# Issue M-28: Solana zetaclient inbound observer is blocked indefinitely when an additional account is passed to the `call` instruction \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/326 \n\n## Found by \nberndartmueller\n\n## Summary\n\nBy providing an additional account to the Solana gateway `call` function, the Solana zetaclient will error, preventing processing other inbound Solana transactions.\n\n## Root Cause\n\nThe Solana gateway `call` function expects one account, the signer, to be passed in the instruction.\n\n[`programs/gateway/src/contexts.rs#L109-L114`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/contexts.rs#L109-L114)\n\n```rust\n109: #[derive(Accounts)]\n110: pub struct Call<'info> {\n111:     /// The account of the signer making the call.\n112:     #[account(mut)]\n113:     pub signer: Signer<'info>,\n114: }\n```\n\nAdditional accounts can be passed by the user, they will just be ignored by the program.\n\nHowever, the Solana zetaclient expects the instruction to have exactly one signer account. If there is more than one signer account, it errors out with the message `want only 1 signer account, got %d`.\n\n[`pkg/contracts/solana/inbound.go#L264-L266`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/solana/inbound.go#L264-L266)\n\n```go\n238: func ParseInboundAsCall(\n239: \ttx *solana.Transaction,\n240: \tinstructionIndex int,\n241: \tslot uint64,\n242: ) (*Inbound, error) {\n...   // [...]\n257:\n258: \t// get the sender address (skip if unable to parse signer address)\n259: \tinstructionAccounts, err := instruction.ResolveInstructionAccounts(&tx.Message)\n260: \tif err != nil {\n261: \t\treturn nil, err\n262: \t}\n263:\n264: \tif len(instructionAccounts) != 1 {\n265: \t\treturn nil, fmt.Errorf(\"want only 1 signer account, got %d\", len(instructionAccounts))\n266: \t}\n```\n\nThis error is propagated up to the `FilterInboundEventsAndVote` function, which is called in the `ObserveInbound` function.\n\n[`zetaclient/chains/solana/observer/inbound.go#L88-L92`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/inbound.go#L88-L92)\n\n```go\n88: // filter inbound events and vote\n89: if err = ob.FilterInboundEventsAndVote(ctx, txResult); err != nil {\n90: \t// we have to re-scan this signature on next ticker\n91: \treturn errors.Wrapf(err, \"error FilterInboundEventAndVote for sig %s\", sigString)\n92: }\n```\n\nDue to this error, the same transaction will be re-scanned on the next ticker, preventing other inbound Solana transactions from being processed indefinitely.\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\n1. The attacker sends a Solana transaction with the `call` instruction to the Solana gateway program with an additional account.\n2. The Solana transaction is executed, and the `call` instruction is processed successfully.\n3. The Solana zetaclient processes the transaction and calls the `ParseInboundAsCall` function. The check `len(instructionAccounts) != 1` fails, as there are two accounts passed in the instruction.\n4. The error is propagated up to the `ObserveInbound` function, which re-scans the same transaction on the next ticker.\n5. The error is surfaced repeatedly, preventing any other inbound Solana transactions from being processed.\n\n## Impact\n\nThe Solana inbound processing is blocked indefinitely, preventing any other inbound Solana transactions from being processed.\n\n## PoC\n\nTo demonstrate how an additional account breaks this check, we can run the[`TestSolanaToZEVMCall`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/e2etests/test_solana_to_zevm_call.go) E2E test case. This test case calls the Solana gateway's `call` function with an additional account.\n\nUpdate the [`CreateSOLCallInstruction`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/runner/solana.go#L78-L99) function to add an additional account to the instruction:\n\n```diff\nreturn &solana.GenericInstruction{\n\t\tProgID:    r.GatewayProgram,\n\t\tDataBytes: callData,\n\t\tAccountValues: []*solana.AccountMeta{\n\t\t\tsolana.Meta(signer).WRITE().SIGNER(),\n+\t\t\tsolana.Meta(solana.SystemProgramID),\n\t\t},\n\t}\n```\n\nRun the test case with the following command:\n\n```bash\nmake start-solana-test\n```\n\nAfter a while, when the E2E test is running, you will see the following error in the Docker logs of the zetaclient (both, 1 and 2):\n\n```bash\n2025-05-07T12:21:46Z INF got wrong amount of signatures chain=902 chain_network=solana method=ObserveInbound module=inbound signatures=1\n\n2025-05-07T12:21:46Z ERR task failed error=\"error FilterInboundEventAndVote for sig 2f4P2EAtrh7HitBEU4ZzvPCw72PcAR5EjqPqfTaqi5oyvFiY8zUwNdnvK9mxWN3kumEpnWWXCasbiNhPNkTD3cj: error FilterInboundEvent: error ParseInboundAsCall: want only 1 signer account, got 2\" module=scheduler task.group=sol:902 task.name=observe_inbound task.type=interval_ticker\n```\n\nThe error will be logged repeatedly as the zetaclients re-scan this same transaction. Any other inbound Solana transaction will not be processed.\n\n## Mitigation\n\nLike other instances in the Solana zetaclient, the check for the number of accounts should use `< 1` instead of `!= 1`. This will allow the program to process the transaction successfully, even if additional accounts are passed in the instruction.\n\n```go\n264: \tif len(instructionAccounts) < 1 {\n265: \t\treturn nil, fmt.Errorf(\"want required 1 signer account, got %d\", len(instructionAccounts))\n266: \t}\n```\n\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3895\n\n\n**gjaldon**\n\nThe issue is fixed by changing the [check](https://github.com/zeta-chain/node/pull/3895/files\\#diff-ecd8b83a122aff6936f99a346430e981ae911aabd6a58d26dfaa75dd774d0617R248-R249) to less than the accountsNumberCall so that more instruction accounts can be passed.\n\n\n\n# Issue M-29: Stateful precompiles panic on empty calldata, which can be exploited to prevent finalizing outbound CCTXs \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/328 \n\nThis issue has been acknowledged by the team but won't be fixed at this time.\n\n## Found by \nberndartmueller, oade\\_hacks, sherlockxyz\n\n## Summary\n\nThe precompile caller can pass invalid (empty) calldata, which results in a panic in the precompile. This immediately exits the EVM and propagates up to the Cosmos SDK baseapp. The Cosmos SDK message (e.g., `MsgVoteOutbound`) fails, and the outbound cctx cannot be finalized. The cctx remains pending in the queue.\n\n## Root Cause\n\nIn ZetaChain's precompiles, it is assumed that the provided `input` (calldata) by the caller is at least 4 bytes long (function selector).\n\nThis is observed in the following instances:\n\n- [`precompiles/bank/bank.go#L95`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/bank/bank.go#L95)\n- [`precompiles/bank/bank.go#L113`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/bank/bank.go#L113)\n- [`precompiles/prototype/prototype.go#L96`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/prototype/prototype.go#L96)\n- [`precompiles/prototype/prototype.go#L226`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/prototype/prototype.go#L226)\n- [`precompiles/staking/staking.go#L122`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/staking/staking.go#L122)\n- [`precompiles/staking/staking.go#L140`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/precompiles/staking/staking.go#L140)\n\nFor example, the `RequiredGas()` function in the `bank` precompile,\n\n```go\n92: func (c *Contract) RequiredGas(input []byte) uint64 {\n93: \t// get methodID (first 4 bytes)\n94: \tvar methodID [4]byte\n95: \tcopy(methodID[:], input[:4])\n```\n\naccesses the first 4 bytes of the `input` without checking if the length is at least 4 bytes. If the length is less than 4 bytes, this will panic with an `index out of range` error.\n\nThis panic is propagated up until the Cosmos SDK baseapp, where it is [caught](https://github.com/cosmos/cosmos-sdk/blob/7b9d2ff98d02bd5a7edd3b153dd577819cc1d777/baseapp/baseapp.go#L839-L847) to prevent the node from crashing.\n\nHowever, the Cosmos SDK message has failed and cannot be executed.\n\n## Internal Pre-conditions\n\nNone\n\n## External Pre-conditions\n\nNone\n\n## Attack Path\n\n1. Attacker sends 500 ([MaxPendingCctxs = 500](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/grpc_query_cctx.go#L19)) outbound CCTXs from ZetaChain to an external EVM chain (e.g., Ethereum) where they will error when calling the attacker's contract\n2. Observers vote on the failed outbound CCTXs\n3. The \"final\" CCTX vote attempts to finalize the outbound CCTX on ZetaChain by processing the revert on ZEVM\n4. [As part of the revert process](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/cctx_orchestrator_validate_outbound.go#L408-L418), the attacker contract's `onRevert()` function is [called](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts/contracts/zevm/GatewayZEVM.sol#L463-L502)\n5. In `onRevert()`, ZetaChain's precompile is called with invalid calldata (e.g., empty calldata)\n6. The precompile panics, and the entire transaction fails\n7. The outbound CCTX cannot be finalized and remains pending in the queue\n8. This is repeated for all 500 outbound CCTXs, which remain pending in the queue\n9. EVM zetaclients will be unable to process other outbound CCTXs [due to their lookback criteria](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/evm/evm.go#L176-L183)\n\n## Impact\n\n- A Solidity contract that explicitly uses `try-catch` to ensure an external contract call must never cause the overall transaction to revert is unable to do so if the called contract calls the precompile with invalid calldata so that it panics.\n- More severe, if the ZEVM call is initiated as part of the `MsgVoteOutbound` message in response to a failed outbound cctx (i.e., reverting the cctx on ZEVM), and it panics, the entire message fails. As a result, the outbound cctx cannot be finalized, it remains pending. An attacker can repeatedly cause multiple cctx's to remain in pending state so that the [pending cctx queue fills up until the limit is reached](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/x/crosschain/keeper/grpc_query_cctx.go#L193-L200) and the zetaclients are unable to process any new outbound cctx's.\n\n## PoC\n\nThis simple PoC runs the `e2e/e2etests/test_precompiles_staking_through_contract.go` E2E test and demonstrates how the staking precompile is called with empty calldata so that it panics. The `try-catch` is just cosmetic as it is unable to catch the panic.\n\nUpdate the `stakeWithStateUpdate` function in [`TestStaking.sol`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/e2e/contracts/teststaking/TestStaking.sol) and add the new `causePanic` function:\n\n```solidity\nfunction stakeWithStateUpdate(\n    address staker,\n    string memory validator,\n    uint256 amount\n) external onlyOwner returns (bool) {\n    counter = counter + 1;\n    // transfer full balance\n    (bool success, ) = payable(address(0xdead)).call{value: payable(this).balance }(\"\");\n    require(success, \"transfer to dead address failed\");\n\n    success = staking.stake(staker, validator, amount);\n\n    try this.causePanic() {} catch {\n        // do nothing\n    }\n\n    counter = counter + 1;\n    return success;\n}\n\nfunction causePanic() external {\n    // call `staking` contract with low level call invalid calldata so that it panics\n    address(staking).call(hex\"\");\n}\n```\n\nRe-generate the ABI Go bindings:\n\n```bash\ngo generate ./e2e/contracts/teststaking\n```\n\nRun the test:\n\n```bash\nzetae2e run precompile_contracts_staking_through_contract --config cmd/zetae2e/config/local.yml --verbose\n```\n\nThe precompile will panic, and all state changes will be reverted. The Docker logs will show the panic:\n\n```bash\npanic recovered in runTx err=\"recovered: runtime error: slice bounds out of range [:4] with capacity 0\n```\n\n## Mitigation\n\nConsider checking that the `input` length is at least 4 bytes.\n\n\n# Issue M-30: Insufficient Transaction Broadcast Timeout in EVM Chains \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/341 \n\n## Found by \n0xpetern\n\n### Summary\n\nThe ZetaChain protocol uses an excessively short timeout (1 second) for broadcasting transactions to EVM chains, causing premature timeouts particularly on networks with higher latency like Arbitrum. When the initial transaction broadcast times out, all subsequent retry attempts fail with \"nonce too low\" errors. This leads to missed outbound tracker postings, creating state inconsistencies between ZetaChain and connected EVM chains that can only be fixed through code changes requiring a hard fork.\n\n### Root Cause\n\nIn `signer.go` for evm chains, a 1-second timeout value is hardcoded, which is too short for transaction broadcasting.\n\n```go\nconst (\n\t// broadcastBackoff is the initial backoff duration for retrying broadcast\n\tbroadcastBackoff = 1000 * time.Millisecond\n\n\t// broadcastRetries is the maximum number of retries for broadcasting a transaction\n\tbroadcastRetries = 5\n)\n```\nEarly timeout potentially lead to missed outbound tracker in Arbitrum.\nWhen the initial tx broadcasting gets timeout, all the subsequent retries will return `nonce too low` in Arbitrum, therefore ignores the outbound tracker posting.\n\n```go\nfunc (signer *Signer) broadcast(ctx context.Context, tx *ethtypes.Transaction) error {\n\tctx, cancel := context.WithTimeout(ctx, 1*time.Second)\n\tdefer cancel()\n\n\treturn signer.client.SendTransaction(ctx, tx)\n}\n```\nRequired Links: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/evm/signer/signer.go#L32C1-L38C2\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/evm/signer/signer.go#L227C1-L232C2\n\n### Internal Pre-conditions\n\n1. Cross-chain transactions are being processed from ZetaChain to EVM chains\n2. Transaction broadcaster uses hardcoded 1-second timeout\n3. Network conditions cause transaction propagation to exceed 1 second\n4. System is configured to retry failed broadcasts\n\n### External Pre-conditions\n\n1. EVM chain (particularly Arbitrum) has higher transaction processing latency\n2. Network congestion increases transaction confirmation time\n3. Users are initiating cross-chain transactions requiring outbound trackers.\n\n### Attack Path\n\n1. The system attempts to broadcast transaction to Arbitrum\n2. 1-second timeout expires but transaction is still in mempool\n3. The system attempts to retry with same nonce, receives \"nonce too low\" errors\n4. Transaction fails and system fails to post outbound tracker due to nonce errors.\n\n\n### Impact\n\n1. Failed cross chain transaction.\n2. Missing outbound Trackers.\n3. Cross-chain state inconsistency.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nModify timeout and backoff constants and uses these values in the broadcast function.\n\n```go\n// New constants with appropriate values\nconst (\n    // broadcastBackoff is the initial backoff duration for retrying broadcast\n    broadcastBackoff = time.Second * 6\n    \n    // broadcastRetries is the maximum number of retries for broadcasting a transaction\n    broadcastRetries = 5\n    \n    // broadcastTimeout is the timeout for broadcasting a transaction\n    // we should allow enough time for the tx submission and avoid fast timeout\n    broadcastTimeout = time.Second * 15\n)\n```\n\n```go\nfunc (signer *Signer) broadcast(ctx context.Context, tx *ethtypes.Transaction) error {\n\tctx, cancel := context.WithTimeout(ctx, broadcastTimeout)\n\tdefer cancel()\n\n\treturn signer.client.SendTransaction(ctx, tx)\n}\n```\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\n https://github.com/zeta-chain/node/pull/3850\n\n\n\n\n# Issue M-31: Missing Nonce Reset During TSS Address Update Allowing Signature Replay \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/352 \n\n## Found by \n0xpetern, berndartmueller, g\n\n### Summary\n\nIn the solana implementation, When updating the TSS address via `update_tss`, the function fails to reset the nonce counter. \nThis creates a security issue where previously authorized signatures could remain valid after a TSS address change and can be replayed.\n\n### Root Cause\n\nThe [update_tss()](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-solana/programs/gateway/src/instructions/admin.rs#L35) function updates the TSS address (used for signature verification) without resetting the nonce counter that prevents signature replay attacks. This creates a dangerous security gap where the system transitions to a new signing authority while maintaining continuity with the nonce sequence of the previous authority.\n\n```rust\n// Updates the TSS address. Caller is authority stored in PDA.\npub fn update_tss(ctx: Context<UpdateTss>, tss_address: [u8; 20]) -> Result<()> {\n    verify_authority(&ctx.accounts.signer.key(), &ctx.accounts.pda)?;\n    let pda = &mut ctx.accounts.pda;\n    pda.tss_address = tss_address;\n\n    msg!(\n        \"TSS address updated: new TSS address = {:?}, PDA authority = {}\",\n        tss_address,\n        ctx.accounts.signer.key()\n    );\n\n    Ok(())\n}\n```\nThis implementation only modifies th TSS address, while the nonce remain unchanged, when this function is called. \nPrevious signatures remain valid with their existing nonces.\n\n\n\n\n### Internal Pre-conditions\n\n1. Protocol uses TSS for cross-chain transaction authorization\n2. Transaction nonce serves as a replay protection mechanism\n3. TSS address is used to verify transaction signatures\n4. Authority has ability to update TSS address\n\n### External Pre-conditions\n\n1. Protocol is actively processing cross-chain transactions\n2. Transactions are flowing between chains with TSS signatures\n\n### Attack Path\n\nHere is a possible attack path\n1. System uses TSS address A with current nonce sequence (e.g., nonce = N)\n2. Authority updates TSS from address A to address B but Nonce remains at N (not reset to 0)\n3. System now accepts signatures from TSS B, continuing from nonce N+1.\n4. System validates signature against TSS B but with nonces that were used by TSS A resulting to a successful signature replay.\n\n### Impact\n\n1. Signatures from old TSS could be replayed.\n2. System may accept unauthorized transactions.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nUpdate the update_tss function to reset the nonce counter when changing the TSS address:\n```rust\npub fn update_tss(ctx: Context, tss_address: [u8; 20]) -> Result<()> {\n    verify_authority(&ctx.accounts.signer.key(), &ctx.accounts.pda)?;\n    let pda = &mut ctx.accounts.pda;\n    \n    // Update TSS address\n    pda.tss_address = tss_address;\n    \n    // Reset nonce to zero - CRITICAL SECURITY FIX\n    pda.nonce = 0;\n\n    msg!(\n        \"TSS address updated: new TSS address = {:?}, nonce reset to 0, PDA authority = {}\",\n        tss_address,\n        ctx.accounts.signer.key()\n    );\n\n    Ok(())\n}\n```\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/protocol-contracts-solana/pull/107\n\n\n**gjaldon**\n\nThe issue is [fixed](https://github.com/zeta-chain/protocol-contracts-solana/pull/107/files\\#diff-baae2283858b528c23facc8e021bf0856a34f417584af5ad6cf471f72196d505R39-R44) by resetting the nonce to 0 when updating the TSS address.\n\n\n\n# Issue M-32: Incorrect de-structuring of parse_intent()'s return value will trigger unexpected operations \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/365 \n\n## Found by \ng\n\n### Summary\n\nThe wrong de-structuring of [`parse_intent()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L223) when the TON Gateway contract receives an internal message will trigger the incorrect operation. \n\n### Root Cause\n\n[`parse_intent()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L41-L46)'s return values the following format `(slice, (int, int))`. \n\n```funC\n(slice, (int, int)) ~parse_intent(slice message_body) impure inline {\n    int op = message_body~load_uint(size::op_code_size);\n    int query_id = message_body~load_uint(size::query_id_size);\n\n    return (message_body, (op, query_id));\n}\n```\n\nHowever, [`recv_internal()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L223) misuses it by assigning the first element in the returned tuple to an `int`.\n\n```funC\n(int op, _) = in_msg_body~parse_intent();\n```\n\nThe first tuple should be the `message_body`, which is of `slice` type and not an `int`. \n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. A user wants to deposit TON coins and sends an internal message to the Gateway contract\n2. Due to the wrong de-structuring, the `op` value is their 160-bit [EVM address](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L98) padded with zeroes. \n3. The transaction would fail with an [`unknown_op`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L257) error.\n\n### Impact\n\nInternal messages will fail with `unknown_op` or trigger different operations than expected. This affects all user and admin operations, including `update_code` (for upgrading the contract). \n\n### PoC\n\n_No response_\n\n### Mitigation\n\nConsider fixing the de-structuring to the following.\n\n```funC\n(_, (int op, _)) = in_msg_body~parse_intent();\n```\n\n# Issue M-33: TON Gateway withdrawals can be DOS'd with an invalid withdrawal recipient \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/367 \n\n## Found by \n0x73696d616f, g\n\n### Summary\n\nUnlike in Gateway contracts in other chains, there is only one way to increment the nonce in the Ton Gateway contract. The nonce is only [incremented](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L312) in `handle_withdrawal()`. \n\nAny failure in `handle_withdrawal` before the nonce is incremented and committed will [prevent withdrawals](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L301) with higher nonces from getting executed. \n\n### Root Cause\n\nThere is no way to increment the nonce other than through [`handle_withdrawal()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L312-L315).\n\n```funC\n    state::total_locked -= (amount + tx_fee);\n    state::seqno += 1;\n\n    mutate_state();\n    commit();\n```\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. A user withdraws TON coins from Zetachain, triggering an Outbound Withdrawal to TON. The Outbound Withdrawal has a nonce of 5 and a receiver of `{workchain: 1, recipient_addr: 0xted}`.\n2. An Observer executes the Outbound Withdrawal and it fails with a [wrong workchain error](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L297). \n3. The nonce in the TON Gateway contract stays at nonce 5 because it does not get incremented and committed.\n4. Outbound Withdrawals with nonce greater than 5 will fail because of [this check](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-ton/contracts/gateway.fc#L301).\n\n### Impact\n\nWithdrawals to the TON Gateway contract are DOS'd indefinitely.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nConsider applying the same `increment_nonce` approach that is applied to the Solana Gateway contract.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3977\n\n\n**gjaldon**\n\nThe issue is [fixed](https://github.com/zeta-chain/node/pull/3977/files\\#diff-465c1d777ce2840091a6424b19070a06fd488910fe61ce099807a20a81bf3180R72-R93) by calling IncreaseSeqno in the Gateway when an invalid workchain is used.\n\n\n\n# Issue M-34: TON Gateway transactions with a skipped compute phase will cause a panic \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/370 \n\n## Found by \n0x73696d616f, g\n\n### Summary\n\nThere are cases when a transaction for the Gateway will skip the compute phase. When this happens, [`exitCodeFromTx()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/ton/gateway_parse.go#L291-L293) will panic because `TrPhaseComputeVm` will be nil. \n\n### Root Cause\n\n[`exitCodeFromTx()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/ton/gateway_parse.go#L291-L293) assumes that `TrPhaseComputeVm` is always present. However, that is false because `ComputePh` is a Sum type with two variants.\n\n```golang\ntype TrComputePhase struct {\n\tSumType\n\tTrPhaseComputeSkipped struct {\n\t\tReason ComputeSkipReason\n\t} `tlbSumType:\"tr_phase_compute_skipped$0\"`\n\tTrPhaseComputeVm struct {\n                     // ... snip ...\n\t\t} `tlb:\"^\"`\n\t} `tlbSumType:\"tr_phase_compute_vm$1\"`\n}\n```\n\nA transaction can skip the compute phase when the message sent to the Gateway contract provides less gas than is required for minimal execution. The minimum amount of gas needed is implementation-dependent, but roughly in the ~10\u2075 nanoton (0.1 TON) range for basic contracts.\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. A malicious user sends a message with very low value, e.g., 0.001 TON. Set the `forward_fee` or `fee_remaining` so that only a tiny gas budget is passed. The message will have a valid message body for a deposit operation.\n2. TON VM will then skip execution and record `compute.skipped = true` for the message sent to the Gateway contract.\n3. The TON Observer will [get](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/ton/observer/inbound.go#L44-L65) every transaction and parse it. \n4. When parsing the Inbound Deposit that skipped the compute phase, it will panic when calling [`exitCodeFromTx()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/pkg/contracts/ton/gateway_parse.go#L71).\n\n### Impact\n\nAll TON Observers' `ObserveInbound` and `ProcessInboundTrackers` go-routines will crash and no longer process any inbound deposits to the TON Gateway.\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nConsider adding a check to ensure the compute phase is present before accessing the `TrPhaseComputeVm` field.\n\n# Issue M-35: A Malicious Observer can broadcast the Solana cancel tx instead of the execute tx \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/372 \n\n## Found by \nAl-Qa-qa, Laksmana, g\n\n### Summary\n\nSolana Observers will [sign](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/signer/signer.go#L450-L455) both the execute and increment nonce instructions when preparing an execute SPL transaction. Since all Observers will sign both instructions, a malicious Observer can broadcast the fallback transaction (increment nonce) instead of the execute transaction. A single Observer can prevent Solana `execute` instructions indefinitely. \n\n### Root Cause\n\nWhen preparing the `execute` or `execute SPL` transactions, all the observers TSS-sign both the execute and increment nonce instructions. Since the TSS signature for the increment nonce transaction is available, a malicious Observer can use this to broadcast the increment nonce transaction instead of the execute transaction.\n\n```golang\n// in `prepareExecuteTx()` and `prepareExecuteSPLTx()`\n\ntx, err := signer.signTx(ctx, inst, params.CallOptions.GasLimit)\n// ... snip ...\n\nfallbackTx, err := signer.signTx(ctx, fallbackInst, 0)\n```\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. A malicious Observer modifies their Solana Observer code to broadcast the increment nonce transaction instead of the execute transaction.\n2. Once an [`execute`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/signer/signer.go#L165-L173) outbound is processed, the malicious Observer can broadcast the increment nonce without [delay](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/signer/signer.go#L298), so they can be the first to broadcast and consume the nonce.\n3. The nonce will be incremented, and the execute Outbound is effectively cancelled. \n4. This attack can be done repeatedly to prevent most execute Outbounds from getting broadcasted.\n\n### Impact\n\nA single Observer can prevent Solana `execute` Outbounds indefinitely. \n\n### PoC\n\n_No response_\n\n### Mitigation\n\n_No response_\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3914\n\n\n\n\n# Issue M-36: An attacker can stuff a Solana Outbound transaction with multiple instructions to block it from finalizing \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/377 \n\n## Found by \ng\n\n### Summary\n\nA Solana Outbound transaction is [expected](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/outbound.go#L296-L302) to have 1 or 2 instructions. An attacker can exploit this expectation and force the Gateway instruction parsing to fail by stuffing a valid Outbound transaction with at least two more instructions. This will block the Outbound from finalizing because [`ProcessOutboundTrackers()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/outbound.go#L57-L65) will always fail when it calls [`CheckFinalizedTx()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/outbound.go#L251-L255) on the transaction.\n\n### Root Cause\n\nAn attacker can exploit the [expectation](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/outbound.go#L296-L302) that a Solana Gateway transaction must only have 1 or 2 instructions. \n\n```golang\n\tinstructionCount := len(tx.Message.Instructions)\n\tif instructionCount < 1 || instructionCount > 2 {\n\t\treturn nil, fmt.Errorf(\"unexpected number of instructions: %d\", instructionCount)\n\t}\n\n\t// get gateway instruction\n\tinstruction := tx.Message.Instructions[instructionCount-1]\n```\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\nThis issue can be exploited by an attacker front-running a valid Outbound transaction or by a malicious Observer.\n\n1. An attacker observes a valid `increment_nonce` Outbound transaction. They front-run this transaction and replace it with the valid `increment_nonce` instruction plus two more valid instructions. \n2. The transaction succeeds and uses up a nonce. Since no other Outbound transaction exists with the same nonce, an Observer adds an Outbound Tracker for this transaction.\n3. When the Outbound Tracker is processed, [checking](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/outbound.go#L57-L65) the finalized transaction will always fail when it attempts to parse the gateway instruction because it has three instructions.\n4. Since [`SetTxResult()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/solana/observer/outbound.go#L69) is never called on the nonce, no Observers will vote on the Outbound, and it will not finalize.\n5. The sender of the Outbound transaction will not get their withdrawal, and their refund will not get triggered in Zetachain. \n\n### Impact\n\nThe sender of the Outbound transaction will lose their withdrawn funds. The Observers must manually vote failure on the victim's Outbound before the victim can get a refund. There can be a significant delay before their funds are recovered. There is no loss for the attacker. \n\n### PoC\n\n_No response_\n\n### Mitigation\n\nConsider removing any expectations on the format/structure of the Solana transaction.\n\n## Discussion\n\n**sherlock-admin2**\n\nThe protocol team fixed this issue in the following PRs/commits:\nhttps://github.com/zeta-chain/node/pull/3957\n\n\n**gjaldon**\n\nThe instructions are now [parsed](https://github.com/zeta-chain/node/pull/3957/files\\#diff-f792ae0bd93e42740c8a9b99b1238abb88e093a09afdb7d33279a3f2c5260c5bR337-R392) without expecting a certain number of instructions in the transaction. For as long as the transaction includes a valid Gateway instruction that matches the current nonce, it will be parsed successfully.\n\n\n\n# Issue M-37: Inconsistent Pause Mechanism Allows Withdrawals During Emergency Pause \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/410 \n\n## Found by \n0xC2939291, Al-Qa-qa, Alpha, Goran, Laksmana, ifeco445, pks\\_, zanderbyte\n\n### Summary\n\nThe `withdraw_impl` function in `gateway.move` lacks a pause mechanism, unlike the deposit functionality which has proper pause checks. This creates an inconsistency in the security controls and could allow withdrawals to continue even when the system should be paused.\n\n### Root Cause\n\nThe `withdraw_impl` function does not have a pause flag, while the deposit functions (`check_receiver_and_deposit_to_vault`) have the flag. This creates a security gap where withdrawals can continue even when deposits are paused.\n\nhttps://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/protocol-contracts-sui/sources/gateway.move#L271-L282\n\n### Internal Pre-conditions\n\n- The caller must have a valid WithdrawCap\n- The coin type must be whitelisted\n- The nonce must match the current gateway nonce\n\n\n### External Pre-conditions\n\n- The caller must have permission to call the function\n- The system(gateway vault) must have sufficient balance for the withdrawal\n\n### Attack Path\n\n1. An attacker with WithdrawCap can continue to withdraw funds even when the system is in a paused state\n2. This could be exploited during emergency situations when the system should be completely paused\n3. The attacker could withdraw funds from gateway vaults while legitimate deposits are blocked\n\n### Impact\n\n1. Withdrawals can continue even when the system should be completely paused\n2. Different security controls for deposits and withdrawals create confusion and potential vulnerabilities\n\n### PoC\n\n_No response_\n\n### Mitigation\n\nAdd a `paused` variable to Gateway struct and use this check in both deposits and withdrawals.\n```move\nassert!(!gateway.paused, EPaused);\n```\nThis will ensure that withdrawals are properly paused when the system is in a paused state.\n\n# Issue M-38: Bitcoin Observers' signed transactions will be rejected due to invalid sighashes \n\nSource: https://github.com/sherlock-audit/2025-04-zetachain-cross-chain-judging/issues/424 \n\n## Found by \ng\n\n### Summary\n\nThe Bitcoin Observers use invalid [sighashes](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/bitcoin/signer/sign.go#L211) when generating the witness hash for each transaction input. The Bitcoin network will reject all the signed transactions by the Observers.\n\n### Root Cause\n\nIn [`SignTx()`](https://github.com/sherlock-audit/2025-04-zetachain-cross-chain/blob/main/node/zetaclient/chains/bitcoin/signer/sign.go#L211), an empty byteslice and zero amount are used when generating the sighashes.\n\n```golang\nsigHashes := txscript.NewTxSigHashes(tx, txscript.NewCannedPrevOutputFetcher([]byte{}, 0))\n```\n\nThe correct input is the previous output's scriptPubKey and amount.\n\n### Internal Pre-conditions\n\nNone\n\n### External Pre-conditions\n\nNone\n\n### Attack Path\n\n1. The Observers sign the generated Bitcoin transaction with TSS.\n2. When the transaction is broadcast, the Bitcoin network rejects it because it fails verification since the sighash it reconstructs using the actual previous outputs' script and amount does not match the sighash created by the Observers.\n\n### Impact\n\nAll the withdrawal transactions in the Bitcoin network will fail. Users will be unable to withdraw from the Bitcoin network. \n\n### PoC\n\n_No response_\n\n### Mitigation\n\nConsider generating the correct sighashes with the below pseudocode. \n\n```golang\nprevOutFetcher := txscript.NewMultiPrevOutFetcher(nil)\nfor i, input := range tx.TxIn {\n\tprevOut := selected.UTXOs[i]  // Get the corresponding UTXO\n\tscript, err := hex.DecodeString(prevOut.ScriptPubKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tamount := int64(prevOut.Amount * btcutil.SatoshiPerBitcoin)\n\tprevOutFetcher.AddPrevOut(input.PreviousOutPoint, &wire.TxOut{\n\t\tValue:    amount,\n\t\tPkScript: script,\n\t})\n}\nsigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)\nsigHashes := txscript.NewTxSigHashes(tx, txscript.NewCanned\n```\n\n","rewards_tiers":[{"unlock_percentage":100.0,"severity":{"color":"f0b375","text_color":"423d38","name":"Medium","id":2.0},"min_count":0,"id":795}],"prize_pool":113600,"judging_prize_pool":3000,"escalation_ends_at":1748449787,"is_judging_v3":true,"context_questions":[{"question":"On what chains are the smart contracts going to be deployed?","answer":"Smart contracts for ZetaChain's CCTX (Cross-Chain Transaction) functionality are going to be deployed on the following chains:\n\nCurrently Integrated & Deployed\nZetaChain (Universal Layer 1 \u2013 main deployment)\nEthereum\nBinance Smart Chain (BSC)\nBase\nSolana\nBitcoin (via external interaction, not smart contracts)\n\nUnder Integration / Planned\nTON (The Open Network) \u2013 under integration\nSui \u2013 under integration\nArbitrum \u2013 planned\nAvalanche (Avax) \u2013 planned","order":1},{"question":"If you are integrating tokens, are you allowing only whitelisted tokens to work with the codebase or any complying with the standard? Are they assumed to have certain properties, e.g. be non-reentrant? Are there any types of [weird tokens](https://github.com/d-xo/weird-erc20) you want to integrate?","answer":"ZetaChain primarily uses a whitelist approach for tokens integrated with its CCTX system:\n -The Solidity codebase (e.g. ZetaConnector, Gateway, and ZRC20 contracts) checks against registered/whitelisted token addresses before allowing interaction.\n -This is a security-first approach to avoid risks with malicious or non-standard tokens.\n\nWhile ZetaChain is designed to interact with standardized tokens (ERC20, SPL, etc.), there are definitely assumptions:\n - No reentrancy: The contracts often rely on non-reentrant behavior, especially around transfer and approve. Reentrancy protections (like OpenZeppelin\u2019s nonReentrant) are used on critical entrypoints.\n - Standard behavior: Tokens are assumed to:\n - Return a boolean from transfer and transferFrom\n - Not perform unexpected state changes or external calls\n - Not have deflationary or fee-on-transfer mechanics (unless explicitly supported)","order":2},{"question":"Are there any limitations on values set by admins (or other roles) in the codebase, including restrictions on array lengths?","answer":"Owner is trusted\nKeeper is not trusted\nFeeManager is only trusted to collect the fees correctly\n\nThere are no enforced limitations on values set by admin roles in the codebase, including array lengths in access-restricted functions. This applies across all contracts, including any out-of-scope components.\n\nValues such as protocol fees, connector addresses, TSS addresses, and whitelisted tokens are all admin-configurable without hardcoded caps or bounds in the smart contracts.\n\n","order":3},{"question":"Are there any limitations on values set by admins (or other roles) in protocols you integrate with, including restrictions on array lengths?","answer":"There are no limitations enforced by ZetaChain on values set by admins in integrated protocols, including restrictions on array lengths or config parameters in those external contracts.\n\nZetaChain assumes that the governance of integrated protocols is trusted and correctly manages their own access-controlled functions. This includes any contracts for token standards (e.g., ERC20), bridges, messaging layers, or chain-specific components (e.g., Sui Move modules, Solana programs).","order":4},{"question":"Is the codebase expected to comply with any specific EIPs?","answer":"he codebase sticks to a few key EIPs to stay compatible with the broader Ethereum and multi-chain ecosystem. Here\u2019s what we follow and why:\n\nEIP-20 (ERC20)\nWe use this for ZRC20 tokens so they behave just like regular ERC20s. It makes integration with wallets, DEXs, and other protocols super easy \u2014 important for cross-chain stuff.\n\nEIP-165 (Interface Detection)\nThis helps other contracts know what our contracts support (like a \u201csupports interface\u201d check). Super useful when you\u2019re interacting across chains or with unknown contracts.\n\nEIP-712 (Typed Signing)\nUsed for secure off-chain signatures \u2014 great for things like meta-transactions or cross-chain messages where security matters.","order":5},{"question":"Are there any off-chain mechanisms involved in the protocol (e.g., keeper bots, arbitrage bots, etc.)? We assume these mechanisms will not misbehave, delay, or go offline unless otherwise specified.","answer":"The core off-chain mechanism in ZetaChain is the Threshold Signature Scheme (TSS) relayer, sometimes referred to as the Keeper. It plays a crucial role in:\n- Watching multiple chains (EVM, Solana, TON, Sui, etc.)\n- Reading events emitted from smart contracts (e.g. cross-chain transaction requests)\n- Verifying the data and its source\n- Signing the message (via TSS)\n- Relaying it to the destination chain by calling the appropriate contract (e.g. ZetaConnector, ZetaReceiver, etc.)\n\n","order":6},{"question":"What properties/invariants do you want to hold even if breaking them has a low/unknown impact?","answer":"- ZRC20 supply must match locked tokens 1:1\n- Cross-chain messages should only be executed once\n- Critical roles (e.g. TSS, connectors) must always point to valid addresses\n- Protocol and gas fees must not exceed transferred value\n- Paused contracts should block all state-changing operations","order":7},{"question":"Please discuss any design choices you made.","answer":"Simplified fee handling\nSkips edge cases like fee-on-transfer tokens to avoid overcomplicating logic \u2014 only standard tokens are supported anyway.\n\nOne-time CCTX execution (nonce-based)\nUses nonces to prevent double execution of cross-chain messages \u2014 assumes relayers behave correctly.\n\nTrusted roles (Owner, FeeManager)\nRoles have broad permissions to simplify governance \u2014 trust is placed in proper key management.\n\nZRC20 token wrappers\nUses wrapped tokens instead of native bridging \u2014 allows consistent token handling across chains.\n\nNo array length restrictions in admin functions\nAdmin functions accept flexible input sizes \u2014 relies on trusted roles to avoid abuse.","order":8},{"question":"Please provide links to previous audits (if any).","answer":"Audit Reports & Security Programs\nZetaChain Audit Reports Repository\nAll formal audits are published here, including PDF reports and summaries:\nhttps://github.com/zeta-chain/audit-reports\n\nBug Bounty on HackenProof\nOngoing bug bounty program inviting ethical hackers to identify vulnerabilities:\nhttps://hackenproof.com/zetachain\n\nCompetitive Audits\nCode4rena (November 2023)\nCommunity audit covering core protocol logic and smart contracts:\nhttps://code4rena.com/audits/2023-11-zetachain\n\nCantina (January 2024)\nCompetitive audit focused on ZetaChain\u2019s cross-chain mechanics:\nhttps://cantina.xyz/competitions/80a33cf0-ad69-4163-a269-d27756aacb5e\n\n\n","order":9},{"question":"Please list any relevant protocol resources.","answer":"Documentation\nFull technical and developer documentation:\nhttps://docs.zetachain.com\n\nWhitepaper\nDetailed explanation of ZetaChain\u2019s architecture and omnichain design:\nhttps://www.zetachain.com/whitepaper.pdf\n\nWebsite\nOfficial website with protocol overview and ecosystem details:\nhttps://www.zetachain.com\n\n","order":10},{"question":"Additional audit information.","answer":"We\u2019d like Watsons to focus on the following areas:\n\nCross-chain message execution logic\nHow CCTXs are processed across chains, with special attention to replay protection and data validation.\n\nToken wrapping and unwrapping (ZRC20)\nFocus on mint/burn logic, 1:1 backing assumptions, and any trust assumptions around whitelisted tokens.\n\nRole-based access control and upgradeability\nVerify that role permissions (Owner, Keeper, FeeManager, TSS) are scoped appropriately and can't be abused. Check any upgrade paths and emergency controls.\n\nFee handling and gas reimbursements\nInspect logic for calculating and distributing fees, including any assumptions about gas coin behavior (e.g., ZRC20-gas).\n\nThreat Model: Solana Integration (withdrawAndCall)\nThis threat model focuses on the Solana integration for the withdrawAndCall functionality, which allows users to withdraw assets from ZetaChain and invoke a function on a target Solana contract. It involves both node-level logic and on-chain Solana programs.\n\nNode PRs (Message validation, gas logic, and replay protection)\nValidates Solana messages from ZetaChain\nhttps://github.com/zeta-chain/node/pull/3450\n\nAdjusts gas estimates for Solana transactions\nhttps://github.com/zeta-chain/node/pull/3520\n\nHandles Solana transaction construction\nhttps://github.com/zeta-chain/node/pull/3638\n\nTracks and enforces nonces for Solana messages\nhttps://github.com/zeta-chain/node/pull/3637\n\nAdds pre-execution simulation for Solana transactions\nhttps://github.com/zeta-chain/node/pull/3527\n\nProtocol Contract PRs (Solana-side logic for withdrawAndCall)\nImplements core withdrawAndCall instruction\nhttps://github.com/zeta-chain/protocol-contracts-solana/pull/74\n\nSecures Cross-Program Invocations (CPI)\nhttps://github.com/zeta-chain/protocol-contracts-solana/pull/77\n\nClarification of how to consider a DOS to be >=7 days:\nIf the DOS requires a hard fork to be fixed, then it should be considered as a seven-day DOS that can be valid. If the DOS can be resolved in a simpler way, then it's considered less than seven days and invalid\n\nThe precompiles currently disabled in protocol, https://github.com/zeta-chain/node/blob/a32ffd88ea9957c8f3424466bb358b4197d41889/precompiles/staking/staking.go#L181-L184 However, if you find bugs regarding them, feel free to submit them and they will be considered as valid.","order":11}],"lead_judge_avatar_url":"https://sherlock-files.ams3.digitaloceanspaces.com/profile_images/defaults/default_avatar_2.png","lead_judge_handle":"Oblivionis","lead_senior_auditor_handle":"g","id":857,"lead_judge_fixed_pay":9000,"lead_senior_auditor_fixed_pay":60900,"scope":[{"repo":"zeta-chain/ethermint","branch_name":"main","commit_hash":"ea52413a15f3088afbce30f60fd0705df6adaa8b","total_nsloc":19605,"files":[{"name":"app/ante/ante.go","nsloc":115},{"name":"app/ante/authz.go","nsloc":64},{"name":"app/ante/eip712.go","nsloc":232},{"name":"app/ante/eth.go","nsloc":246},{"name":"app/ante/fee_checker.go","nsloc":93},{"name":"app/ante/fee_market.go","nsloc":38},{"name":"app/ante/fees.go","nsloc":137},{"name":"app/ante/handler_options.go","nsloc":82},{"name":"app/ante/interfaces.go","nsloc":36},{"name":"app/ante/reject_msgs.go","nsloc":19},{"name":"app/ante/setup.go","nsloc":137},{"name":"app/ante/sigverify.go","nsloc":54},{"name":"app/app.go","nsloc":782},{"name":"app/export.go","nsloc":185},{"name":"app/signer.go","nsloc":36},{"name":"app/upgrades.go","nsloc":14},{"name":"app/utils.go","nsloc":166},{"name":"client/config.go","nsloc":47},{"name":"client/debug/debug.go","nsloc":116},{"name":"client/docs/config.json","nsloc":null},{"name":"client/docs/statik/init.go","nsloc":1},{"name":"client/docs/statik/statik.go","nsloc":8},{"name":"client/export.go","nsloc":69},{"name":"client/import.go","nsloc":37},{"name":"client/keys/add.go","nsloc":231},{"name":"client/keys.go","nsloc":73},{"name":"client/keys/utils.go","nsloc":45},{"name":"client/testnet.go","nsloc":469},{"name":"cmd/config/config.go","nsloc":36},{"name":"cmd/ethermintd/flags.go","nsloc":18},{"name":"cmd/ethermintd/genaccounts.go","nsloc":155},{"name":"cmd/ethermintd/main.go","nsloc":24},{"name":"cmd/ethermintd/root.go","nsloc":215},{"name":"crypto/codec/amino.go","nsloc":17},{"name":"crypto/codec/codec.go","nsloc":10},{"name":"crypto/ethsecp256k1/ethsecp256k1.go","nsloc":149},{"name":"crypto/hd/algorithm.go","nsloc":72},{"name":"encoding/codec/codec.go","nsloc":19},{"name":"encoding/config.go","nsloc":58},{"name":"indexer/kv_indexer.go","nsloc":179},{"name":"rpc/apis.go","nsloc":165},{"name":"rpc/backend/account_info.go","nsloc":162},{"name":"rpc/backend/backend.go","nsloc":134},{"name":"rpc/backend/blocks.go","nsloc":382},{"name":"rpc/backend/call_tx.go","nsloc":308},{"name":"rpc/backend/chain_info.go","nsloc":198},{"name":"rpc/backend/filters.go","nsloc":26},{"name":"rpc/backend/node_info.go","nsloc":237},{"name":"rpc/backend/sign_tx.go","nsloc":111},{"name":"rpc/backend/tracing.go","nsloc":147},{"name":"rpc/backend/tx_info.go","nsloc":313},{"name":"rpc/backend/utils.go","nsloc":226},{"name":"rpc/ethereum/pubsub/pubsub.go","nsloc":114},{"name":"rpc/namespaces/ethereum/debug/api.go","nsloc":238},{"name":"rpc/namespaces/ethereum/debug/trace_fallback.go","nsloc":12},{"name":"rpc/namespaces/ethereum/debug/trace.go","nsloc":56},{"name":"rpc/namespaces/ethereum/debug/utils.go","nsloc":47},{"name":"rpc/namespaces/ethereum/eth/api.go","nsloc":303},{"name":"rpc/namespaces/ethereum/eth/filters/api.go","nsloc":475},{"name":"rpc/namespaces/ethereum/eth/filters/filters.go","nsloc":196},{"name":"rpc/namespaces/ethereum/eth/filters/filter_system.go","nsloc":238},{"name":"rpc/namespaces/ethereum/eth/filters/subscription.go","nsloc":45},{"name":"rpc/namespaces/ethereum/eth/filters/utils.go","nsloc":85},{"name":"rpc/namespaces/ethereum/miner/api.go","nsloc":31},{"name":"rpc/namespaces/ethereum/miner/unsupported.go","nsloc":29},{"name":"rpc/namespaces/ethereum/net/api.go","nsloc":41},{"name":"rpc/namespaces/ethereum/personal/api.go","nsloc":118},{"name":"rpc/namespaces/ethereum/txpool/api.go","nsloc":37},{"name":"rpc/namespaces/ethereum/web3/api.go","nsloc":16},{"name":"rpc/types/addrlock.go","nsloc":26},{"name":"rpc/types/block.go","nsloc":148},{"name":"rpc/types/events.go","nsloc":175},{"name":"rpc/types/query_client.go","nsloc":39},{"name":"rpc/types/types.go","nsloc":65},{"name":"rpc/types/utils.go","nsloc":212},{"name":"rpc/websockets.go","nsloc":557},{"name":"server/config/config.go","nsloc":237},{"name":"server/config/toml.go","nsloc":65},{"name":"server/flags/flags.go","nsloc":70},{"name":"server/indexer_cmd.go","nsloc":103},{"name":"server/indexer_service.go","nsloc":88},{"name":"server/json_rpc.go","nsloc":121},{"name":"server/start.go","nsloc":520},{"name":"server/util.go","nsloc":107},{"name":"store/cachekv/internal/btree.go","nsloc":87},{"name":"store/cachekv/internal/memiterator.go","nsloc":98},{"name":"store/cachekv/internal/mergeiterator.go","nsloc":154},{"name":"store/cachekv/store.go","nsloc":119},{"name":"store/cachemulti/store.go","nsloc":126},{"name":"x/evm/client/cli/query.go","nsloc":105},{"name":"x/evm/client/cli/tx.go","nsloc":88},{"name":"x/evm/client/cli/utils.go","nsloc":34},{"name":"x/evm/genesis.go","nsloc":77},{"name":"x/evm/keeper/abci.go","nsloc":16},{"name":"x/evm/keeper/config.go","nsloc":45},{"name":"x/evm/keeper/gas.go","nsloc":45},{"name":"x/evm/keeper/grpc_query.go","nsloc":508},{"name":"x/evm/keeper/hooks.go","nsloc":21},{"name":"x/evm/keeper/keeper.go","nsloc":256},{"name":"x/evm/keeper/migrations.go","nsloc":14},{"name":"x/evm/keeper/msg_server.go","nsloc":109},{"name":"x/evm/keeper/params.go","nsloc":25},{"name":"x/evm/keeper/statedb.go","nsloc":158},{"name":"x/evm/keeper/state_transition.go","nsloc":273},{"name":"x/evm/keeper/tracer.go","nsloc":28},{"name":"x/evm/keeper/utils.go","nsloc":121},{"name":"x/evm/module.go","nsloc":112},{"name":"x/evm/simulation/decoder.go","nsloc":24},{"name":"x/evm/statedb/access_list.go","nsloc":65},{"name":"x/evm/statedb/config.go","nsloc":35},{"name":"x/evm/statedb/interfaces.go","nsloc":19},{"name":"x/evm/statedb/journal.go","nsloc":172},{"name":"x/evm/statedb/native.go","nsloc":17},{"name":"x/evm/statedb/statedb.go","nsloc":428},{"name":"x/evm/statedb/state_object.go","nsloc":176},{"name":"x/evm/statedb/transient_storage.go","nsloc":28},{"name":"x/evm/types/access_list.go","nsloc":37},{"name":"x/evm/types/access_list_tx.go","nsloc":186},{"name":"x/evm/types/chain_config.go","nsloc":156},{"name":"x/evm/types/codec.go","nsloc":70},{"name":"x/evm/types/compiled_contract.go","nsloc":81},{"name":"x/evm/types/dynamic_fee_tx.go","nsloc":212},{"name":"x/evm/types/ERC20Contract.json","nsloc":null},{"name":"x/evm/types/errors.go","nsloc":79},{"name":"x/evm/types/events.go","nsloc":21},{"name":"x/evm/types/genesis.go","nsloc":36},{"name":"x/evm/types/interfaces.go","nsloc":48},{"name":"x/evm/types/key.go","nsloc":38},{"name":"x/evm/types/legacy_tx.go","nsloc":161},{"name":"x/evm/types/logs.go","nsloc":103},{"name":"x/evm/types/msg.go","nsloc":322},{"name":"x/evm/types/params.go","nsloc":97},{"name":"x/evm/types/params_legacy.go","nsloc":23},{"name":"x/evm/types/query.go","nsloc":20},{"name":"x/evm/types/query.pb.gw.go","nsloc":912},{"name":"x/evm/types/SimpleStorageContract.json","nsloc":null},{"name":"x/evm/types/storage.go","nsloc":45},{"name":"x/evm/types/TestMessageCall.json","nsloc":null},{"name":"x/evm/types/tracer.go","nsloc":39},{"name":"x/evm/types/tx_args.go","nsloc":207},{"name":"x/evm/types/tx_data.go","nsloc":89},{"name":"x/evm/types/tx.go","nsloc":34},{"name":"x/evm/types/tx.pb.gw.go","nsloc":122},{"name":"x/evm/types/utils.go","nsloc":73},{"name":"x/feemarket/client/cli/query.go","nsloc":92},{"name":"x/feemarket/genesis.go","nsloc":26},{"name":"x/feemarket/keeper/abci.go","nsloc":53},{"name":"x/feemarket/keeper/eip1559.go","nsloc":52},{"name":"x/feemarket/keeper/grpc_query.go","nsloc":32},{"name":"x/feemarket/keeper/keeper.go","nsloc":78},{"name":"x/feemarket/keeper/migrations.go","nsloc":19},{"name":"x/feemarket/keeper/msg_server.go","nsloc":18},{"name":"x/feemarket/keeper/params.go","nsloc":50},{"name":"x/feemarket/migrations/v4/migrate.go","nsloc":28},{"name":"x/feemarket/migrations/v4/types/params.go","nsloc":157},{"name":"x/feemarket/module.go","nsloc":110},{"name":"x/feemarket/spec/01_concepts.md","nsloc":null},{"name":"x/feemarket/spec/02_state.md","nsloc":null},{"name":"x/feemarket/spec/03_begin_block.md","nsloc":null},{"name":"x/feemarket/spec/04_end_block.md","nsloc":null},{"name":"x/feemarket/spec/05_keeper.md","nsloc":null},{"name":"x/feemarket/spec/06_events.md","nsloc":null},{"name":"x/feemarket/spec/07_params.md","nsloc":null},{"name":"x/feemarket/spec/08_client.md","nsloc":null},{"name":"x/feemarket/spec/09_antehandlers.md","nsloc":null},{"name":"x/feemarket/spec/10_future_improvements.md","nsloc":null},{"name":"x/feemarket/spec/README.md","nsloc":null},{"name":"x/feemarket/types/codec.go","nsloc":29},{"name":"x/feemarket/types/events.go","nsloc":5},{"name":"x/feemarket/types/genesis.go","nsloc":16},{"name":"x/feemarket/types/interfaces.go","nsloc":11},{"name":"x/feemarket/types/keys.go","nsloc":20},{"name":"x/feemarket/types/msg.go","nsloc":19},{"name":"x/feemarket/types/params.go","nsloc":158},{"name":"x/feemarket/types/query.pb.gw.go","nsloc":209},{"name":"x/feemarket/types/tx.pb.gw.go","nsloc":122}]},{"repo":"zeta-chain/go-tss","branch_name":"master","commit_hash":"032c92237bb9bd7e952cb57d4c6d126cf0d70fdd","total_nsloc":4988,"files":[{"name":"common/local_cache_item.go","nsloc":38},{"name":"common/status.go","nsloc":7},{"name":"common/tss.go","nsloc":772},{"name":"common/tss_helper.go","nsloc":302},{"name":"common/types.go","nsloc":16},{"name":"conversion/bech32.go","nsloc":10},{"name":"conversion/conversion.go","nsloc":195},{"name":"conversion/key_provider.go","nsloc":122},{"name":"conversion/tss_helper.go","nsloc":65},{"name":"keysign/ecdsa/tss_keysign.go","nsloc":252},{"name":"keysign/eddsa/tss_keysign.go","nsloc":248},{"name":"keysign/keysign.go","nsloc":12},{"name":"keysign/notifier.go","nsloc":127},{"name":"keysign/request.go","nsloc":17},{"name":"keysign/response.go","nsloc":31},{"name":"keysign/signature_notifier.go","nsloc":235},{"name":"messages/generate.sh","nsloc":null},{"name":"messages/join_party.proto","nsloc":null},{"name":"messages/p2p_message.go","nsloc":62},{"name":"messages/signature_notifier.proto","nsloc":null},{"name":"messages/tss_messages.go","nsloc":37},{"name":"messages/version.go","nsloc":4},{"name":"monitor/metric.go","nsloc":111},{"name":"p2p/communication.go","nsloc":419},{"name":"p2p/message_id_subscriber.go","nsloc":36},{"name":"p2p/metric_reporter.go","nsloc":48},{"name":"p2p/party_coordinator.go","nsloc":470},{"name":"p2p/peer_address.go","nsloc":15},{"name":"p2p/peer_status.go","nsloc":102},{"name":"p2p/types.go","nsloc":27},{"name":"p2p/whitelist_connection_gater.go","nsloc":63},{"name":"storage/localstate_mgr.go","nsloc":250},{"name":"storage/mock_localstatemanager.go","nsloc":19},{"name":"tss/keygen.go","nsloc":314},{"name":"tss/keysign.go","nsloc":327},{"name":"tss/tss.go","nsloc":235}]},{"repo":"zeta-chain/node","branch_name":"develop","commit_hash":"a32ffd88ea9957c8f3424466bb358b4197d41889","total_nsloc":66845,"files":[{"name":"app/ante/ante.go","nsloc":132},{"name":"app/ante/authz.go","nsloc":75},{"name":"app/ante/fees.go","nsloc":169},{"name":"app/ante/handler_options.go","nsloc":170},{"name":"app/ante/interfaces.go","nsloc":32},{"name":"app/ante/system_tx_priority_decorator.go","nsloc":20},{"name":"app/ante/vesting.go","nsloc":40},{"name":"app/app.go","nsloc":803},{"name":"app/config.go","nsloc":103},{"name":"app/encoding.go","nsloc":59},{"name":"app/export.go","nsloc":210},{"name":"app/modules.go","nsloc":152},{"name":"app/params/encoding.go","nsloc":12},{"name":"app/params/proto.go","nsloc":18},{"name":"app/setup_handlers.go","nsloc":58},{"name":"app/upgrade_tracker.go","nsloc":76},{"name":"pkg/authz/authz_tx_types.go","nsloc":23},{"name":"pkg/bg/bg.go","nsloc":73},{"name":"pkg/chains/address.go","nsloc":86},{"name":"pkg/chains/bitcoin.go","nsloc":39},{"name":"pkg/chains/bitcoin_testnet4.go","nsloc":161},{"name":"pkg/chains/chain_filters.go","nsloc":39},{"name":"pkg/chains/chain.go","nsloc":191},{"name":"pkg/chains/chain_id.go","nsloc":26},{"name":"pkg/chains/chains.go","nsloc":527},{"name":"pkg/chains/conversion.go","nsloc":21},{"name":"pkg/chains/fast_confirmation.go","nsloc":21},{"name":"pkg/chains/status.go","nsloc":12},{"name":"pkg/coin/coin.go","nsloc":33},{"name":"pkg/constant/constant.go","nsloc":15},{"name":"pkg/constant/version.go","nsloc":25},{"name":"pkg/contracts/solana/account_info.go","nsloc":23},{"name":"pkg/contracts/solana/gateway.go","nsloc":47},{"name":"pkg/contracts/solana/gateway_message.go","nsloc":459},{"name":"pkg/contracts/solana/idl.go","nsloc":56},{"name":"pkg/contracts/solana/inbound.go","nsloc":211},{"name":"pkg/contracts/solana/instruction.go","nsloc":316},{"name":"pkg/contracts/solana/pda.go","nsloc":9},{"name":"pkg/contracts/sui/crypto.go","nsloc":107},{"name":"pkg/contracts/sui/deposit.go","nsloc":66},{"name":"pkg/contracts/sui/gateway.go","nsloc":207},{"name":"pkg/contracts/sui/withdrawal.go","nsloc":58},{"name":"pkg/contracts/ton/coin.go","nsloc":16},{"name":"pkg/contracts/ton/gateway_deploy.go","nsloc":55},{"name":"pkg/contracts/ton/gateway.go","nsloc":97},{"name":"pkg/contracts/ton/gateway_msg.go","nsloc":146},{"name":"pkg/contracts/ton/gateway_parse.go","nsloc":220},{"name":"pkg/contracts/ton/gateway_send.go","nsloc":89},{"name":"pkg/contracts/ton/gateway_tx.go","nsloc":42},{"name":"pkg/contracts/ton/tlb.go","nsloc":54},{"name":"pkg/contracts/uniswap/lib/contracts/libraries/transferhelper.sol/transferhelper.go","nsloc":132},{"name":"pkg/contracts/uniswap/v2-core/contracts/libraries/math.sol/math.go","nsloc":132},{"name":"pkg/contracts/uniswap/v2-core/contracts/libraries/safemath.sol/safemath.go","nsloc":132},{"name":"pkg/contracts/uniswap/v2-core/contracts/libraries/uq112x112.sol/uq112x112.go","nsloc":132},{"name":"pkg/contracts/uniswap/v2-core/contracts/uniswapv2erc20.sol/uniswapv2erc20.go","nsloc":535},{"name":"pkg/contracts/uniswap/v2-core/contracts/uniswapv2factory.sol/uniswapv2factory.go","nsloc":351},{"name":"pkg/contracts/uniswap/v2-core/contracts/uniswapv2pair.sol/uniswapv2pair.go","nsloc":1171},{"name":"pkg/contracts/uniswap/v2-periphery/contracts/libraries/safemath.sol/safemath.go","nsloc":132},{"name":"pkg/contracts/uniswap/v2-periphery/contracts/libraries/uniswapv2library.sol/uniswapv2library.go","nsloc":132},{"name":"pkg/contracts/uniswap/v2-periphery/contracts/uniswapv2router02.sol/uniswapv2router02.go","nsloc":399},{"name":"pkg/cosmos/cosmos.go","nsloc":9},{"name":"pkg/crypto/aes256_gcm.go","nsloc":81},{"name":"pkg/crypto/evm_address.go","nsloc":18},{"name":"pkg/crypto/pubkey.go","nsloc":147},{"name":"pkg/crypto/tss.go","nsloc":42},{"name":"pkg/errors/crosschain_error_message.go","nsloc":141},{"name":"pkg/fanout/fanout.go","nsloc":90},{"name":"pkg/gas/gas_limits.go","nsloc":15},{"name":"pkg/graceful/graceful.go","nsloc":132},{"name":"pkg/math/bits/bits.go","nsloc":28},{"name":"pkg/math/cosmos_uint.go","nsloc":14},{"name":"pkg/math/float.go","nsloc":17},{"name":"pkg/math/slice.go","nsloc":31},{"name":"pkg/memo/arg.go","nsloc":34},{"name":"pkg/memo/codec_abi.go","nsloc":60},{"name":"pkg/memo/codec_compact.go","nsloc":186},{"name":"pkg/memo/codec.go","nsloc":40},{"name":"pkg/memo/fields.go","nsloc":7},{"name":"pkg/memo/fields_v0.go","nsloc":126},{"name":"pkg/memo/header.go","nsloc":76},{"name":"pkg/memo/memo.go","nsloc":69},{"name":"pkg/mempool/custom_proposal_handler.go","nsloc":188},{"name":"pkg/mempool/priority_nonce_mempool.go","nsloc":290},{"name":"pkg/mempool/senders_with_nonce.go","nsloc":56},{"name":"pkg/os/console.go","nsloc":32},{"name":"pkg/os/path.go","nsloc":26},{"name":"pkg/proofs/ethereum/proof.go","nsloc":123},{"name":"pkg/proofs/headers.go","nsloc":131},{"name":"pkg/proofs/proof.go","nsloc":82},{"name":"pkg/ptr/ptr.go","nsloc":11},{"name":"pkg/retry/retry.go","nsloc":88},{"name":"pkg/rpc/clients_authority.go","nsloc":13},{"name":"pkg/rpc/clients_cosmos.go","nsloc":25},{"name":"pkg/rpc/clients_crosschain.go","nsloc":108},{"name":"pkg/rpc/clients_ethermint.go","nsloc":17},{"name":"pkg/rpc/clients.go","nsloc":71},{"name":"pkg/rpc/clients_lightclient.go","nsloc":43},{"name":"pkg/rpc/clients_observer.go","nsloc":152},{"name":"pkg/rpc/clients_tendermint.go","nsloc":24},{"name":"pkg/scheduler/context.go","nsloc":40},{"name":"pkg/scheduler/metrics.go","nsloc":23},{"name":"pkg/scheduler/opts.go","nsloc":30},{"name":"pkg/scheduler/scheduler.go","nsloc":168},{"name":"pkg/scheduler/tickers.go","nsloc":109},{"name":"pkg/sdkconfig/default/default.go","nsloc":5},{"name":"pkg/sdkconfig/sdkconfig.go","nsloc":26},{"name":"pkg/ticker/ticker.go","nsloc":134},{"name":"precompiles/bank/bank.go","nsloc":136},{"name":"precompiles/bank/bindings.go","nsloc":2},{"name":"precompiles/bank/const.go","nsloc":13},{"name":"precompiles/bank/logs.go","nsloc":41},{"name":"precompiles/bank/method_balance_of.go","nsloc":58},{"name":"precompiles/bank/method_deposit.go","nsloc":93},{"name":"precompiles/bank/method_withdraw.go","nsloc":106},{"name":"precompiles/logs/logs.go","nsloc":52},{"name":"precompiles/precompiles.go","nsloc":59},{"name":"precompiles/prototype/bindings.go","nsloc":2},{"name":"precompiles/prototype/prototype.go","nsloc":201},{"name":"precompiles/staking/bindings.go","nsloc":2},{"name":"precompiles/staking/const.go","nsloc":22},{"name":"precompiles/staking/logs.go","nsloc":145},{"name":"precompiles/staking/method_claim_rewards.go","nsloc":106},{"name":"precompiles/staking/method_distribute.go","nsloc":84},{"name":"precompiles/staking/method_get_all_validators.go","nsloc":24},{"name":"precompiles/staking/method_get_rewards.go","nsloc":76},{"name":"precompiles/staking/method_get_shares.go","nsloc":50},{"name":"precompiles/staking/method_get_validators.go","nsloc":49},{"name":"precompiles/staking/method_move_stake.go","nsloc":77},{"name":"precompiles/staking/method_stake.go","nsloc":80},{"name":"precompiles/staking/method_unstake.go","nsloc":70},{"name":"precompiles/staking/staking.go","nsloc":239},{"name":"precompiles/types/address.go","nsloc":40},{"name":"precompiles/types/coin.go","nsloc":51},{"name":"precompiles/types/errors.go","nsloc":81},{"name":"precompiles/types/types.go","nsloc":93},{"name":"rpc/apis.go","nsloc":165},{"name":"rpc/backend/account_info.go","nsloc":177},{"name":"rpc/backend/backend.go","nsloc":164},{"name":"rpc/backend/blocks.go","nsloc":492},{"name":"rpc/backend/call_tx.go","nsloc":324},{"name":"rpc/backend/chain_info.go","nsloc":214},{"name":"rpc/backend/filters.go","nsloc":26},{"name":"rpc/backend/node_info.go","nsloc":241},{"name":"rpc/backend/sign_tx.go","nsloc":119},{"name":"rpc/backend/tracing.go","nsloc":108},{"name":"rpc/backend/tx_info.go","nsloc":387},{"name":"rpc/backend/utils.go","nsloc":250},{"name":"rpc/ethereum/pubsub/pubsub.go","nsloc":114},{"name":"rpc/namespaces/ethereum/debug/api.go","nsloc":242},{"name":"rpc/namespaces/ethereum/debug/trace_fallback.go","nsloc":12},{"name":"rpc/namespaces/ethereum/debug/trace.go","nsloc":57},{"name":"rpc/namespaces/ethereum/debug/utils.go","nsloc":30},{"name":"rpc/namespaces/ethereum/eth/api.go","nsloc":324},{"name":"rpc/namespaces/ethereum/eth/filters/api.go","nsloc":533},{"name":"rpc/namespaces/ethereum/eth/filters/filters.go","nsloc":208},{"name":"rpc/namespaces/ethereum/eth/filters/filter_system.go","nsloc":238},{"name":"rpc/namespaces/ethereum/eth/filters/subscription.go","nsloc":45},{"name":"rpc/namespaces/ethereum/eth/filters/utils.go","nsloc":90},{"name":"rpc/namespaces/ethereum/miner/api.go","nsloc":31},{"name":"rpc/namespaces/ethereum/miner/unsupported.go","nsloc":29},{"name":"rpc/namespaces/ethereum/net/api.go","nsloc":41},{"name":"rpc/namespaces/ethereum/personal/api.go","nsloc":131},{"name":"rpc/namespaces/ethereum/txpool/api.go","nsloc":37},{"name":"rpc/namespaces/ethereum/web3/api.go","nsloc":24},{"name":"rpc/types/addrlock.go","nsloc":26},{"name":"rpc/types/block.go","nsloc":172},{"name":"rpc/types/events.go","nsloc":339},{"name":"rpc/types/query_client.go","nsloc":39},{"name":"rpc/types/types.go","nsloc":76},{"name":"rpc/types/utils.go","nsloc":257},{"name":"rpc/websockets.go","nsloc":637},{"name":"x/authority/client/cli/query_authorization_list.go","nsloc":47},{"name":"x/authority/client/cli/query_chain_info.go","nsloc":26},{"name":"x/authority/client/cli/query.go","nsloc":23},{"name":"x/authority/client/cli/query_policies.go","nsloc":26},{"name":"x/authority/client/cli/tx_add_authorization.go","nsloc":54},{"name":"x/authority/client/cli/tx.go","nsloc":24},{"name":"x/authority/client/cli/tx_remove_authorization.go","nsloc":31},{"name":"x/authority/client/cli/tx_remove_chain_info.go","nsloc":32},{"name":"x/authority/client/cli/tx_update_chain_info.go","nsloc":46},{"name":"x/authority/client/cli/tx_update_policies.go","nsloc":45},{"name":"x/authority/genesis.go","nsloc":27},{"name":"x/authority/keeper/authorization_list.go","nsloc":48},{"name":"x/authority/keeper/chain_info.go","nsloc":28},{"name":"x/authority/keeper/grpc_query_authorization_list.go","nsloc":45},{"name":"x/authority/keeper/grpc_query_chain_info.go","nsloc":23},{"name":"x/authority/keeper/grpc_query.go","nsloc":5},{"name":"x/authority/keeper/grpc_query_policies.go","nsloc":22},{"name":"x/authority/keeper/keeper.go","nsloc":40},{"name":"x/authority/keeper/migrator.go","nsloc":20},{"name":"x/authority/keeper/msg_server_add_authorization.go","nsloc":28},{"name":"x/authority/keeper/msg_server.go","nsloc":9},{"name":"x/authority/keeper/msg_server_remove_authorization.go","nsloc":33},{"name":"x/authority/keeper/msg_server_remove_chain_info.go","nsloc":33},{"name":"x/authority/keeper/msg_server_update_chain_info.go","nsloc":34},{"name":"x/authority/keeper/msg_server_update_policies.go","nsloc":24},{"name":"x/authority/keeper/policies.go","nsloc":20},{"name":"x/authority/migrations/v3/migrate.go","nsloc":52},{"name":"x/authority/module.go","nsloc":111},{"name":"x/authority/types/authorization_list.go","nsloc":123},{"name":"x/authority/types/chain_info.go","nsloc":21},{"name":"x/authority/types/codec.go","nsloc":24},{"name":"x/authority/types/errors.go","nsloc":14},{"name":"x/authority/types/genesis.go","nsloc":20},{"name":"x/authority/types/keys.go","nsloc":16},{"name":"x/authority/types/message_add_authorization.go","nsloc":50},{"name":"x/authority/types/message_remove_authorization.go","nsloc":40},{"name":"x/authority/types/message_remove_chain_info.go","nsloc":37},{"name":"x/authority/types/message_update_chain_info.go","nsloc":42},{"name":"x/authority/types/message_update_policies.go","nsloc":41},{"name":"x/authority/types/policies.go","nsloc":53},{"name":"x/authority/types/policy_type.go","nsloc":9},{"name":"x/authority/types/query.pb.gw.go","nsloc":288},{"name":"x/crosschain/client/cli/cli_gas_price.go","nsloc":96},{"name":"x/crosschain/client/cli/cli_last_block_height.go","nsloc":54},{"name":"x/crosschain/client/cli/cli_refund_aborted.go","nsloc":29},{"name":"x/crosschain/client/cli/cli_tss.go","nsloc":54},{"name":"x/crosschain/client/cli/cli_whitelist_erc20.go","nsloc":56},{"name":"x/crosschain/client/cli/cli_zeta_height.go","nsloc":29},{"name":"x/crosschain/client/cli/query_cctx.go","nsloc":85},{"name":"x/crosschain/client/cli/query_cctx_rate_limit.go","nsloc":27},{"name":"x/crosschain/client/cli/query.go","nsloc":36},{"name":"x/crosschain/client/cli/query_inbound_hash_to_cctx.go","nsloc":77},{"name":"x/crosschain/client/cli/query_inbound_tracker.go","nsloc":91},{"name":"x/crosschain/client/cli/query_outbound_tracker.go","nsloc":64},{"name":"x/crosschain/client/cli/query_rate_limiter_flags.go","nsloc":25},{"name":"x/crosschain/client/cli/query_zeta_accounting.go","nsloc":29},{"name":"x/crosschain/client/cli/tx_add_outbound_tracker.go","nsloc":36},{"name":"x/crosschain/client/cli/tx.go","nsloc":31},{"name":"x/crosschain/client/cli/tx_inbound_tracker.go","nsloc":68},{"name":"x/crosschain/client/cli/tx_remove_outbound_tracker.go","nsloc":38},{"name":"x/crosschain/client/cli/tx_vote_inbound.go","nsloc":109},{"name":"x/crosschain/client/cli/tx_vote_outbound.go","nsloc":87},{"name":"x/crosschain/client/cli/tx_vote_stuck_cctx.go","nsloc":25},{"name":"x/crosschain/genesis.go","nsloc":71},{"name":"x/crosschain/keeper/abci.go","nsloc":136},{"name":"x/crosschain/keeper/abort.go","nsloc":168},{"name":"x/crosschain/keeper/cctx_counter.go","nsloc":38},{"name":"x/crosschain/keeper/cctx_gateway_observers.go","nsloc":62},{"name":"x/crosschain/keeper/cctx_gateways.go","nsloc":18},{"name":"x/crosschain/keeper/cctx_gateway_zevm.go","nsloc":43},{"name":"x/crosschain/keeper/cctx.go","nsloc":104},{"name":"x/crosschain/keeper/cctx_orchestrator_validate_inbound.go","nsloc":80},{"name":"x/crosschain/keeper/cctx_orchestrator_validate_outbound.go","nsloc":335},{"name":"x/crosschain/keeper/cctx_utils.go","nsloc":105},{"name":"x/crosschain/keeper/events.go","nsloc":74},{"name":"x/crosschain/keeper/evm_deposit.go","nsloc":132},{"name":"x/crosschain/keeper/evm_hooks.go","nsloc":327},{"name":"x/crosschain/keeper/finalized_inbounds.go","nsloc":30},{"name":"x/crosschain/keeper/foreign_coins.go","nsloc":13},{"name":"x/crosschain/keeper/gas_payment.go","nsloc":341},{"name":"x/crosschain/keeper/gas_price.go","nsloc":55},{"name":"x/crosschain/keeper/grpc_query_cctx.go","nsloc":183},{"name":"x/crosschain/keeper/grpc_query_cctx_rate_limit.go","nsloc":308},{"name":"x/crosschain/keeper/grpc_query_gas_price.go","nsloc":53},{"name":"x/crosschain/keeper/grpc_query.go","nsloc":5},{"name":"x/crosschain/keeper/grpc_query_inbound_hash_to_cctx.go","nsloc":73},{"name":"x/crosschain/keeper/grpc_query_inbound_tracker.go","nsloc":48},{"name":"x/crosschain/keeper/grpc_query_last_block_height.go","nsloc":55},{"name":"x/crosschain/keeper/grpc_query_last_zeta_height.go","nsloc":24},{"name":"x/crosschain/keeper/grpc_query_legacy.go","nsloc":53},{"name":"x/crosschain/keeper/grpc_query_outbound_tracker.go","nsloc":77},{"name":"x/crosschain/keeper/grpc_query_rate_limiter_flags.go","nsloc":22},{"name":"x/crosschain/keeper/grpc_query_zeta_conversion_rate.go","nsloc":49},{"name":"x/crosschain/keeper/inbound_hash_to_cctx.go","nsloc":48},{"name":"x/crosschain/keeper/inbound_tracker.go","nsloc":95},{"name":"x/crosschain/keeper/initiate_outbound.go","nsloc":30},{"name":"x/crosschain/keeper/keeper.go","nsloc":88},{"name":"x/crosschain/keeper/last_block_height.go","nsloc":36},{"name":"x/crosschain/keeper/migrator.go","nsloc":33},{"name":"x/crosschain/keeper/msg_server_abort_stuck_cctx.go","nsloc":33},{"name":"x/crosschain/keeper/msg_server_add_inbound_tracker.go","nsloc":31},{"name":"x/crosschain/keeper/msg_server_add_outbound_tracker.go","nsloc":74},{"name":"x/crosschain/keeper/msg_server.go","nsloc":11},{"name":"x/crosschain/keeper/msg_server_migrate_erc20_custody_funds.go","nsloc":80},{"name":"x/crosschain/keeper/msg_server_migrate_tss_funds.go","nsloc":114},{"name":"x/crosschain/keeper/msg_server_refund_aborted_tx.go","nsloc":66},{"name":"x/crosschain/keeper/msg_server_remove_inbound_tracker.go","nsloc":20},{"name":"x/crosschain/keeper/msg_server_remove_outbound_tracker.go","nsloc":20},{"name":"x/crosschain/keeper/msg_server_update_erc20_custody_pause_status.go","nsloc":77},{"name":"x/crosschain/keeper/msg_server_update_rate_limiter_flags.go","nsloc":20},{"name":"x/crosschain/keeper/msg_server_update_tss.go","nsloc":68},{"name":"x/crosschain/keeper/msg_server_vote_gas_price.go","nsloc":93},{"name":"x/crosschain/keeper/msg_server_vote_inbound_tx.go","nsloc":61},{"name":"x/crosschain/keeper/msg_server_vote_outbound_tx.go","nsloc":137},{"name":"x/crosschain/keeper/msg_server_whitelist_erc20.go","nsloc":161},{"name":"x/crosschain/keeper/outbound_tracker.go","nsloc":57},{"name":"x/crosschain/keeper/rate_limiter_flags.go","nsloc":43},{"name":"x/crosschain/keeper/v2_zevm_inbound.go","nsloc":118},{"name":"x/crosschain/keeper/zeta_accounting.go","nsloc":43},{"name":"x/crosschain/migrations/v5/migrate.go","nsloc":120},{"name":"x/crosschain/module.go","nsloc":124},{"name":"x/crosschain/module_simulation.go","nsloc":25},{"name":"x/crosschain/simulation/decoders.go","nsloc":92},{"name":"x/crosschain/simulation/operation_abort_stuck_cctx.go","nsloc":112},{"name":"x/crosschain/simulation/operation_add_inbound_tracker.go","nsloc":68},{"name":"x/crosschain/simulation/operation_add_outbound_tracker.go","nsloc":136},{"name":"x/crosschain/simulation/operation_gas_price_voter.go","nsloc":82},{"name":"x/crosschain/simulation/operation_refund_aborted_cctx.go","nsloc":71},{"name":"x/crosschain/simulation/operation_remove_outbound_tracker.go","nsloc":59},{"name":"x/crosschain/simulation/operations.go","nsloc":185},{"name":"x/crosschain/simulation/operation_update_erc20_pause_status.go","nsloc":102},{"name":"x/crosschain/simulation/operation_update_rate_limiter_flags.go","nsloc":50},{"name":"x/crosschain/simulation/operation_update_tss_address.go","nsloc":109},{"name":"x/crosschain/simulation/operation_vote_inbound.go","nsloc":151},{"name":"x/crosschain/simulation/operation_vote_outbound.go","nsloc":188},{"name":"x/crosschain/simulation/operation_whitelist_erc20.go","nsloc":127},{"name":"x/crosschain/types/authz.go","nsloc":16},{"name":"x/crosschain/types/cctx.go","nsloc":265},{"name":"x/crosschain/types/cmd_cctxs.go","nsloc":271},{"name":"x/crosschain/types/codec.go","nsloc":54},{"name":"x/crosschain/types/errors.go","nsloc":61},{"name":"x/crosschain/types/expected_keepers.go","nsloc":234},{"name":"x/crosschain/types/genesis.go","nsloc":49},{"name":"x/crosschain/types/inbound_params.go","nsloc":13},{"name":"x/crosschain/types/inbound_parsing.go","nsloc":214},{"name":"x/crosschain/types/key_inbound_hash_to_cctx.go","nsloc":13},{"name":"x/crosschain/types/keys.go","nsloc":64},{"name":"x/crosschain/types/legacy_msgs.go","nsloc":131},{"name":"x/crosschain/types/message_abort_stuck_cctx.go","nsloc":41},{"name":"x/crosschain/types/message_add_inbound_tracker.go","nsloc":45},{"name":"x/crosschain/types/message_add_outbound_tracker.go","nsloc":43},{"name":"x/crosschain/types/message_migrate_erc20_custody_funds.go","nsloc":57},{"name":"x/crosschain/types/message_migrate_tss_funds.go","nsloc":43},{"name":"x/crosschain/types/message_refund_aborted.go","nsloc":46},{"name":"x/crosschain/types/message_remove_inbound_tracker.go","nsloc":42},{"name":"x/crosschain/types/message_remove_outbound_tracker.go","nsloc":42},{"name":"x/crosschain/types/message_update_erc20_custody_pause_status.go","nsloc":43},{"name":"x/crosschain/types/message_update_rate_limiter_flags.go","nsloc":40},{"name":"x/crosschain/types/message_update_tss_address.go","nsloc":43},{"name":"x/crosschain/types/message_vote_gas_price.go","nsloc":44},{"name":"x/crosschain/types/message_vote_inbound.go","nsloc":138},{"name":"x/crosschain/types/message_vote_outbound.go","nsloc":77},{"name":"x/crosschain/types/message_whitelist_erc20.go","nsloc":78},{"name":"x/crosschain/types/outbound_params.go","nsloc":32},{"name":"x/crosschain/types/outbound_tracker.go","nsloc":4},{"name":"x/crosschain/types/query.pb.gw.go","nsloc":2427},{"name":"x/crosschain/types/rate_limiter_flags.go","nsloc":101},{"name":"x/crosschain/types/revert_options.go","nsloc":74},{"name":"x/crosschain/types/status.go","nsloc":79},{"name":"x/crosschain/types/tx_body_verification.go","nsloc":138},{"name":"x/crosschain/types/types.go","nsloc":1},{"name":"x/crosschain/types/validate.go","nsloc":57},{"name":"x/emissions/abci.go","nsloc":183},{"name":"x/emissions/client/cli/query.go","nsloc":20},{"name":"x/emissions/client/cli/query_list_balances.go","nsloc":29},{"name":"x/emissions/client/cli/query_params.go","nsloc":26},{"name":"x/emissions/client/cli/query_show_available_emissions.go","nsloc":32},{"name":"x/emissions/client/cli/tx.go","nsloc":18},{"name":"x/emissions/client/cli/tx_withdraw_emissions..go","nsloc":34},{"name":"x/emissions/exported/exported.go","nsloc":11},{"name":"x/emissions/genesis.go","nsloc":25},{"name":"x/emissions/keeper/block_rewards_components.go","nsloc":11},{"name":"x/emissions/keeper/events.go","nsloc":31},{"name":"x/emissions/keeper/grpc_query.go","nsloc":5},{"name":"x/emissions/keeper/grpc_query_list_balances.go","nsloc":20},{"name":"x/emissions/keeper/grpc_query_params.go","nsloc":19},{"name":"x/emissions/keeper/grpc_query_show_available_emissions.go","nsloc":28},{"name":"x/emissions/keeper/keeper.go","nsloc":75},{"name":"x/emissions/keeper/migrator.go","nsloc":23},{"name":"x/emissions/keeper/msg_server.go","nsloc":13},{"name":"x/emissions/keeper/msg_server_update_params.go","nsloc":27},{"name":"x/emissions/keeper/msg_server_withdraw_emissions.go","nsloc":42},{"name":"x/emissions/keeper/params.go","nsloc":29},{"name":"x/emissions/keeper/withdrawable_emissions.go","nsloc":68},{"name":"x/emissions/migrations/v5/migrate.go","nsloc":60},{"name":"x/emissions/module.go","nsloc":123},{"name":"x/emissions/module_simulation.go","nsloc":25},{"name":"x/emissions/simulation/decoders.go","nsloc":32},{"name":"x/emissions/simulation/operations.go","nsloc":30},{"name":"x/emissions/simulation/operation_withdraw_emissions.go","nsloc":80},{"name":"x/emissions/types/codec.go","nsloc":22},{"name":"x/emissions/types/distributions.go","nsloc":23},{"name":"x/emissions/types/errors.go","nsloc":15},{"name":"x/emissions/types/expected_keepers.go","nsloc":36},{"name":"x/emissions/types/genesis.go","nsloc":10},{"name":"x/emissions/types/keys.go","nsloc":34},{"name":"x/emissions/types/legacy_params.go","nsloc":9},{"name":"x/emissions/types/message_update_params.go","nsloc":31},{"name":"x/emissions/types/message_withdraw_emissions.go","nsloc":39},{"name":"x/emissions/types/params.go","nsloc":122},{"name":"x/emissions/types/params_legacy.go","nsloc":25},{"name":"x/fungible/client/cli/query_code_hash.go","nsloc":28},{"name":"x/fungible/client/cli/query_foreign_coins.go","nsloc":55},{"name":"x/fungible/client/cli/query_gas_stability_pool.go","nsloc":79},{"name":"x/fungible/client/cli/query.go","nsloc":26},{"name":"x/fungible/client/cli/query_system_contract.go","nsloc":25},{"name":"x/fungible/client/cli/tx_deploy_fungible_coin_zrc_4.go","nsloc":59},{"name":"x/fungible/client/cli/tx_deploy_system_contracts.go","nsloc":30},{"name":"x/fungible/client/cli/tx.go","nsloc":29},{"name":"x/fungible/client/cli/tx_remove_foreign_coin.go","nsloc":29},{"name":"x/fungible/client/cli/tx_update_contract_bytecode.go","nsloc":29},{"name":"x/fungible/client/cli/tx_update_gateway_contract.go","nsloc":28},{"name":"x/fungible/client/cli/tx_update_system_contract.go","nsloc":28},{"name":"x/fungible/client/cli/tx_update_zrc20_liquidity_cap.go","nsloc":31},{"name":"x/fungible/client/cli/tx_update_zrc20_paused_status.go","nsloc":53},{"name":"x/fungible/client/cli/tx_update_zrc20_withdraw_fee.go","nsloc":34},{"name":"x/fungible/genesis.go","nsloc":23},{"name":"x/fungible/keeper/deposits.go","nsloc":225},{"name":"x/fungible/keeper/evm_gateway.go","nsloc":233},{"name":"x/fungible/keeper/evm.go","nsloc":709},{"name":"x/fungible/keeper/evm_hooks.go","nsloc":43},{"name":"x/fungible/keeper/foreign_coins.go","nsloc":93},{"name":"x/fungible/keeper/gas_coin_and_pool.go","nsloc":186},{"name":"x/fungible/keeper/gas_price.go","nsloc":114},{"name":"x/fungible/keeper/gas_stability_pool.go","nsloc":60},{"name":"x/fungible/keeper/grpc_query_code_hash.go","nsloc":28},{"name":"x/fungible/keeper/grpc_query_foreign_coins.go","nsloc":65},{"name":"x/fungible/keeper/grpc_query_gas_stability_pool.go","nsloc":65},{"name":"x/fungible/keeper/grpc_query.go","nsloc":5},{"name":"x/fungible/keeper/grpc_query_system_contract.go","nsloc":22},{"name":"x/fungible/keeper/keeper.go","nsloc":63},{"name":"x/fungible/keeper/migrator.go","nsloc":16},{"name":"x/fungible/keeper/msg_server_deploy_fungible_coin_zrc20.go","nsloc":74},{"name":"x/fungible/keeper/msg_server_deploy_system_contract.go","nsloc":63},{"name":"x/fungible/keeper/msg_server.go","nsloc":11},{"name":"x/fungible/keeper/msg_server_pause_zrc20.go","nsloc":41},{"name":"x/fungible/keeper/msg_server_remove_foreign_coin.go","nsloc":26},{"name":"x/fungible/keeper/msg_server_udpate_zrc20_liquidity_cap.go","nsloc":25},{"name":"x/fungible/keeper/msg_server_unpause_zrc20.go","nsloc":41},{"name":"x/fungible/keeper/msg_server_update_contract_bytecode.go","nsloc":71},{"name":"x/fungible/keeper/msg_server_update_gateway_contract.go","nsloc":65},{"name":"x/fungible/keeper/msg_server_update_system_contract.go","nsloc":131},{"name":"x/fungible/keeper/msg_server_update_zrc20_name.go","nsloc":47},{"name":"x/fungible/keeper/msg_server_update_zrc20_withdraw_fee.go","nsloc":84},{"name":"x/fungible/keeper/system_contract.go","nsloc":709},{"name":"x/fungible/keeper/zeta.go","nsloc":38},{"name":"x/fungible/keeper/zevm_msg_passing.go","nsloc":65},{"name":"x/fungible/keeper/zrc20_cosmos_coins_mapping.go","nsloc":120},{"name":"x/fungible/keeper/zrc20_methods.go","nsloc":382},{"name":"x/fungible/migrations/v3/migrate.go","nsloc":22},{"name":"x/fungible/module.go","nsloc":114},{"name":"x/fungible/module_simulation.go","nsloc":25},{"name":"x/fungible/simulation/decoders.go","nsloc":29},{"name":"x/fungible/simulation/operations.go","nsloc":62},{"name":"x/fungible/types/codec.go","nsloc":40},{"name":"x/fungible/types/errors.go","nsloc":48},{"name":"x/fungible/types/evm.go","nsloc":12},{"name":"x/fungible/types/expected_keepers.go","nsloc":58},{"name":"x/fungible/types/gas_stablity_pool.go","nsloc":13},{"name":"x/fungible/types/genesis.go","nsloc":21},{"name":"x/fungible/types/key_foreign_coins.go","nsloc":13},{"name":"x/fungible/types/keys.go","nsloc":22},{"name":"x/fungible/types/message_deploy_fungible_coin_zrc20.go","nsloc":66},{"name":"x/fungible/types/message_deploy_system_contracts.go","nsloc":37},{"name":"x/fungible/types/message_pause_zrc20.go","nsloc":47},{"name":"x/fungible/types/message_remove_foreign_coin.go","nsloc":38},{"name":"x/fungible/types/message_unpause_zrc20.go","nsloc":47},{"name":"x/fungible/types/message_update_contract_bytecode.go","nsloc":49},{"name":"x/fungible/types/message_update_gateway_contract.go","nsloc":46},{"name":"x/fungible/types/message_update_system_contract.go","nsloc":46},{"name":"x/fungible/types/message_update_zrc20_liquidity_cap.go","nsloc":44},{"name":"x/fungible/types/message_update_zrc20_name.go","nsloc":47},{"name":"x/fungible/types/message_update_zrc20_withdraw_fee.go","nsloc":53},{"name":"x/fungible/types/query.pb.gw.go","nsloc":607},{"name":"x/fungible/types/zrc20.go","nsloc":30},{"name":"x/ibccrosschain/client/cli/query.go","nsloc":18},{"name":"x/ibccrosschain/client/cli/tx.go","nsloc":18},{"name":"x/ibccrosschain/genesis.go","nsloc":11},{"name":"x/ibccrosschain/ibc_module.go","nsloc":135},{"name":"x/ibccrosschain/keeper/grpc_query.go","nsloc":5},{"name":"x/ibccrosschain/keeper/keeper.go","nsloc":49},{"name":"x/ibccrosschain/keeper/msg_server.go","nsloc":9},{"name":"x/ibccrosschain/module.go","nsloc":107},{"name":"x/ibccrosschain/types/client_ctx.go","nsloc":11},{"name":"x/ibccrosschain/types/codec.go","nsloc":17},{"name":"x/ibccrosschain/types/errors.go","nsloc":1},{"name":"x/ibccrosschain/types/expected_keepers.go","nsloc":5},{"name":"x/ibccrosschain/types/genesis.go","nsloc":7},{"name":"x/ibccrosschain/types/keys.go","nsloc":8},{"name":"x/lightclient/client/cli/query_block_header.go","nsloc":60},{"name":"x/lightclient/client/cli/query_chain_state.go","nsloc":64},{"name":"x/lightclient/client/cli/query.go","nsloc":24},{"name":"x/lightclient/client/cli/query_verification_flags.go","nsloc":29},{"name":"x/lightclient/client/cli/tx_disable_verification_flags.go","nsloc":45},{"name":"x/lightclient/client/cli/tx_enable_verification_flags.go","nsloc":45},{"name":"x/lightclient/client/cli/tx.go","nsloc":21},{"name":"x/lightclient/genesis.go","nsloc":27},{"name":"x/lightclient/keeper/block_header.go","nsloc":111},{"name":"x/lightclient/keeper/block_header_verification.go","nsloc":38},{"name":"x/lightclient/keeper/chain_state.go","nsloc":36},{"name":"x/lightclient/keeper/grpc_query_block_header.go","nsloc":48},{"name":"x/lightclient/keeper/grpc_query_chain_state.go","nsloc":48},{"name":"x/lightclient/keeper/grpc_query.go","nsloc":5},{"name":"x/lightclient/keeper/grpc_query_header_enabled_chains.go","nsloc":40},{"name":"x/lightclient/keeper/grpc_query_prove.go","nsloc":63},{"name":"x/lightclient/keeper/keeper.go","nsloc":43},{"name":"x/lightclient/keeper/msg_server_disable_block_header_verification.go","nsloc":27},{"name":"x/lightclient/keeper/msg_server.go","nsloc":9},{"name":"x/lightclient/keeper/proof.go","nsloc":42},{"name":"x/lightclient/module.go","nsloc":107},{"name":"x/lightclient/types/block_header_verification.go","nsloc":77},{"name":"x/lightclient/types/codec.go","nsloc":24},{"name":"x/lightclient/types/errors.go","nsloc":15},{"name":"x/lightclient/types/expected_keepers.go","nsloc":9},{"name":"x/lightclient/types/genesis.go","nsloc":33},{"name":"x/lightclient/types/keys.go","nsloc":16},{"name":"x/lightclient/types/message_disable_verification_flags.go","nsloc":45},{"name":"x/lightclient/types/message_enable_verification_flags.go","nsloc":46},{"name":"x/lightclient/types/query.pb.gw.go","nsloc":514},{"name":"x/lightclient/types/verification_flags.go","nsloc":35},{"name":"x/observer/abci.go","nsloc":34},{"name":"x/observer/client/cli/query_ballot.go","nsloc":61},{"name":"x/observer/client/cli/query_blame.go","nsloc":90},{"name":"x/observer/client/cli/query_chain_nonce.go","nsloc":60},{"name":"x/observer/client/cli/query_chain_params.go","nsloc":58},{"name":"x/observer/client/cli/query_get_tss_address.go","nsloc":72},{"name":"x/observer/client/cli/query.go","nsloc":43},{"name":"x/observer/client/cli/query_keygen.go","nsloc":27},{"name":"x/observer/client/cli/query_node_account.go","nsloc":54},{"name":"x/observer/client/cli/query_observers.go","nsloc":29},{"name":"x/observer/client/cli/query_operational_flags.go","nsloc":27},{"name":"x/observer/client/cli/query_pending_nonces.go","nsloc":27},{"name":"x/observer/client/cli/query_permission_flags.go","nsloc":27},{"name":"x/observer/client/cli/query_show_observer_count.go","nsloc":29},{"name":"x/observer/client/cli/query_supported_chains.go","nsloc":27},{"name":"x/observer/client/cli/query_tss_fund_migrator.go","nsloc":53},{"name":"x/observer/client/cli/query_tss.go","nsloc":46},{"name":"x/observer/client/cli/tx_add_observer.go","nsloc":37},{"name":"x/observer/client/cli/tx_disable_cctx_flags.go","nsloc":38},{"name":"x/observer/client/cli/tx_disable_fast_confirmation.go","nsloc":35},{"name":"x/observer/client/cli/tx_enable_cctx_flags.go","nsloc":38},{"name":"x/observer/client/cli/tx.go","nsloc":33},{"name":"x/observer/client/cli/tx_remove_chain_params.go","nsloc":36},{"name":"x/observer/client/cli/tx_reset_chain_nonces.go","nsloc":46},{"name":"x/observer/client/cli/tx_update_chain_params.go","nsloc":46},{"name":"x/observer/client/cli/tx_update_gas_price_increase_flags.go","nsloc":57},{"name":"x/observer/client/cli/tx_update_keygen.go","nsloc":33},{"name":"x/observer/client/cli/tx_update_observer.go","nsloc":52},{"name":"x/observer/client/cli/tx_update_operational_flags.go","nsloc":56},{"name":"x/observer/client/cli/tx_vote_blame.go","nsloc":78},{"name":"x/observer/client/cli/tx_vote_tss.go","nsloc":40},{"name":"x/observer/client/cli/utils.go","nsloc":15},{"name":"x/observer/genesis.go","nsloc":170},{"name":"x/observer/keeper/ballot.go","nsloc":103},{"name":"x/observer/keeper/blame.go","nsloc":66},{"name":"x/observer/keeper/chain_nonces.go","nsloc":36},{"name":"x/observer/keeper/chain_params.go","nsloc":64},{"name":"x/observer/keeper/crosschain_flags.go","nsloc":46},{"name":"x/observer/keeper/events.go","nsloc":56},{"name":"x/observer/keeper/grpc_query_ballot.go","nsloc":80},{"name":"x/observer/keeper/grpc_query_blame.go","nsloc":57},{"name":"x/observer/keeper/grpc_query_chain_params.go","nsloc":40},{"name":"x/observer/keeper/grpc_query_crosschain_flags.go","nsloc":22},{"name":"x/observer/keeper/grpc_query.go","nsloc":5},{"name":"x/observer/keeper/grpc_query_keygen.go","nsloc":16},{"name":"x/observer/keeper/grpc_query_node_account.go","nsloc":48},{"name":"x/observer/keeper/grpc_query_nonces.go","nsloc":86},{"name":"x/observer/keeper/grpc_query_observer.go","nsloc":40},{"name":"x/observer/keeper/grpc_query_operational_flags.go","nsloc":21},{"name":"x/observer/keeper/grpc_query_supported_chain.go","nsloc":14},{"name":"x/observer/keeper/grpc_query_tss_funds_migrator_info.go","nsloc":40},{"name":"x/observer/keeper/grpc_query_tss.go","nsloc":95},{"name":"x/observer/keeper/hooks.go","nsloc":136},{"name":"x/observer/keeper/keeper.go","nsloc":81},{"name":"x/observer/keeper/keygen.go","nsloc":24},{"name":"x/observer/keeper/last_observer_count.go","nsloc":20},{"name":"x/observer/keeper/migrator.go","nsloc":46},{"name":"x/observer/keeper/msg_server_add_observer.go","nsloc":53},{"name":"x/observer/keeper/msg_server_disable_cctx_flags.go","nsloc":39},{"name":"x/observer/keeper/msg_server_disable_fast_confirmation.go","nsloc":43},{"name":"x/observer/keeper/msg_server_enable_cctx_flags.go","nsloc":39},{"name":"x/observer/keeper/msg_server.go","nsloc":11},{"name":"x/observer/keeper/msg_server_remove_chain_params.go","nsloc":36},{"name":"x/observer/keeper/msg_server_reset_chain_nonces.go","nsloc":36},{"name":"x/observer/keeper/msg_server_update_chain_params.go","nsloc":32},{"name":"x/observer/keeper/msg_server_update_gas_price_increase_flags.go","nsloc":38},{"name":"x/observer/keeper/msg_server_update_keygen.go","nsloc":36},{"name":"x/observer/keeper/msg_server_update_observer.go","nsloc":94},{"name":"x/observer/keeper/msg_server_update_operational_chain_params.go","nsloc":37},{"name":"x/observer/keeper/msg_server_update_operational_flags.go","nsloc":20},{"name":"x/observer/keeper/msg_server_vote_blame.go","nsloc":44},{"name":"x/observer/keeper/msg_server_vote_block_header.go","nsloc":52},{"name":"x/observer/keeper/msg_server_vote_tss.go","nsloc":104},{"name":"x/observer/keeper/node_account.go","nsloc":36},{"name":"x/observer/keeper/nonce_to_cctx.go","nsloc":44},{"name":"x/observer/keeper/observer_set.go","nsloc":85},{"name":"x/observer/keeper/operational_flags.go","nsloc":22},{"name":"x/observer/keeper/pending_nonces.go","nsloc":84},{"name":"x/observer/keeper/tss_funds_migrator.go","nsloc":41},{"name":"x/observer/keeper/tss.go","nsloc":95},{"name":"x/observer/keeper/vote_inbound.go","nsloc":61},{"name":"x/observer/keeper/vote_outbound.go","nsloc":37},{"name":"x/observer/keeper/voting.go","nsloc":153},{"name":"x/observer/migrations/v10/migrate.go","nsloc":31},{"name":"x/observer/module.go","nsloc":140},{"name":"x/observer/module_simulation.go","nsloc":25},{"name":"zetaclient/authz/authz_signer.go","nsloc":32},{"name":"zetaclient/chains/base/confirmation.go","nsloc":75},{"name":"zetaclient/chains/base/logger.go","nsloc":69},{"name":"zetaclient/chains/base/observer.go","nsloc":308},{"name":"zetaclient/chains/base/signer.go","nsloc":108},{"name":"zetaclient/chains/bitcoin/bitcoin.go","nsloc":162},{"name":"zetaclient/chains/bitcoin/client/client.go","nsloc":198},{"name":"zetaclient/chains/bitcoin/client/commands.go","nsloc":329},{"name":"zetaclient/chains/bitcoin/client/helpers.go","nsloc":161},{"name":"zetaclient/chains/bitcoin/client/mockgen.go","nsloc":69},{"name":"zetaclient/chains/bitcoin/common/fee.go","nsloc":210},{"name":"zetaclient/chains/bitcoin/common/tx_script.go","nsloc":237},{"name":"zetaclient/chains/bitcoin/common/utils.go","nsloc":27},{"name":"zetaclient/chains/bitcoin/observer/db.go","nsloc":54},{"name":"zetaclient/chains/bitcoin/observer/event.go","nsloc":185},{"name":"zetaclient/chains/bitcoin/observer/gas_price.go","nsloc":42},{"name":"zetaclient/chains/bitcoin/observer/inbound.go","nsloc":182},{"name":"zetaclient/chains/bitcoin/observer/inbound_tracker.go","nsloc":77},{"name":"zetaclient/chains/bitcoin/observer/observer.go","nsloc":169},{"name":"zetaclient/chains/bitcoin/observer/outbound.go","nsloc":398},{"name":"zetaclient/chains/bitcoin/observer/utxos.go","nsloc":145},{"name":"zetaclient/chains/bitcoin/observer/witness.go","nsloc":155},{"name":"zetaclient/chains/bitcoin/signer/outbound_data.go","nsloc":84},{"name":"zetaclient/chains/bitcoin/signer/signer.go","nsloc":139},{"name":"zetaclient/chains/bitcoin/signer/sign.go","nsloc":179},{"name":"zetaclient/chains/evm/client/clientcustom.go","nsloc":195},{"name":"zetaclient/chains/evm/client/client.go","nsloc":72},{"name":"zetaclient/chains/evm/common/cctx.go","nsloc":57},{"name":"zetaclient/chains/evm/common/constant.go","nsloc":21},{"name":"zetaclient/chains/evm/common/validation.go","nsloc":70},{"name":"zetaclient/chains/evm/evm.go","nsloc":205},{"name":"zetaclient/chains/evm/observer/inbound.go","nsloc":598},{"name":"zetaclient/chains/evm/observer/observer_gas.go","nsloc":70},{"name":"zetaclient/chains/evm/observer/observer.go","nsloc":184},{"name":"zetaclient/chains/evm/observer/outbound.go","nsloc":409},{"name":"zetaclient/chains/evm/observer/v2_inbound.go","nsloc":371},{"name":"zetaclient/chains/evm/observer/v2_inbound_tracker.go","nsloc":79},{"name":"zetaclient/chains/evm/observer/v2_outbound.go","nsloc":254},{"name":"zetaclient/chains/evm/signer/gas.go","nsloc":91},{"name":"zetaclient/chains/evm/signer/outbound_data.go","nsloc":160},{"name":"zetaclient/chains/evm/signer/outbound_tracker_reporter.go","nsloc":60},{"name":"zetaclient/chains/evm/signer/signer_admin.go","nsloc":148},{"name":"zetaclient/chains/evm/signer/signer.go","nsloc":412},{"name":"zetaclient/chains/evm/signer/sign.go","nsloc":176},{"name":"zetaclient/chains/evm/signer/v2_signer.go","nsloc":30},{"name":"zetaclient/chains/evm/signer/v2_sign.go","nsloc":181},{"name":"zetaclient/chains/interfaces/interfaces.go","nsloc":152},{"name":"zetaclient/chains/solana/observer/inbound.go","nsloc":222},{"name":"zetaclient/chains/solana/observer/inbound_tracker.go","nsloc":30},{"name":"zetaclient/chains/solana/observer/observer_gas.go","nsloc":36},{"name":"zetaclient/chains/solana/observer/observer.go","nsloc":63},{"name":"zetaclient/chains/solana/observer/outbound.go","nsloc":245},{"name":"zetaclient/chains/solana/rpc/rpc.go","nsloc":124},{"name":"zetaclient/chains/solana/signer/execute.go","nsloc":70},{"name":"zetaclient/chains/solana/signer/execute_spl.go","nsloc":106},{"name":"zetaclient/chains/solana/signer/increment_nonce.go","nsloc":54},{"name":"zetaclient/chains/solana/signer/outbound_tracker_reporter.go","nsloc":63},{"name":"zetaclient/chains/solana/signer/signer.go","nsloc":523},{"name":"zetaclient/chains/solana/signer/whitelist.go","nsloc":51},{"name":"zetaclient/chains/solana/signer/withdraw.go","nsloc":58},{"name":"zetaclient/chains/solana/signer/withdraw_spl.go","nsloc":83},{"name":"zetaclient/chains/solana/solana.go","nsloc":174},{"name":"zetaclient/chains/sui/client/client.go","nsloc":137},{"name":"zetaclient/chains/sui/observer/inbound.go","nsloc":154},{"name":"zetaclient/chains/sui/observer/observer.go","nsloc":105},{"name":"zetaclient/chains/sui/observer/outbound.go","nsloc":223},{"name":"zetaclient/chains/sui/signer/signer.go","nsloc":93},{"name":"zetaclient/chains/sui/signer/signer_tracker.go","nsloc":50},{"name":"zetaclient/chains/sui/signer/signer_tx.go","nsloc":66},{"name":"zetaclient/chains/sui/signer/signer_withdrawcap.go","nsloc":49},{"name":"zetaclient/chains/sui/sui.go","nsloc":158},{"name":"zetaclient/chains/ton/config/config.go","nsloc":83},{"name":"zetaclient/chains/ton/liteapi/client.go","nsloc":175},{"name":"zetaclient/chains/ton/observer/inbound.go","nsloc":262},{"name":"zetaclient/chains/ton/observer/observer.go","nsloc":91},{"name":"zetaclient/chains/ton/observer/outbound.go","nsloc":203},{"name":"zetaclient/chains/ton/signer/signer.go","nsloc":199},{"name":"zetaclient/chains/ton/signer/signer_tracker.go","nsloc":65},{"name":"zetaclient/chains/ton/ton.go","nsloc":150},{"name":"zetaclient/common/constant.go","nsloc":8},{"name":"zetaclient/common/env.go","nsloc":18},{"name":"zetaclient/compliance/compliance.go","nsloc":45},{"name":"zetaclient/config/config_chain.go","nsloc":58},{"name":"zetaclient/config/config.go","nsloc":173},{"name":"zetaclient/config/types.go","nsloc":153},{"name":"zetaclient/context/app.go","nsloc":180},{"name":"zetaclient/context/chain.go","nsloc":146},{"name":"zetaclient/context/context.go","nsloc":24},{"name":"zetaclient/db/db.go","nsloc":79},{"name":"zetaclient/keys/interfaces/interfaces.go","nsloc":14},{"name":"zetaclient/keys/keys.go","nsloc":160},{"name":"zetaclient/keys/relayer_key.go","nsloc":107},{"name":"zetaclient/logs/fields.go","nsloc":21},{"name":"zetaclient/maintenance/shutdown_listener.go","nsloc":138},{"name":"zetaclient/maintenance/tss_listener.go","nsloc":129},{"name":"zetaclient/metrics/burn_rate.go","nsloc":77},{"name":"zetaclient/metrics/metrics.go","nsloc":219},{"name":"zetaclient/metrics/telemetry.go","nsloc":269},{"name":"zetaclient/orchestrator/contextupdater.go","nsloc":89},{"name":"zetaclient/orchestrator/v2_bootstrap.go","nsloc":239},{"name":"zetaclient/orchestrator/v2_orchestrator.go","nsloc":262},{"name":"zetaclient/ratelimiter/rate_limiter.go","nsloc":87},{"name":"zetaclient/testutils/constant.go","nsloc":42},{"name":"zetaclient/testutils/evm.go","nsloc":30},{"name":"zetaclient/testutils/mempool_client.go","nsloc":137},{"name":"zetaclient/tss/config.go","nsloc":84},{"name":"zetaclient/tss/crypto.go","nsloc":183},{"name":"zetaclient/tss/healthcheck.go","nsloc":102},{"name":"zetaclient/tss/keygen.go","nsloc":244},{"name":"zetaclient/tss/readme.md","nsloc":null},{"name":"zetaclient/tss/service.go","nsloc":251},{"name":"zetaclient/tss/setup.go","nsloc":250},{"name":"zetaclient/types/dynamic_ticker.go","nsloc":36},{"name":"zetaclient/types/ethish.go","nsloc":7},{"name":"zetaclient/types/event.go","nsloc":61},{"name":"zetaclient/types/sql_btc.go","nsloc":95},{"name":"zetaclient/types/sql_evm.go","nsloc":117},{"name":"zetaclient/types/sql.go","nsloc":28},{"name":"zetaclient/types/telemetry_types.go","nsloc":4},{"name":"zetaclient/zetacore/broadcast.go","nsloc":148},{"name":"zetaclient/zetacore/client_cosmos.go","nsloc":23},{"name":"zetaclient/zetacore/client_crosschain.go","nsloc":49},{"name":"zetaclient/zetacore/client_fungible.go","nsloc":32},{"name":"zetaclient/zetacore/client.go","nsloc":232},{"name":"zetaclient/zetacore/client_monitor.go","nsloc":135},{"name":"zetaclient/zetacore/client_operationalflags.go","nsloc":9},{"name":"zetaclient/zetacore/client_start.go","nsloc":94},{"name":"zetaclient/zetacore/client_subscriptions.go","nsloc":60},{"name":"zetaclient/zetacore/client_vote.go","nsloc":152},{"name":"zetaclient/zetacore/constant.go","nsloc":20},{"name":"zetaclient/zetacore/tx.go","nsloc":97}]},{"repo":"zeta-chain/protocol-contracts","branch_name":"main","commit_hash":"1d79e0ff60291a3f9fb0338e6ac7415b60f73ab8","total_nsloc":917,"files":[{"name":"contracts/evm/ERC20Custody.sol","nsloc":98},{"name":"contracts/evm/GatewayEVM.sol","nsloc":197},{"name":"contracts/evm/ZetaConnectorBase.sol","nsloc":66},{"name":"contracts/evm/ZetaConnectorNative.sol","nsloc":27},{"name":"contracts/evm/ZetaConnectorNonNative.sol","nsloc":37},{"name":"contracts/zevm/GatewayZEVM.sol","nsloc":173},{"name":"contracts/zevm/SystemContract.sol","nsloc":89},{"name":"contracts/zevm/WZETA.sol","nsloc":47},{"name":"contracts/zevm/ZRC20.sol","nsloc":183}]},{"repo":"zeta-chain/protocol-contracts-solana","branch_name":"main","commit_hash":"55b1c78199b1f770718cf86284d4b61a58627697","total_nsloc":0,"files":[{"name":"programs/gateway/src/contexts.rs","nsloc":null},{"name":"programs/gateway/src/errors.rs","nsloc":null},{"name":"programs/gateway/src/instructions/admin.rs","nsloc":null},{"name":"programs/gateway/src/instructions/deposit.rs","nsloc":null},{"name":"programs/gateway/src/instructions/execute.rs","nsloc":null},{"name":"programs/gateway/src/instructions/mod.rs","nsloc":null},{"name":"programs/gateway/src/instructions/withdraw.rs","nsloc":null},{"name":"programs/gateway/src/lib.rs","nsloc":null},{"name":"programs/gateway/src/state.rs","nsloc":null},{"name":"programs/gateway/src/utils/constants.rs","nsloc":null},{"name":"programs/gateway/src/utils/mod.rs","nsloc":null},{"name":"programs/gateway/src/utils/prepare_account_metas.rs","nsloc":null},{"name":"programs/gateway/src/utils/recover_and_verify_eth_address.rs","nsloc":null},{"name":"programs/gateway/src/utils/validate_message_hash.rs","nsloc":null},{"name":"programs/gateway/src/utils/validate_message.rs","nsloc":null},{"name":"programs/gateway/src/utils/verify_and_update_nonce.rs","nsloc":null},{"name":"programs/gateway/src/utils/verify_ata_match.rs","nsloc":null},{"name":"programs/gateway/src/utils/verify_authority.rs","nsloc":null},{"name":"programs/gateway/src/utils/verify_payload_size.rs","nsloc":null}]},{"repo":"zeta-chain/protocol-contracts-sui","branch_name":"main","commit_hash":"b1bd4edde62ce4598eaa4d53edb2f17a01742997","total_nsloc":0,"files":[{"name":"sources/gateway.move","nsloc":null}]},{"repo":"zeta-chain/protocol-contracts-ton","branch_name":"main","commit_hash":"5def0091aaee6e9f05629e1e74d90b1ac24fa0c9","total_nsloc":0,"files":[{"name":"contracts/common/crypto.fc","nsloc":null},{"name":"contracts/common/errors.fc","nsloc":null},{"name":"contracts/common/gas.fc","nsloc":null},{"name":"contracts/common/messages.fc","nsloc":null},{"name":"contracts/common/state.fc","nsloc":null},{"name":"contracts/gateway.fc","nsloc":null},{"name":"contracts/imports/stdlib.fc","nsloc":null}]}],"short_description":"ZetaChain is a layer one EVM compatible chain with a native interopability solution built in. Allowing developers to build Universal Dapps that interact with all chains","is_best_efforts":true,"uses_live_issues":false,"template_repo_name":"sherlock-audit/2025-04-zetachain-cross-chain","is_judging_visible":false,"calc_completed":true,"private":false,"reserve_auditor_message":null,"score_sequence":252,"requires_kyc":true,"logo_url":"https://sherlock-files.ams3.digitaloceanspaces.com/contests/ZetaChain.jpg","nsloc":92355,"languages":[],"escalation_started_at":1748276987,"token":"USDC","status":"FINISHED","is_judging_enabled":false,"bug_bounty_contest":false,"description":null,"judging_docs_hash":"2668590b5fd3c5aae6b3120a0e1e7b79524bd673","first_blood_pool":3500,"starts_at":1744642800,"allows_signups":false,"update_nsloc":null,"is_legacy_contest":false}
