Deposit Contract
This chapter presents a complete, real-world contract: a Fe reimplementation of the
Ethereum 2.0 deposit contract
(mainnet 0x00000000219ab540356cBB839Cbe05303d7705Fa).
It is a shape-for-shape port of the Solidity reference. It demonstrates patterns that go beyond a
typical token: dynamic bytes message inputs with exact length validation, an incremental Merkle
tree built on the std::evm::ssz Merkleization primitives, SSZ serialization, the RawMem effect,
ERC-165 interface detection, and the #[payable] handler attribute.
Full Source Code
Section titled “Full Source Code”// Fe reimplementation of the ETH 2.0 Deposit Contract.//// Dynamic `bytes` inputs arrive as `std::abi::Bytes`, and the runtime validates// their lengths (48, 32, 96) exactly like Solidity's `require` checks. The// branchless Merkle-tree algorithm and SHA-256 construction match the original// contract bit-for-bit, built on top of the `std::evm::ssz` Merkleization// primitives.
use std::evm::{ ether, gwei, ssz, wei, Ctx, Evm, Log, Merkleize, RawMem,}use std::abi::{ Bytes, Bytes4, Bytes32, bytes_from_word, bytes_from_words, bytes_from_words_prefix, sol,}use core::num::{IntDowncast, IntWord}
const DEPOSIT_CONTRACT_TREE_DEPTH: usize = 32// NOTE: this also ensures `deposit_count` will fit into 64-bitsconst MAX_DEPOSIT_COUNT: u256 = 2 ** (DEPOSIT_CONTRACT_TREE_DEPTH as u256) - 1
msg DepositMsg { #[selector = sol("deposit(bytes,bytes,bytes,bytes32)")] Deposit { pubkey: Bytes, withdrawal_credentials: Bytes, signature: Bytes, deposit_data_root: Bytes32, },
#[selector = sol("supportsInterface(bytes4)")] SupportsInterface { interface_id: Bytes4 } -> bool,
#[selector = sol("get_deposit_root()")] GetDepositRoot -> u256,
#[selector = sol("get_deposit_count()")] GetDepositCount -> Bytes,}
#[event]struct DepositEvent { pubkey: Bytes, withdrawal_credentials: Bytes, amount: Bytes, signature: Bytes, index: Bytes,}
struct DepositStore { branch: [u256; DEPOSIT_CONTRACT_TREE_DEPTH], deposit_count: u256, zero_hashes: [u256; DEPOSIT_CONTRACT_TREE_DEPTH],}
pub contract DepositContract uses (ctx: Ctx, mem: mut RawMem, log: mut Log) { mut store: DepositStore,
// Precompute zero_hashes for empty subtrees. The Solidity original leaves // zero_hashes[0] = 0 (implicit default) and writes levels 1..DEPOSIT_CONTRACT_TREE_DEPTH. // We have to match that exact layout — otherwise every root is shifted // one level. init() uses (mut store, mut mem) { for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH - 1 { let cur: u256 = store.zero_hashes[height] store.zero_hashes[height + 1] = ssz::hash_pair(left: cur, right: cur) } }
recv DepositMsg { #[payable] Deposit { pubkey, withdrawal_credentials, signature, deposit_data_root } uses (mut store, ctx, mut mem, mut log) { assert!(pubkey.len == 48, "DepositContract: invalid pubkey length") assert!(withdrawal_credentials.len == 32, "DepositContract: invalid withdrawal_credentials length") assert!(signature.len == 96, "DepositContract: invalid signature length")
let value: u256 = ctx.value() assert!(value >= ether(1), "DepositContract: deposit value too low") assert!(value % gwei(1) == 0, "DepositContract: deposit value not multiple of gwei") let deposit_amount: u256 = value / gwei(1) assert!(deposit_amount <= u64::MASK, "DepositContract: deposit value too high") // Truncating cast matches Solidity's `uint64(x)`. The assert above // guarantees no information is lost. let amount_gwei: u64 = deposit_amount.downcast_truncate()
let mut node: u256 = compute_deposit_data_root( pubkey, withdrawal_credentials, signature, amount_gwei, ) assert!( node == deposit_data_root.val, "DepositContract: reconstructed DepositData does not match supplied deposit_data_root", )
// Emit DepositEvent (index is the pre-increment deposit_count). log.emit( DepositEvent { pubkey, withdrawal_credentials, amount: ssz::serialize_u64(amount_gwei), signature, index: ssz::serialize_u64(store.deposit_count.downcast_truncate()), }, )
assert!(store.deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full") store.deposit_count += 1
let mut size: u256 = store.deposit_count for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH { if size & 1 == 1 { store.branch[height] = node return } node = ssz::hash_pair(left: store.branch[height], right: node) size = size / 2 } // Unreachable: the MAX_DEPOSIT_COUNT check above guarantees we exit // via the `size & 1 == 1` branch within DEPOSIT_CONTRACT_TREE_DEPTH iterations. assert!(false) }
GetDepositRoot -> u256 uses (store, mut mem) { let mut node: u256 = 0 let mut size: u256 = store.deposit_count for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH { if size & 1 == 1 { node = ssz::hash_pair(left: store.branch[height], right: node) } else { node = ssz::hash_pair(left: node, right: store.zero_hashes[height]) } size = size / 2 } // Finalize with SSZ mix-in-length over `deposit_count`. // MAX_DEPOSIT_COUNT = 2**32 - 1 means the truncating cast never // loses information. ssz::mix_in_length(root: node, len: store.deposit_count.downcast_truncate()) }
GetDepositCount -> Bytes uses (store, mut mem) { let count: u64 = store.deposit_count.downcast_truncate() ssz::serialize_u64(count) }
SupportsInterface { interface_id } -> bool { interface_id.val == 0x01ffc9a7 || interface_id.val == 0x85640907 } }}
// Reconstruct the deposit-data SSZ root from its constituent parts. Exposed at// module level so tests can precompute a valid `deposit_data_root` to pass in.#[inline(always)]pub fn compute_deposit_data_root( pubkey: Bytes, withdrawal_credentials: Bytes, signature: Bytes, amount_gwei: u64,) -> u256uses (mem: mut RawMem){ assert!(withdrawal_credentials.len == 32) ( ssz::hash_tree_root<ssz::ByteVector<48>>(pubkey), withdrawal_credentials.word_at(0), ssz::u64_chunk(amount_gwei), ssz::hash_tree_root<ssz::ByteVector<96>>(signature), ) .merkleize()}Walkthrough
Section titled “Walkthrough”Message Interface
Section titled “Message Interface”The contract’s public ABI is declared with a msg block. Each variant carries an explicit
Solidity selector, so the on-chain ABI matches the canonical deposit contract exactly. Dynamic
arguments are typed as std::abi::Bytes:
msg DepositMsg { #[selector = sol("deposit(bytes,bytes,bytes,bytes32)")] Deposit { pubkey: Bytes, withdrawal_credentials: Bytes, signature: Bytes, deposit_data_root: Bytes32, },
#[selector = sol("supportsInterface(bytes4)")] SupportsInterface { interface_id: Bytes4 } -> bool, // ...}Storage and the Merkle Branch
Section titled “Storage and the Merkle Branch”State lives in a DepositStore storage struct holding the incremental Merkle branch, the
deposit_count, and a precomputed table of zero_hashes for empty subtrees. The init block fills
zero_hashes level by level so the tree layout matches the Solidity original bit-for-bit:
struct DepositStore { branch: [u256; DEPOSIT_CONTRACT_TREE_DEPTH], deposit_count: u256, zero_hashes: [u256; DEPOSIT_CONTRACT_TREE_DEPTH],}
init() uses (mut store, mut mem) { for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH - 1 { let cur: u256 = store.zero_hashes[height] store.zero_hashes[height + 1] = ssz::hash_pair(left: cur, right: cur) }}The deposit Handler
Section titled “The deposit Handler”The Deposit handler is marked #[payable] because it receives ETH. It mirrors the Solidity
require checks using the assert! macro — including the message string that becomes the revert
reason — validates the deposit amount in gwei, reconstructs the SSZ deposit-data root and checks it
against the caller-supplied value, emits the DepositEvent, and finally appends the new leaf to the
incremental Merkle tree:
#[payable]Deposit { pubkey, withdrawal_credentials, signature, deposit_data_root }uses (mut store, ctx, mut mem, mut log){ assert!(pubkey.len == 48, "DepositContract: invalid pubkey length") // ... let value: u256 = ctx.value() assert!(value >= ether(1), "DepositContract: deposit value too low") assert!(value % gwei(1) == 0, "DepositContract: deposit value not multiple of gwei") // ... reconstruct root, emit event, update branch ...}Reading State
Section titled “Reading State”GetDepositRoot walks the branch combining it with the zero-hash table, then mixes in the deposit
count via ssz::mix_in_length. GetDepositCount returns the count as a little-endian SSZ-serialized
Bytes. Both are read-only handlers (uses (store, mut mem)):
GetDepositRoot -> u256 uses (store, mut mem) { let mut node: u256 = 0 let mut size: u256 = store.deposit_count for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH { if size & 1 == 1 { node = ssz::hash_pair(left: store.branch[height], right: node) } else { node = ssz::hash_pair(left: node, right: store.zero_hashes[height]) } size = size / 2 } ssz::mix_in_length(root: node, len: store.deposit_count.downcast_truncate())}ERC-165 Support
Section titled “ERC-165 Support”SupportsInterface implements ERC-165 interface detection, returning true for the ERC-165
selector itself (0x01ffc9a7) and the deposit-contract interface id (0x85640907).
The example ships with a property-style test suite that runs under fe test. It exercises ERC-165
detection, rejects deposits with too little value, non-gwei amounts, and mismatched data roots
(using #[test(should_revert)]), and verifies that successive deposits advance both the count and
the Merkle root. Test handlers drive the contract through the Evm effect:
#[test(balance = 4_000_000_000_000_000_000)]fn test_deposit_success_updates_root_and_count() uses (evm: mut Evm, mem: mut RawMem) { let c = evm.create2<DepositContract>(value: 0, args: (), salt: 0) // ... make a valid deposit, then assert the count and root changed ... assert!(root1 != root0)}Key Patterns
Section titled “Key Patterns”| Pattern | Where |
|---|---|
Dynamic bytes inputs | pubkey, withdrawal_credentials, signature typed as Bytes |
| Exact length validation | assert!(pubkey.len == 48, "...") |
| Revert reasons | assert!(cond, "message") macro |
| Payable handler | #[payable] on Deposit |
| Memory effect | uses (mem: mut RawMem) |
| SSZ / Merkleization | std::evm::ssz, .merkleize(), ssz::mix_in_length |
| Value helpers | ether(1), gwei(1), wei(1) |
| Narrowing casts | .downcast_truncate() with a guarding assert! |
| ERC-165 | SupportsInterface handler |