Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides11 min read

Deposit Contract: How Ethereum Staking Deposits Are Verified On-Chain

A technical guide to the Ethereum deposit contract, how deposit data is validated, how to verify a deposit on-chain, and how to diagnose common deposit failures.

TL;DR

The deposit contract is the on-chain entry point through which ETH and validator deposit data enter Ethereum's proof-of-stake system. It does not merely accept ETH; it validates the structure of each deposit, computes a Merkle root over the deposit data, and emits an event that beacon-chain clients consume. This article explains the deposit data format, the validation rules enforced by the contract, and a reproducible procedure for verifying a deposit using standard JSON-RPC calls. It also covers common failure modes, the limits of what the contract can and cannot check, and how to choose reliable RPC access for verification workflows.

Scope and the two meanings of deposit contract

In general legal and financial usage, a deposit contract is a private agreement in which one party places funds with another as security, a reservation payment, or a guarantee of performance. That meaning appears in property reservations, rental security deposits, and bank deposit agreements. This article does not cover those instruments. It focuses on the technical meaning that dominates blockchain engineering: the Ethereum Beacon Deposit Contract, a smart contract deployed on Ethereum mainnet that accepts staking deposits and emits the data beacon nodes need to activate validators.

The distinction matters because the two concepts share a name but almost nothing else. A legal deposit contract is enforced by courts and specifies return or forfeiture conditions. The on-chain deposit contract is enforced by bytecode and specifies a fixed 48-byte public key, 32-byte withdrawal credentials, a 96-byte signature, and a 32-byte deposit data root. Understanding the on-chain version requires reading its validation logic rather than its legal terms.

  • Legal deposit contract: private agreement, jurisdiction-dependent, covers reservation or security funds.
  • Ethereum deposit contract: immutable smart contract, validates deposit structure, emits DepositEvent.
  • This guide covers only the on-chain contract and the verification workflow around it.

What the deposit contract actually validates

The deposit contract is deliberately minimal. For each deposit it receives, it checks that the ETH amount is a whole multiple of 1 ETH, that the supplied deposit data root matches the hash tree root computed from the public key, withdrawal credentials, and signature, and that the amount encoded in the deposit data matches the value sent. If any check fails, the transaction reverts. If all checks pass, the contract appends the deposit to an internal Merkle tree, updates the deposit count, and emits a DepositEvent containing the public key, withdrawal credentials, amount, signature, and index.

Crucially, the contract does not verify the BLS signature, does not confirm that the validator is eligible to activate, and does not check whether the withdrawal credentials point to a legitimate address or a valid BLS withdrawal key. Those checks happen later, off-chain, in beacon-chain client software. This separation is intentional: signature verification is expensive and belongs in the consensus layer, while the execution-layer contract only guarantees data integrity and event emission.

  • Enforced on-chain: 1 ETH granularity, deposit data root match, amount consistency.
  • Not enforced on-chain: BLS signature validity, validator activation eligibility, withdrawal credential ownership.
  • Emitted per deposit: public key, withdrawal credentials, amount, signature, Merkle index.

Deposit data format and the Merkle root

A deposit is defined by four fields: pubkey (48 bytes), withdrawal_credentials (32 bytes), signature (96 bytes), and amount (8 bytes, little-endian, in Gwei). The deposit data root is the SHA-256 hash tree root of these fields, computed with the same SSZ container rules used throughout the consensus layer. The contract recomputes this root from the calldata and compares it to the deposit_data_root argument; a mismatch reverts the transaction.

The contract also maintains a Merkle tree of all deposits. Each new deposit is inserted at the next index, and the contract stores the branch needed to reconstruct the root. Beacon nodes read the DepositEvent logs and the deposit count to reconstruct the full deposit list. This design means that a deposit is only useful to the consensus layer if both the event and the Merkle state are consistent, which is why verification should check the event log and the deposit count together rather than the transaction receipt alone.

Prerequisites for a verification workflow

To follow the procedure below you need an Ethereum JSON-RPC endpoint that supports eth_getLogs, eth_call, and eth_getTransactionReceipt. Public endpoints often rate-limit log queries, so for repeated verification use a managed endpoint from the multi-chain RPC endpoints guide or provision a dedicated node if you need predictable throughput. You also need the deposit transaction hash and the deposit contract address for the network you are inspecting.

The deposit contract address is network-specific. Mainnet and testnets use different deployments, so always confirm the address against the network's official configuration before querying. Do not hardcode an address from a tutorial without checking it against the chain you are actually using. The examples below use RPC_URL and DEPOSIT_CONTRACT_ADDRESS as placeholders; substitute values you have verified independently.

  • An RPC endpoint with log and call support.
  • The deposit transaction hash.
  • The verified deposit contract address for the target network.
  • Optional: a local SSZ or hash-tree-root library for independent root recomputation.

Verifying a deposit with curl and JSON-RPC

The first check is the transaction receipt. A successful deposit transaction has status 0x1 and contains at least one log emitted by the deposit contract. The following request retrieves the receipt and confirms the contract emitted an event. Replace the placeholders with your values.

A reverted transaction returns status 0x0 and no deposit event. If the receipt is missing entirely, the transaction may still be pending or the RPC node may not have indexed it yet. In that case, retry against a different endpoint before concluding the deposit failed.

curl -s -X POST "$RPC_URL" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getTransactionReceipt",
    "params": ["0xYOUR_TX_HASH"]
  }'

Reading the DepositEvent log in JavaScript

The receipt alone does not prove the deposit data was well-formed; it only proves the transaction succeeded. To inspect the deposit payload, decode the DepositEvent log. The event signature is DepositEvent(bytes,bytes,bytes,bytes,bytes), and the five data fields are the public key, withdrawal credentials, amount, signature, and index. The example below uses ethers to fetch and decode the log. It prints the fields so you can compare them against the deposit data file produced by your staking tool.

Run this against the same transaction hash used above. If the decoded public key does not match the validator key you intended to deposit, stop and investigate before assuming the deposit is valid. A mismatch usually indicates the wrong deposit data file was submitted.

import { JsonRpcProvider, Interface } from "ethers";

const provider = new JsonRpcProvider(process.env.RPC_URL);
const depositContract = process.env.DEPOSIT_CONTRACT_ADDRESS;
const txHash = process.env.DEPOSIT_TX_HASH;

const iface = new Interface([
  "event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index)"
]);

const receipt = await provider.getTransactionReceipt(txHash);
if (!receipt || receipt.status !== 1) {
  throw new Error("Deposit transaction did not succeed");
}

for (const log of receipt.logs) {
  if (log.address.toLowerCase() !== depositContract.toLowerCase()) continue;
  const parsed = iface.parseLog(log);
  console.log({
    pubkey: parsed.args.pubkey,
    withdrawalCredentials: parsed.args.withdrawal_credentials,
    amountGwei: parsed.args.amount,
    signature: parsed.args.signature,
    index: parsed.args.index
  });
}

Cross-checking the deposit count and Merkle state

The deposit contract exposes a get_deposit_count() function that returns the number of deposits recorded so far, encoded as a little-endian byte array. Reading it confirms that the contract state advanced after your transaction. The following eth_call retrieves the raw return data; decode it as a little-endian integer to get the count.

Compare the count before and after the transaction if you need stronger evidence. A deposit that emitted an event but did not increment the count would indicate an RPC inconsistency or a reorg, not a valid deposit. For high-value operations, query two independent endpoints and compare results before treating the deposit as final.

curl -s -X POST "$RPC_URL" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_call",
    "params": [{
      "to": "'"$DEPOSIT_CONTRACT_ADDRESS"'",
      "data": "0x621fd130"
    }, "latest"]
  }'

Expected verification result

A correctly submitted deposit produces a receipt with status 0x1, exactly one DepositEvent from the deposit contract, a decoded public key matching your validator key, an amount that is a whole multiple of 1 ETH, and a deposit count that increased by one. The withdrawal credentials should match the format you intended: either a 0x00 prefix for BLS withdrawal credentials or a 0x01 or 0x02 prefix for execution-layer withdrawal addresses.

If all of these hold, the execution-layer portion of the deposit is complete. The validator will not necessarily activate immediately; activation depends on the beacon-chain deposit queue, the validator's position, and network conditions. Verification of the deposit contract event is a necessary but not sufficient condition for activation.

  • Receipt status is 0x1.
  • One DepositEvent from the expected contract address.
  • Decoded pubkey matches the intended validator key.
  • Amount is a whole multiple of 1 ETH and matches the deposit data.
  • Deposit count increased by one.

Troubleshooting common deposit failures

Most deposit failures fall into a small number of categories. A reverted transaction with a root mismatch usually means the deposit data root was computed incorrectly or the deposit data file was edited after the root was generated. A reverted transaction with an amount error means the ETH value sent was not a whole multiple of 1 ETH or did not match the amount encoded in the deposit data. A successful transaction with an unexpected public key usually means the wrong deposit data file was used.

A deposit that succeeds on-chain but never activates is a different class of problem. Common causes include an invalid BLS signature, withdrawal credentials that do not correspond to a key you control, or a validator that was already deposited under the same key. These issues are diagnosed at the consensus layer, not by reading the deposit contract. Use beacon-node tooling or a validator client's deposit verification commands to investigate.

  • Root mismatch: regenerate deposit data and recompute the root.
  • Amount mismatch: confirm 1 ETH granularity and encoded amount.
  • Wrong pubkey: verify the deposit data file against the validator key.
  • No activation: check BLS signature and withdrawal credentials off-chain.

Limitations and tradeoffs

The deposit contract's minimalism is a feature, but it shifts responsibility to the depositor. Because the contract does not verify signatures, a deposit with an invalid signature will still succeed on-chain and consume ETH, but the validator will never activate. There is no on-chain refund path for such mistakes. This is why staking tools generate and verify deposit data locally before broadcasting.

Verification via JSON-RPC also has limits. Log queries can be rate-limited, pruned, or served by nodes that are behind the chain head. A receipt from a lagging node may be missing or inconsistent. For production workflows, use endpoints with documented consistency guarantees and consider querying more than one provider. The RPC pricing page and API service overview describe options for managed access, while the networks directory helps confirm which networks are supported.

Next steps for deposit verification at scale

If you verify deposits occasionally, the curl and JavaScript examples above are sufficient. If you operate a staking service or monitor many validators, build a small verification service that fetches receipts, decodes events, and compares them against your internal deposit records. Store the transaction hash, deposit index, and decoded public key together so that later audits do not depend on re-querying historical logs.

For teams that need predictable RPC throughput during deposit windows, review the blockchain node hosting guide and consider dedicated infrastructure. For endpoint selection specifically, the RPC Assistant can recommend providers based on your network and latency requirements; this article deliberately avoids naming a single provider because the right choice depends on your verification volume and consistency needs.

  • Persist deposit index and decoded pubkey alongside the transaction hash.
  • Query two independent endpoints for high-value deposits.
  • Monitor deposit count changes as a consistency signal.
  • Use dedicated nodes when verification volume exceeds public rate limits.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started