{"slug": "i-built-the-same-escrow-on-two-chains-the-architectures-couldn-t-be-more", "title": "I Built the Same Escrow on Two Chains. The Architectures Couldn't Be More Different.", "summary": "A developer who maintains a non-custodial escrow protocol on both Base and TON found that the two blockchains' fundamentally different execution models forced a complete rethink of the contract design. On Base, contracts are objects with shared state and synchronous calls, requiring reentrancy guards and pull-payment patterns, while on TON, contracts are actors with message passing, eliminating the need for such defenses. The developer highlights that the same feature on different chains can lead to divergent architectures, challenging the notion of a simple port.", "body_md": "I maintain a non-custodial escrow protocol that runs on two blockchains: Base, an Ethereum L2, and TON, the chain behind Telegram. Same product, same core logic on paper: lock funds, deliver work, release on approval, handle disputes.\n\nI expected the second implementation to be a port. It wasn't. The two chains disagree so fundamentally about how a contract should be structured that writing the same feature twice forced me to rethink what \"the same\" even means. This post is about those differences, the design decisions each model pushed me toward, and what I'd tell anyone about to make the same jump.\n\nLight on code, heavy on the reasoning. The interesting part was never the syntax.\n\nThe single most important difference is not the language. It's the execution model underneath.\n\n**On Base**, a contract is an object with shared state. You write Solidity, and it behaves like a class instance sitting in memory that everyone calls into. A user calls a function, the function reads and writes contract storage synchronously, and either the whole thing succeeds or it reverts atomically. All my escrows live in one contract, in one big mapping, and every call reaches straight into that shared state.\n\n**On TON**, a contract is an actor that receives messages. You write Tact, and it behaves like an isolated process with a mailbox. You don't call a function; you send a message, and the contract handles it in its own turn. There is no synchronous cross-contract call in the EVM sense. Interaction between contracts is asynchronous message passing, and you design around that or you fight it the whole way.\n\nThe shape of the entry point tells the whole story. On Base, an external caller invokes a named function:\n\n```\nfunction approveWork(uint256 _contractId)\n    external validContract(_contractId) nonReentrant whenNotPaused\n{\n    // reads and writes shared contract storage, synchronously\n}\n```\n\nOn TON, nothing \"calls\" the contract. A message arrives, and a handler consumes it in the actor's own turn:\n\n``` js\nreceive(msg: ClaimTimeout) {\n    let escrow: EscrowData = self.requireEscrow(msg.escrowId);\n    require(context().sender == escrow.provider, \"Only provider can claim timeout\");\n    // ... handled to completion, no synchronous caller waiting on the stack\n}\n```\n\nEverything downstream flows from this one distinction.\n\nOn Base, the first thing I reached for was ReentrancyGuard. Every state-changing function that moves money carries the guard:\n\n```\ncontract NovaCont is ReentrancyGuard, Ownable2Step, Pausable {\n    // every fund-moving function is marked nonReentrant:\n    //   ... external payable whenNotPaused nonReentrant\n    //   ... external validContract(id) nonReentrant whenNotPaused\n}\n```\n\nThis is not optional hygiene in the EVM world; the synchronous call model is exactly what makes reentrancy possible. When your contract calls out to transfer funds, control can re-enter before your state settles. The entire discipline of \"checks-effects-interactions,\" pull-payment patterns, and reentrancy guards exists to tame that one property.\n\nOn TON, I went looking for the equivalent guard and slowly realised I didn't need one. The actor model doesn't have the synchronous re-entry surface in the same shape. A message is handled to completion in its turn; an outbound transfer is itself just another message queued for later, not a synchronous callout that hands control to attacker code mid-execution. The reentrancy attack that dominates EVM security thinking simply doesn't map onto the model the same way.\n\nThat was the first moment the \"port\" mentality broke. A whole category of defensive code on one chain was answering a question the other chain doesn't ask.\n\nThis difference is subtle and it shaped both designs.\n\nOn Base I use a pull-payment pattern. When someone is owed money, I don't push it to them inside the function. I credit a balance they withdraw later, in a separate transaction:\n\n```\n// settlement doesn't send; it credits a balance\npendingWithdrawals[token][recipient] += amount;\n// the recipient later calls withdraw() themselves\n```\n\nThis is deliberate. Pushing funds to an arbitrary address inside your logic hands control to that address, which reintroduces exactly the reentrancy and gas-griefing risks you're trying to avoid. Pull-payment isolates the value transfer from the state change. It's more steps for the user, but it's the safe EVM idiom, and withdrawal even stays available while the contract is paused, because a pause should stop new agreements, not trap funds people already earned.\n\n**On TON**, sending is just emitting a message, so the calculus changes. A payout is dispatched inline as part of settling, and because that send is itself a queued message rather than a synchronous callout, it doesn't hand control to the recipient's code mid-execution:\n\n```\nsend(SendParameters{\n    to: escrow.provider,\n    value: providerNet,\n    mode: SendPayGasSeparately | SendBounceIfActionFail\n});\n```\n\nThe pressure that makes push-payment dangerous on the EVM is largely absent. The design leans into sending directly, with the gas budget for those outbound messages accounted for explicitly at creation time rather than assumed.\n\nSame goal, \"get funds to the right party safely,\" and the safe path is shaped differently on each chain because the danger is shaped differently.\n\nHere's where the models pull hardest in opposite directions.\n\n**Base rewards centralisation**. Because state is shared and calls are synchronous, putting every escrow in one contract with one mapping is natural and cheap to reason about. One deployment, one address, one place for the dispute logic to reach into. The cost, and it is a real cost I carry, is that all state accumulates in a single contract, and per-transaction cost and storage rent grow with it. But the model makes the centralised design the path of least resistance.\n\n**TON rewards the opposite**. The idiomatic TON design is a contract per item: a factory that spawns a child contract for each escrow, each an actor with its own storage and its own lifecycle. This is not a stylistic preference; it's how the chain wants to scale, because isolated actors parallelise where shared state serialises. I did not start there, and the single-contract approach is the sharpest architectural debt in the TON build precisely because it runs against the grain of the platform.\n\nThe lesson: the \"obvious\" structure on one chain is the anti-pattern on the other. Centralised is comfortable on Base and a scaling ceiling on TON.\n\nOne decision did survive the crossing, and it's worth calling out because it's chain-independent.\n\nOn Base, dispute resolution lives in a separate contract, an independent arbitration system that the escrow contract talks to across a tightly controlled interface, rather than being welded into the escrow logic itself. The reasoning is isolation: a bug or an exploit in the dispute machinery shouldn't be able to reach into and compromise the custody of funds in escrow. Two contracts, two blast radii.\n\nThat principle, keep the thing that holds the money simpler than the thing that makes judgments about it, held up on both chains, even though the two express \"another contract\" so differently (a synchronous interface on one, message passing on the other). When a design principle survives a paradigm shift intact, that's usually a sign it was a real principle and not just a local convention.\n\n**Don't port. Re-derive.** The temptation is to translate your Solidity into Tact line by line. Resist it. Start from the execution model and ask what the right structure is on the target chain. Half of what you wrote on the first chain is answering questions the second chain doesn't have.\n\n**Let the danger model define the safe pattern.** Reentrancy guards, pull-payments, and reentrancy-driven ordering are EVM answers to EVM threats. On a different model, some of that is dead weight, and a different set of concerns (message ordering, gas accounting for outbound messages, storage rent) takes its place. Learn the new threat model before you copy the old defenses.\n\n**Keep custody boundaries simple, everywhere**. The one thing worth holding constant across chains is that the code holding funds should be as small and as boring as possible, with the complex logic pushed to the side. That travels.\n\nTwo chains, two paradigms, one product. The parts I thought would transfer (the defensive patterns) mostly didn't, and the part I almost undervalued (a clean separation between custody and judgment) turned out to be the only thing that was truly portable.\n\n*I build NovaCont, a non-custodial escrow protocol on Base and TON. Contract addresses and security notes are public. [Docs]*([https://novacont.gitbook.io/nova-docs](https://novacont.gitbook.io/nova-docs)).", "url": "https://wpnews.pro/news/i-built-the-same-escrow-on-two-chains-the-architectures-couldn-t-be-more", "canonical_source": "https://dev.to/novacont/i-built-the-same-escrow-on-two-chains-the-architectures-couldnt-be-more-different-2b9b", "published_at": "2026-08-02 06:17:32+00:00", "updated_at": "2026-08-02 06:40:55.045608+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Base", "TON", "Telegram", "Solidity", "Tact", "EVM", "NovaCont"], "alternates": {"html": "https://wpnews.pro/news/i-built-the-same-escrow-on-two-chains-the-architectures-couldn-t-be-more", "markdown": "https://wpnews.pro/news/i-built-the-same-escrow-on-two-chains-the-architectures-couldn-t-be-more.md", "text": "https://wpnews.pro/news/i-built-the-same-escrow-on-two-chains-the-architectures-couldn-t-be-more.txt", "jsonld": "https://wpnews.pro/news/i-built-the-same-escrow-on-two-chains-the-architectures-couldn-t-be-more.jsonld"}}