EIP-2535 diamonds turn a fallback function into a selector router
▲ 1 r/solidity+1 crossposts

EIP-2535 diamonds turn a fallback function into a selector router

Most proxy designs assume one implementation contract. That gets awkward once a protocol grows beyond the 24 KB bytecode limit or needs to upgrade one module without replacing the rest.

An EIP-2535 diamond keeps one stateful address and maps each four-byte function selector to a facet contract. The fallback reads msg.sig, finds the facet, and runs it with delegatecall. msg.sender and msg.value stay intact, while every storage read and write still lands in the diamond.

The routing is straightforward. Storage is where the risk moves.

Facets do not own isolated state. If two facets assume incompatible layouts, an otherwise valid upgrade can corrupt the same slots. I use namespaced storage libraries and test the selector-to-facet map before and after every diamondCut.

diamondCut also lets you add, replace, or remove selectors and run initialization in one transaction. Loupe functions then give tooling a way to verify which facet owns each selector.

I put together a Foundry walkthrough that deploys the diamond and facets, adds a new selector, and checks the routing:

https://andreyobruchkov1996.substack.com/p/diamonds-in-evm-the-proxy-that-scales-beyond-limits-2fedc282cadf

For teams that have used diamonds in production, what caused more trouble: storage coordination, selector governance, or the larger audit surface?

u/Resident_Anteater_35 — 15 hours ago

Zero-copy in Anchor: the layout and borrow pitfalls that matter in practice

I followed up on my Borsh account-layout post by testing the zero-copy path in Anchor.

The main benefit is not that serialization disappears everywhere. It is that AccountLoader lets the program borrow a fixed-layout view of the account buffer instead of reconstructing the whole state through ordinary Borsh deserialization.

The parts that required the most care were:

  1. #[repr(C)] preserves field order, but it does not remove alignment padding.

  2. bytemuck’s Pod contract rules out pointers and variable-size fields. That means no String, Vec, or other heap-backed values inside the mapped structure.

  3. Padding should be explicit. A u8 followed by a u64 needs seven bytes before the u64 if the structure uses eight-byte alignment.

  4. bool is awkward for Pod because not every bit pattern is a valid Rust boolean. A u8 flag avoids that issue.

  5. load() and load_mut() return runtime-checked borrows. Holding a RefMut longer than necessary can make a later borrow fail inside the same instruction.

  6. Accounts larger than 10,240 bytes need a separate creation and initialization flow because Anchor’s normal init path creates the account through CPI.

I now treat the byte layout as a versioned storage format. I test size_of, alignment, offsets, and raw bytes rather than assuming the Rust declaration tells the whole story.

The complete write-up and code are here:

https://andreyobruchkov1996.substack.com/p/architecting-high-performance-solana

For large production accounts, are you keeping the full state zero-copy, or separating a small Borsh control account from a larger fixed-layout data account?

u/Resident_Anteater_35 — 12 days ago

Zero-copy state removes a deserialization cost but creates a layout contract

Serialization is often treated as plumbing, but it becomes an architectural decision when on-chain state grows large and is accessed frequently.

In Solana programs built with Anchor, regular accounts are commonly decoded through Borsh. That is convenient for variable-length fields and ordinary application state. For hot, fixed-size state such as order books or large registries, repeatedly decoding the full account can become unnecessary work.

Zero-copy changes the tradeoff. AccountLoader borrows the account buffer and maps it to a fixed Rust structure instead of rebuilding the structure through ordinary deserialization.

The performance benefit comes with a stricter layout contract:

  • #[repr(C)] preserves field order, but alignment padding still exists
  • Pod types cannot contain pointers, String, Vec, or other variable-size fields
  • implicit padding can violate bytemuck’s safety requirements, so padding should be explicit
  • flags are often stored as u8 rather than bool because every u8 bit pattern is valid
  • load_mut() returns a runtime-checked mutable borrow, so long-lived RefMut values can cause borrow failures
  • schema evolution becomes a migration problem because the byte offsets are part of the persisted format

I would not use zero-copy for every account. A practical split is Borsh for small control state and fixed-layout accounts for large data that is read or updated on performance-critical paths.

Where do you draw that boundary in production systems?

Disclosure: this Reddit post was drafted with AI assistance. I reviewed the technical claims and take responsibility for the final text.

reddit.com
u/Resident_Anteater_35 — 12 days ago

How Anchor account data looks after Borsh serialization, byte by byte

I wanted a concrete way to reason about Anchor account layouts without treating serialization as magic.

For a small account like:

#[account]
pub struct UserProfile {
    pub bump: u8,
    pub score: u64,
}

the stored data begins with Anchor’s 8-byte account discriminator, followed by fields in declaration order. u8 takes one byte and u64 takes eight bytes in little-endian order, so this account uses 17 bytes before any additional fields.

Dynamic fields change the calculation:

  • String and Vec add a 4-byte little-endian length prefix plus their payload
  • Option adds a 1-byte tag plus the inner value when it is Some
  • nested collections require more allocation and copying during deserialization

This matters when calculating account space, diagnosing deserialization failures, matching TypeScript client layouts, and investigating compute or heap spikes on larger accounts.

I wrote up the full byte-level walkthrough, including discriminator derivation and raw buffer decoding:

https://andreyobruchkov1996.substack.com/p/solana-deep-dive-unpacking-borsh

How are you inspecting layouts in practice? Anchor IDL, direct buffer parsing, or custom zero-copy layouts for larger state?

u/Resident_Anteater_35 — 29 days ago
▲ 17 r/BlockchainStartups+2 crossposts

Token-2022 utility extensions are more useful than they look

Most Token-2022 discussions focus on Transfer Hooks and Confidential Transfers. I spent some time testing four of the smaller extensions on Devnet, and they cover a surprising amount of application-level logic.

The four extensions are:

  • Permanent Delegate
  • Non-Transferable Tokens
  • Default Account State
  • Required Transfer Memos

Permanent Delegate gives one authority transfer and burn rights across every account associated with a mint. That is useful for regulated assets, subscriptions, and recovery workflows, but it also creates an authority that users need to understand before accepting the token.

Non-Transferable makes the restriction part of the mint itself. There is no wrapper program or freeze workaround. Transfers fail at the token program level, while the holder can still burn the asset.

The interesting part is combining it with Permanent Delegate. You can model prepaid API or compute credits as fungible tokens that users can hold but cannot resell. The protocol backend burns credits as the service is consumed.

Default Account State covers a different problem. New token accounts can begin frozen and remain unusable until an authority explicitly thaws them. That gives regulated protocols a native account-level approval flow.

Required Transfer Memos is applied by the receiving token account rather than the mint. It forces incoming transfers to include transaction context, which is useful for exchange deposits and treasury accounting.

The trade-off is clear: Token-2022 removes a lot of custom wrapper code, but it moves more policy and authority into the token configuration itself. Wallets and explorers need to make those permissions very visible.

I documented the Devnet tests, SPL CLI commands, and Solscan transactions here:

https://andreyobruchkov1996.substack.com/p/the-utility-extensions-completing

Which of these extensions would you trust in a production asset, and what warnings should wallets show before a user receives one?

u/Resident_Anteater_35 — 12 days ago
▲ 8 r/ethereum+1 crossposts

The RPC bottleneck of ethgetLogs: EVM event architecture and topic filtering

EVM events don't live in state; they sit in the transaction receipt logs. When you fire an ethgetLogs RPC call, you are leveraging the node's bloom filters to query these receipts without touching the state trie.

The architectural constraint here is the topic limit. An event can have up to 4 topics: topics0 is the keccak256 signature hash (e.g., keccak256("Transfer(address,address,uint256)")), leaving only 3 slots for indexed parameters. These are fixed at 32 bytes. Node providers can rapidly filter these topics because they function as native search keys.

Everything else is packed into the unindexed data blob as raw bytes. The trade-off:
keeping fields unindexed saves EVM gas by avoiding topic structuring, but pushes the computational load to your off-chain infra, which now has to pull the raw logs and ABI-decode the hex blobs manually. When you construct an RPC call searching for a specific block range and target address, minimizing the reliance on unindexed data decoding is crucial for high-throughput indexers.

Source/Full Breakdown: https://andreyobruchkov1996.substack.com/p/understanding-events-the-evms-built

For those building high-frequency indexers, at what scale of log ingestion do you abandon standard?

u/Resident_Anteater_35 — 3 months ago