Workspace IndexDev Notes › Simulation belongs before the wallet prompt

#34PoC

Simulation belongs before the wallet prompt

A wallet prompt tells the user what they are being asked to sign; simulation tells the application what that signed transaction is expected to do.

Wrap three writes with viem simulateContract — one success, one custom-error revert, one that passes simulation but fails after another transaction changes state — and decode the first two before ever requesting a signature.

Why

Most failed writes are knowable before gas or user attention is spent. Simulation cannot guarantee future state, but it turns avoidable failures into application errors rather than wallet surprises.

Simulation removes failures already implied by current state and exposes return data or revert reasons without spending gas. It does not reserve that state, guarantee ordering, or eliminate front-running. The useful product pattern is therefore simulate → explain → sign → monitor, not simulate → promise.

How it works

Wrap three writes with viem simulateContract: one success, one custom-error revert, and one state-dependent failure. Compare the predicted outcome with the receipt and surface decoded failure before requesting a signature.

PoC

Wrap three writes: a success, a custom-error revert, and a transaction that succeeds in simulation but fails after another transaction changes state. Decode the first two before requesting a signature and use the third to document the boundary of the guarantee.

const { request, result } = await publicClient.simulateContract(args)
// show decoded effect and warnings
const hash = await walletClient.writeContract(request)

Reference: viem simulateContract.

Review clarification

The message in one sentence, and what caused this card

Never ask a user to sign a transaction you could have known would fail or surprise them — check first, explain in plain words, sign, then keep watching, because the check is not a guarantee. The card was not born from theory; three expensive histories sit behind it:

  • Blind signing kept burning people, up to $1.5B. Years of wallets showing unreadable hex built the wallet-drainer economy — thousands signing setApprovalForAll or permits to phishing sites — and it climaxed with the Bybit hack (2025-02, ~$1.5B): professional multisig signers approved a tampered UI's malicious delegatecall. Every loss had the same shape: what the transaction would do was knowable before signing, and nobody's software said it out loud. That is the explain half.
  • Users literally pay for predictable failures. A revert still costs gas. The canonical case is the Otherside mint (2022): well over $150M burned in a gas war, a chunk of it on transactions that failed — money paid for nothing that a simulateContract call would have caught. That is the simulate half.
  • The market voted, but patched the wrong layer. Simulation became a product category — Tenderly, Blockaid, Pocket Universe, Rabby's built-in preview, MetaMask + Blockaid. But a wallet can only show a generic balance diff; only the application knows the domain meaning ("you will receive ~132 USDC, or this reverts because your allowance is 50 short").

The third write's formal name: TOCTOU

Time-of-check to time-of-use. Simulation is a check against latest; the signed transaction executes against future state, and the gap is the mempool. The moral is ancient and settled: a check is not a reservation — which is the card's "simulate → explain → sign → monitor, not simulate → promise" in one word. A worthwhile fourth write someday: block-environment drift — a contract branching on block.timestamp or basefee can pass simulation and legitimately fail with no adversary and no state change by anyone, a different edge of the guarantee than write #3.

Embedded wallets remove the second surface

With an external wallet, the prompt is an independent second surface where a bad transaction might still get caught. With an embedded wallet (embedded-wallet-policy) that surface does not exist — the app owns the entire consent moment, so simulate-and-explain stops being polish and becomes the only place informed consent can happen. This is what promotes the card from UX nicety to the seed of a shared service (Jayverse #6, Wallet & Simulation-before-sign).

You can only decode errors you know

viem decodes custom errors from the ABI it was given. A revert bubbling up from a nested third-party contract arrives as a raw selector — undecodable without that ABI. A service version needs an error-selector registry (own ABIs + a 4byte-style directory + an honest "unknown reason" fallback). "Decoded failure" quietly ranges from "insufficient allowance, need 50 more" to "something reverted" — show one of each.

The pipeline, and the KPI

This card and receipt-is-not-settlement are one product: the monitor leg of simulate → explain → sign → monitor is exactly that card's detected → included → safe → finalized (+ reorged) machine. Before the signature this card removes knowable failures; after it, the tier machine handles the unknowable ones. The production KPI falls out naturally: the false-promise rate — the share of transactions that passed simulation but failed on-chain, per contract and per market condition. That number is the honesty meter of the explain step, and the SLO the service should publish about itself.

← All Dev Notes · Workspace Index · Top ↑

시뮬레이션은 지갑 프롬프트 앞에 놓인다

지갑 프롬프트는 사용자에게 무엇을 서명하라고 요청하는지 알려 주고, 시뮬레이션은 애플리케이션에게 그 서명된 트랜잭션이 무엇을 할 것으로 예상되는지 알려 줍니다.

viem simulateContract 로 쓰기 셋을 감쌉니다 — 성공 하나, 커스텀 에러 revert 하나, 시뮬레이션은 통과하지만 다른 트랜잭션이 상태를 바꾼 뒤 실패하는 것 하나. 앞의 둘은 서명을 요청하기 전에 디코드합니다.

실패하는 쓰기의 대부분은 가스나 사용자의 주의를 쓰기 전에 알 수 있습니다. 시뮬레이션이 미래 상태를 보장하지는 못하지만, 피할 수 있는 실패를 지갑에서의 놀람이 아니라 애플리케이션 에러로 바꿔 줍니다.

시뮬레이션은 현재 상태가 이미 암시하는 실패를 제거하고, 가스를 쓰지 않고 반환 데이터나 revert 사유를 드러냅니다. 그 상태를 예약해 주지도, 순서를 보장하지도, 프런트러닝을 없애 주지도 않습니다. 그래서 유용한 제품 패턴은 simulate → promise 가 아니라 simulate → explain → sign → monitor 입니다.

동작 방식

viem simulateContract 로 쓰기 셋을 감쌉니다: 성공, 커스텀 에러 revert, 상태 의존 실패. 예측 결과를 receipt 와 비교하고, 서명을 요청하기 전에 디코드된 실패를 보여 줍니다.

PoC

쓰기 셋을 감쌉니다: 성공, 커스텀 에러 revert, 그리고 시뮬레이션은 통과하지만 다른 트랜잭션이 상태를 바꾼 뒤 실패하는 트랜잭션. 앞의 둘은 서명 요청 전에 디코드하고, 셋째는 보장의 경계를 문서화하는 데 씁니다.

const { request, result } = await publicClient.simulateContract(args)
// show decoded effect and warnings
const hash = await walletClient.writeContract(request)

참고: viem simulateContract.

검토 후 보완

한 문장의 메시지, 그리고 이 카드를 낳은 것

실패하거나 사용자를 놀라게 할 것을 미리 알 수 있었던 트랜잭션에 서명을 요구하지 말라 — 먼저 검사하고, 쉬운 말로 설명하고, 서명하고, 그 뒤에도 지켜보라. 검사는 보장이 아니니까. 이 카드는 이론에서 태어나지 않았습니다; 값비싼 실화 셋이 뒤에 있습니다:

  • 블라인드 서명은 계속 사람들을 태웠고, $1.5B 까지 갔습니다. 읽을 수 없는 16진수를 보여주던 지갑의 세월이 지갑 드레이너 경제를 만들었고 — 수천 명이 피싱 사이트에 setApprovalForAll 과 permit 을 서명 — Bybit 해킹(2025-02, 약 $1.5B)에서 정점을 찍었습니다: 전문 멀티시그 서명자들이 조작된 UI 의 악성 delegatecall 을 승인했습니다. 모든 손실의 모양이 같습니다: 트랜잭션이 무엇을 할지는 서명 전에 알 수 있었는데, 누구의 소프트웨어도 소리 내어 말하지 않았다. 이게 explain 절반입니다.
  • 사용자는 예측 가능한 실패에 실돈을 냅니다. revert 도 가스를 씁니다. 정전급 사례는 Otherside 민팅(2022): 가스 전쟁에서 $1.5억 이상이 탔고, 그중 상당액이 실패한 트랜잭션 — simulateContract 한 번이면 잡혔을 실패에 낸 돈입니다. 이게 simulate 절반입니다.
  • 시장은 투표했지만, 엉뚱한 층에 패치했습니다. 시뮬레이션은 제품 카테고리가 됐습니다 — Tenderly, Blockaid, Pocket Universe, Rabby 내장 미리보기, MetaMask + Blockaid. 하지만 지갑은 일반적인 잔액 diff 만 보여줄 수 있고, 도메인의 의미("~132 USDC 를 받게 됩니다, 아니면 허용량 50 부족으로 revert")는 애플리케이션만 압니다.

셋째 쓰기의 정식 이름: TOCTOU

Time-of-check to time-of-use. 시뮬레이션은 latest 에 대한 검사이고, 서명된 트랜잭션은 미래 상태에서 실행되며, 그 간극이 멤풀입니다. 교훈은 오래됐고 정해져 있습니다: 검사는 예약이 아니다 — 카드의 "simulate → explain → sign → monitor, promise 아님"을 한 단어로 줄인 것. 언젠가 추가할 만한 넷째 쓰기: 블록 환경 표류 — block.timestamp 나 basefee 로 분기하는 컨트랙트는 적도, 누구의 상태 변경도 없이 시뮬을 통과하고 정당하게 실패할 수 있습니다. 셋째 쓰기와는 다른, 보장의 또 다른 모서리입니다.

임베디드 지갑은 두 번째 표면을 없앤다

외부 지갑에서는 프롬프트가 나쁜 트랜잭션을 잡아낼 수도 있는 독립된 두 번째 표면입니다. 임베디드 지갑(embedded-wallet-policy)에서는 그 표면이 존재하지 않습니다 — 앱이 동의의 순간 전체를 소유하므로, 시뮬레이션+설명은 광택이 아니라 정보에 기반한 동의가 일어날 수 있는 유일한 자리가 됩니다. 이것이 이 카드를 UX 의 장식에서 공유 서비스(Jayverse #6, Wallet & Simulation-before-sign)의 씨앗으로 격상시킵니다.

아는 에러만 디코드된다

viem 은 건네받은 ABI 의 커스텀 에러만 풉니다. 중첩된 서드파티 컨트랙트에서 올라온 revert 는 날 셀렉터로 도착하고, 그 ABI 없이는 못 풉니다. 서비스 버전에는 에러 셀렉터 레지스트리(자체 ABI + 4byte 류 디렉터리 + 정직한 "원인 불명" 폴백)가 필요합니다. "디코드된 실패"는 조용히 "허용량 50 부족"부터 "무언가 revert 됨"까지 폭이 있습니다 — 각각 하나씩 보여줄 것.

파이프라인, 그리고 KPI

이 카드와 receipt-is-not-settlement 는 한 제품입니다: simulate → explain → sign → monitor 의 monitor 구간이 정확히 그 카드의 detected → included → safe → finalized (+ reorged) 머신입니다. 서명 전에는 이 카드가 알 수 있는 실패를 제거하고, 서명 후에는 계층 머신이 알 수 없는 실패를 처리합니다. 프로덕션 KPI 도 자연히 떨어집니다: 거짓 약속률(false-promise rate) — 시뮬은 통과했는데 온체인에서 실패한 트랜잭션의 비율(컨트랙트별·시장 상황별). 이 숫자가 explain 단계의 정직성 계기판이고, 서비스가 스스로에 대해 공표해야 할 SLO 입니다.

← 전체 개발 노트 · 워크스페이스 인덱스 · 맨 위 ↑