시뮬레이션은 지갑 프롬프트 앞에 놓인다
지갑 프롬프트는 사용자에게 무엇을 서명하라고 요청하는지 알려 주고, 시뮬레이션은 애플리케이션에게 그 서명된 트랜잭션이 무엇을 할 것으로 예상되는지 알려 줍니다.
viem `simulateContract` 로 쓰기 셋을 감쌉니다 — 성공 하나, 커스텀 에러 revert 하나, 시뮬레이션은 통과하지만 다른 트랜잭션이 상태를 바꾼 뒤 실패하는 것 하나. 앞의 둘은 서명을 요청하기 전에 디코드합니다.
기술 노트
각 항목이 실제로 무엇을 보여주고 어떻게 동작하는지 — 위 카드보다 자세한 기술 설명입니다.
목적: 실패하는 쓰기의 대부분은 가스나 사용자의 주의를 쓰기 전에 알 수 있습니다. 시뮬레이션이 미래 상태를 보장하지는 못하지만, 피할 수 있는 실패를 지갑에서의 놀람이 아니라 애플리케이션 에러로 바꿔 줍니다. 시뮬레이션은 현재 상태가 이미 암시하는 실패를 제거하고, 가스를 쓰지 않고 반환 데이터나 revert 사유를 드러냅니다. 그 상태를 예약해 주지도, 순서를 보장하지도, 프런트러닝을 없애 주지도 않습니다. 그래서 유용한 제품 패턴은 simulate → promise 가 아니라 simulate → explain → sign → monitor 입니다.
동작 방식: viem `simulateContract` 로 쓰기 셋을 감쌉니다: 성공, 커스텀 에러 revert, 상태 의존 실패. 예측 결과를 receipt 와 비교하고, 서명을 요청하기 전에 디코드된 실패를 보여 줍니다. ### PoC 쓰기 셋을 감쌉니다: 성공, 커스텀 에러 revert, 그리고 시뮬레이션은 통과하지만 다른 트랜잭션이 상태를 바꾼 뒤 실패하는 트랜잭션. 앞의 둘은 서명 요청 전에 디코드하고, 셋째는 보장의 경계를 문서화하는 데 씁니다. ```ts const { request, result } = await publicClient.simulateContract(args) // show decoded effect and warnings const hash = await walletClient.writeContract(request) ``` 참고: [viem simulateContract](https://viem.sh/docs/contract/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 입니다.