EVM chain architecture, and Tron

Cake LabsSenior Mobile Engineer2023 to 2026251 files, net fewer lines

Polygon, Base and Arbitrum one at a time, then a 251-file refactor that deleted more than it added. BNB Smart Chain landed in 103 files after it. Tron, built from scratch, is the non-EVM sibling.

Context

Cake Wallet has a rule I like: the app never imports a chain package. A build-time generator writes an interface per chain, a CI check enforces the boundary, and a build without a chain simply has no implementation behind it. It is good for the app and expensive per chain.

When I added Polygon in December 2023 it was the second EVM chain and it cost a whole new package, a hand-written proxy of a few hundred lines, a block in the generator and an entry in the CI guard. Most of the package was Ethereum’s with the names changed. A reviewer asked me to note the duplicated helpers for a refactor. Six weeks later I did that refactor, and two years later I did it again properly.

I wrote every one of these nine merged changes: about 24,800 lines added and 10,900 removed, across 1,006 file touches, plus the two guides in the repo that explain how to do it next time.

Problem

Each chain is mostly the same and slightly different. Base needs a re-estimation pass on send-all. Arbitrum takes no priority tip. BNB Smart Chain’s nodes dislike EIP-1559 encoding. Every difference needs somewhere to live, and with one class per chain the differences leaked into shared code as branches on chain id.

And every chain needs the same twenty things around it: a wallet type, nodes, icons, URL schemes on two platforms, build flags, buy providers, view-model switches and 30 translation files.

Constraints

The import boundary stays. Whatever the chain layer becomes, the app still cannot see it directly.

Wallets already in people’s hands. Wallet files, storage boxes, history files and node preferences were all keyed per chain by wallet type. All of them had to keep reading.

Cake also ships a Monero-only build from the same code, so nothing shared can depend on an EVM type.

Decisions and trade-offs

Abstract base with per-chain overrides (January 2024). The first refactor created a shared package and shrank the two chain wallets from about 500 lines each to about 70. Review pushed hard towards “make it abstract, let each chain override”, and that is what shipped: seven members each chain must provide. It was right for two chains. By four it had produced branches on chain id in shared code, a switch over chain ids for a stablecoin’s address, and a fee model that could not say “this chain has no tip”.

No subclasses at all (January 2026). The second refactor inverted it. The chain wallet became concrete, the overridable members became lookups against a config row per chain, and the only thing still subclassed is the client, at about 46 lines a chain, for the two things that differ: how the transaction is built when a chain’s nodes dislike EIP-1559, and how a signed transaction is prepared. Four packages and four proxies were deleted, one proxy of 516 lines took their place, and the change was net negative: 251 files, 7,306 lines added, 7,761 removed. The price is that a chain quirk now has to be expressible as config or as a small client override. Anything else has nowhere to live, which is the point.

Keep the old wallet types rather than migrate them. The clean version is one EVM wallet type and a data migration for every existing wallet. I did not do that. Stored wallets keep their type; a saved chain id in the wallet file overrides it when present and falls back to the type’s chain when absent, so an untouched wallet behaves as before with no migration step. The old token box migrates lazily the first time a wallet opens. The cost is visible in the repo today: the enum still lists five EVM chains and my own guide says new chains should not add another. I argued in review that compatibility checks should key on chain id rather than wallet type so an old wallet and a new one on the same chain take the same path, and that is how they work.

The whole refactor in one change, not in stages. Base and Arbitrum, two months earlier, show what incremental looks like, and the review thread had been logging deferred items against the refactor for weeks. Doing it at once is what let the change be net negative and what let a per-wallet network switch ship with it, since switching a wallet between chains is not expressible while each chain is its own class. The price was a diff too large for GitHub to render, 52 days in review, 29 review events, and two items the approval still missed. I would take it again.

Track the bridge after the send (April 2026). A cross-chain transfer is not done when the source transaction confirms. Rather than treat the send as the end, a scanning service polls delivery and a stored history keeps the transfer visible while it completes, for USDT across Ethereum, Polygon and Arbitrum. A polling service for one token on three chains, in exchange for a transfer that shows progress instead of vanishing for minutes.

Tron gets its own package (May 2024). Tron is not EVM at the RPC level, so it could not reuse any of this. I built it from scratch in 20 files with its own ABI and HTTP provider, and wrote a 300-line guide on adding a wallet type in the same change. Review asked me to check the derived key against other Tron wallets for the same seed, which is the kind of question you want on a key path.

What was hard

The same bug class once per chain until it had somewhere to live. Fees rendered in the token’s symbol instead of the chain’s currency, caught in review on Polygon. The same confusion in the first refactor. An empty fee currency default on Base. A fee model that assumed a tip until Arbitrum. The native symbol only stopped being a special case when it became a field on the chain config.

Token identity across chains. USDC exists on all five chains with the same name and symbol. An equality check compared titles and ignored the chain tag, so a lookup for USDC on Arbitrum could return USDC on Base. One line, reported by a tester, not findable with fewer than three chains carrying the same tokens.

The app layer did not collapse with the chain layer. BNB Smart Chain is the measurement: no package, no proxy, no generator block, 193 lines across nine files in the chain layer. And still 103 files overall, because the wallet type, nodes, icons, schemes, flags and translations did not shrink. The review opened by saying adding an EVM chain had become simple, then listed the two things still missing. Both are true.

Result

Cake Wallet holds Ethereum, Polygon, Base, Arbitrum and BNB Smart Chain, each with its own nodes, default tokens, explorer and history, and an EVM wallet can be switched between networks in place instead of creating a new wallet. Fees are quoted in the chain’s own currency, estimated separately for native and token transfers, and computed without a tip on the chain that does not take one. USDT can move between Ethereum, Polygon and Arbitrum inside the wallet with delivery tracked. Tron wallets create, restore, send, receive, sync, and buy, sell and exchange.

The honest comparison for “faster to add a chain” is two public numbers: Base needed a 16-file package plus a 238-line proxy; BNB Smart Chain needed neither. I have no time measurement and will not invent one.

A sixth chain is open as I write this, not shipped.

All case studies