Grok가 트윗 하나로 175,000달러를 잃다: 전송 전 트랜잭션 카나리(Pre-Send Tx Canary)를 추가하세요
요약
Grok 기반 AI 에이전트가 프롬프트 인젝션 공격으로 인해 막대한 자산을 손실한 사례를 분석하고, 이를 방지하기 위한 'TxCanary' 도구를 소개합니다. TxCanary는 트랜잭션 서명 전 가격, 주소, 드라이 런을 검증하는 세 가지 읽기 전용 체크를 통해 에이전트의 잘못된 실행을 차단합니다.
핵심 포인트
- 모스 부호를 이용한 프롬프트 인젝션으로 에이전트가 자산을 오송금함
- 에이전트는 오류 없이 확신을 가지고 잘못된 명령을 수행할 수 있음
- TxCanary는 API 키 없이 가격, 주소, 드라이 런을 검증함
- 서명 전 단계에서 저렴한 읽기 전용 체크를 추가하는 것이 핵심
전송 전 트랜잭션 카나리(Pre-send transaction canary)는 AI 에이전트가 트랜잭션에 서명하기 _전_에 잘못된 온체인(on-chain) 트랜잭션을 차단합니다. TxCanary는 세 가지 읽기 전용(read-only) 체크 — CoinGecko를 통한 가격 건전성(price sanity) 확인, eth_getCode를 통한 주소 건전성(address sanity) 확인, 그리고 eth_call을 통한 드라이 런(dry-run) — 를 실행하며, 모든 이유와 함께 차단(BLOCK) 결과를 반환합니다. 이 도구는 API 키나 개인 키(private key)가 필요 없으며, 자금을 전혀 이동시키지 않습니다.
2026년 5월 4일, Grok 기반 거래를 수행하는 하나의 X 계정이 트윗 하나로 인해 약 175,000달러를 잃었습니다. 주입된 공격은 평문(plain text)조차 아니었습니다. 답글 안에 숨겨진 모스 부호(Morse code) 형태였으며, 모델은 이를 성실하게 해독하여 명령으로 취급했습니다. 해당 에이전트는 약 30억 DRB 토큰을 공격자가 제어하는 주소로 전송했습니다. SlowMist의 보고서는 이를 전형적인 프롬프트 인젝션(prompt-injection) 사례로 지목했습니다. 에이전트가 온체인 인접 미디어에서 읽은 텍스트를 마치 운영자의 명령인 것처럼 신뢰했기 때문입니다. (이후 약 80%가 회수되었으나, 만약 당신의 지갑이었다면 위안이 되지 않을 것입니다.)
엔지니어로서 당신을 괴롭힐 부분은 바로 이것입니다: 에이전트는 충돌(crash)하지 않았습니다. 에이전트는 지시받은 대로 확신에 차서 잘못된 행동을 수행했고, 트랜잭션에 서명했습니다. 예외(exception)도, 스택 트레이스(stack trace)도 없었습니다. 그저 신뢰할 이유가 전혀 없는 주소로 자금을 전송했을 뿐입니다.
모든 인젝션을 프롬프트로만 막을 수는 없습니다. 하지만 에이전트가 건전성 검사(sanity-check)를 수행하지 않은 트랜잭션의 전송은 거부할 수 있습니다. 이 포스트는 당신이 서명하기 _전_에 세 가지 읽기 전용 장벽을 실행하는 약 90줄의 Python 도구인 TxCanary를 소개합니다. API 키나 개인 키가 필요 없으며, 자금을 전혀 이동시키지 않습니다. 60초 안에 실행해 보세요.
요약(TL;DR)
- LLM 에이전트는 잘못된 가격을 읽거나 환각 (Hallucination)된 주소를 읽고 망설임 없이 실행할 것입니다. 이 실패는 소리 없이 발생합니다.
- 무거운 오라클 (Oracle)이나 ZK 증명 (ZK proofs)을 찾기 전에, 전송 전 (pre-send)에 실행되는 **저렴한 카나리 (cheap canary)**를 추가하세요: 세 가지 읽기 전용 (read-only) 체크입니다.
TxCanary.precheck()는 다음 사항들에 대해PASS/BLOCK및 사유를 반환합니다: (1) CoinGecko 대비 가격 건전성 (price sanity), (2)eth_getCode를 통한 주소 건전성 (address sanity), (3)eth_call을 통한 드라이 런 (dry-run).- 표준 라이브러리 (Stdlib) +
requests를 사용합니다.web3, 키, 서명 (signing)은 필요하지 않습니다. 키가 필요 없는 CoinGecko와 공개 Ethereum RPC를 호출합니다. - 이것은 스모크 테스트 (smoke test)이지, 감사 (audit)가 아닙니다. 제한 사항은 하단에 명시되어 있습니다.
이 글은 에이전트가 실행한 후가 아니라, 실행하기 전에 제어하는 방법에 관한 짧은 시리즈의 두 번째 글입니다. 첫 번째 글은 폭주하는 에이전트 루프를 위한 엄격한 지출 한도 설정 (a hard spend-cap for runaway agent loops)이었으며, 이번 글은 단 한 번의 잘못된 트랜잭션에 관한 것입니다.
실패의 원인은 모델이 멍청해서가 아닙니다. 모델이 확신하기 때문입니다.
우리는 에이전트를 더 똑똑하게 만들기 위해 많은 에너지를 소비합니다. 더 큰 컨텍스트 (context), 더 나은 도구 (tools), 더 날카로운 프롬프트 (prompts) 등이 그것입니다. 하지만 그 어떤 것도 실제 온체인 (on-chain) 실패 모드에는 대응하지 못합니다. 그 실패 모드는 더 좁고 더 지독합니다. 즉, 에이전트가 구문론적으로는 완벽하지만 의미론적으로는 쓰레기인 (syntactically perfect and semantically garbage) 트랜잭션을 생성하고, 그 트랜잭션과 서명자 (signer) 사이에는 아무런 장치가 없는 상태입니다.
이런 일이 발생하는 세 가지 구체적인 방법이 있으며, 이 모든 방법은 메인넷 읽기 전용 (mainnet read-only) 환경에서 재현할 수 있습니다:
- 환각을 일으킨 주소 (A hallucinated address). 에이전트가 USDC 컨트랙트를 "알고" 있거나, 웹페이지에서 주소를 파싱했거나, 혹은 주소가 주입(injected)된 경우입니다. 에이전트는 실제로 배포된 컨트랙트가 아닌 문자열—때로는 일반 지갑, 때로는 오타, 때로는 소각 주소(burn address)—로 전송을 생성합니다. 트랜잭션(tx) 자체는 유효하지만, 목적지가 잘못된 것입니다.
- 오래되었거나 잘못된 가격 (A stale or lied-to price). 시장 상황은 다른데 에이전트의 추론은 "ETH는 1,600달러이므로 좋은 매수 기회이다"라고 판단하거나, 주입된 메시지가 가짜 숫자를 제공하는 경우입니다. 에이전트는 존재하지 않는 가격을 기준으로 거래 규모(size)를 결정합니다.
- 리버트(revert)될 칼데이터 (Calldata that will revert). 허용량(allowance)이 없는
transferFrom호출, 데드라인이 지난 스왑(swap), 혹은 컨트랙트가 마이닝되는 즉시 거부할 메서드 등이 해당됩니다. 운이 좋으면 단순히 가스(gas)만 소모하고 끝나지만, 운이 나쁘면 되돌릴 수 없는 단계 이후에 해당 트랜잭션이 시퀀싱(sequenced)될 수 있습니다.
이 모든 문제에 대한 값비싼 해결책은 실제 트랜잭션 시뮬레이션 스택(transaction-simulation stack)—Tenderly, 포크된 노드(forked node), 정책 엔진(policy engine) 등—을 사용하는 것입니다. 결국에는 이것들을 갖추어야 합니다. 하지만 이것들이 시작점은 아니며, 구축하는 데 비용이 들지 않는 것도 아닙니다. 카나리(canary)는 비용이 들지 않습니다. 카나리는 에이전트가 서명하기 전에 이 세 가지 유형의 "멍청한" 버전들을 잡아냅니다.
해결책: 서명 전 실행하는 세 가지 읽기 전용 장벽 (read-only barriers)
도구 전체는 하나의 함수 — precheck(tx, claimed_price_usd, token_id, token_address) — 로 구성되며, 이는 판결(verdict)과 이유 목록을 반환합니다. 각 장벽은 독립적이며 읽기 전용(read-only)입니다. 이 함수가 수행하는 어떤 작업도 키(key)를 요구하거나 사용자의 자금에 접근하지 않습니다.
장벽 1 — 가격 건전성 (price sanity). 에이전트가 거래하고 있다고 "생각하는" 가격을 가져와 CoinGecko의 키가 필요 없는 /simple/price와 비교합니다. 만약 두 가격이 설정한 허용 오차(기본값 2%) 이상으로 차이가 난다면, BLOCK 합니다. 이는 주입된 숫자를 포함하여 "모델이 잘못된 숫자를 읽은" 상황에 대해 가능한 가장 저렴한 방어책입니다.
장벽 2 — 주소 무결성 (address sanity). 대상에 대해 eth_getCode를 호출합니다. 배포된 컨트랙트는 바이트코드 (bytecode)를 반환합니다. 환각된 주소 (hallucinated address), 일반 지갑 (EOA), 또는 소각 주소 (burn address)는 코드가 없는 0x를 반환합니다. 만약 토큰 컨트랙트를 호출하려는데 대상에 코드가 없다면, 그것은 토큰이 아닙니다. BLOCK 하세요. 이 단계 하나만으로도 모델이 지어낸 주소로의 전송을 감지했을 것입니다.
장벽 3 — 드라이 런 (dry-run). 최신 블록을 대상으로 eth_call을 통해 정확한 콜데이터 (calldata)를 전송합니다. eth_call은 읽기 전용 컨텍스트 (read-only context)에서 트랜잭션을 실행합니다. 동일한 EVM을 사용하지만, 상태 변경 (state change)이 없고, 가스 (gas)가 소모되지 않으며, 아무것도 브로드캐스트 (broadcast)되지 않습니다. 만약 실행이 되돌아간다면 (revert), 노드는 에러를 반환하며, 당신은 해당 리버트 사유 (revert reason)와 함께 BLOCK 합니다. 실제 트랜잭션이 실패할 상황이라면, 이 단계에서 먼저 무료로 실패하게 됩니다.
첫 번째 실패에서 바로 중단하는 대신, 세 가지를 모두 실행하고 모든 사유를 수집하는 이유는 잘못된 에이전트 동작이 종종 하나 이상의 문제를 일으키기 때문입니다. 당신은 단 하나의 레드 플래그 (red flag)가 아니라 전체 보고서를 원해야 합니다.
TxCanary — 전체 구성
의도적으로 표준 라이브러리 (Stdlib)와 requests만을 사용했습니다. web3를 사용하지 않으므로, pip install requests만 하면 끝납니다. 다른 아무것도 없는 깨끗한 환경의 머신에서도 실행됩니다. 첫 번째 파트의 지출 한도 (spend-cap) 도구와 동일한 멀티-RPC 폴백 (multi-RPC fallback) 방식을 사용합니다. 만약 publicnode가 속도 제한 (rate-limiting)을 건다면, Cloudflare를 거쳐 llamarpc로 넘어갑니다.
#!/usr/bin/env python3
"""TxCanary - 온체인 에이전트를 위한 전송 전 무결성 검사.
트랜잭션에 서명/전송하기 BEFORE 실행되는 세 가지 읽기 전용 장벽:
...
연동 방법
서명자 (signer) 바로 직전에 한 줄만 추가하면 됩니다. 평소처럼 트랜잭션을 빌드한 다음, 다음과 같이 실행하세요:
result = precheck(tx, claimed_price_usd=agent_price,
token_id="ethereum", token_address=tx["to"])
if result.verdict == "BLOCK":
...
BLOCK은 역방향으로의 되돌릴 수 없는 지점입니다. 만약 BLOCK이 발생하면 서명 코드(signing code)는 절대 실행되지 않습니다. 에이전트가 이미 보유하고 있는 네 가지 요소 — 트랜잭션(transaction), 추론에 사용된 가격, 자산의 CoinGecko ID, 그리고 대상 주소 — 를 전달하면 됩니다. 이 함수는 전체 사유 목록을 반환하므로, 인젝션(injection) 시도가 진행 중인 새벽 3시에 로그를 통해 왜 중단되었는지 확인할 수 있어 매우 중요합니다.
실행하기
pip install requests
python txcanary.py
계정, 키, 파우셋(faucet), 테스트넷(testnet)이 필요 없습니다. 네 가지 데모 케이스는 메인넷 읽기 전용(read-only) 상태에서 결정론적(deterministic)으로 작동합니다: 실제 USDC balanceOf 호출 (통과), 코드 없는 소각 주소 (차단), 실제와 크게 차이 나는 가격 (차단), 그리고 컨트랙트가 거부하는 허용량(allowance) 없는 transferFrom (차단). 이 코드가 접촉하는 두 엔드포인트(endpoint)는 공개되어 있습니다: 키가 필요 없는 CoinGecko의 /simple/price와 eth_getCode / eth_call을 위한 ethereum-rpc.publicnode.com이며, Cloudflare와 llamarpc로 폴백(fallback)됩니다.
출력 결과
실제 실행 결과 (실행 시점의 라이브 엔드포인트 수치 기준; 결정론적인 것은 정확한 값이 아니라 _판정(verdicts)_입니다):
TxCanary 데모 - 읽기 전용, 키 없음, 자금 이동 없음
--- 케이스 (a): 정상적인 USDC 호출, 정직한 가격 ---
...
실행 시마다 고정되는 부분: 케이스 (a)는 통과하고, (b)/(c)/(d)는 차단됩니다. CoinGecko의 정확한 가격과 USDC 코드의 바이트 수(byte-count)는 변동될 수 있지만, 통과/차단 결정은 변하지 않습니다.
이것이 아닌 것
저는 여러분이 이 도구의 범위를 벗어나 신뢰하게 만들기보다는, 차라리 과소평가하는 편을 택하겠습니다. 잘못된 안전감은 카나리(canary)가 없는 것보다 더 위험합니다.
- 이것은 감사(Audit)도 아니고, 실제 시뮬레이터(Simulator)도 아닙니다.
TxCanary는 대상이 컨트랙트(Contract)인지, 가격이 타당한지, 그리고 호출이 지금 당장 리버트(Revert)되지 않을지를 확인합니다. 이 도구는 해당 컨트랙트가 악의적인지, 로직이 안전한지, 또는 세 번의 호출 뒤에 어떤 일이 벌어지는지에 대해서는 아무것도 말해주지 않습니다. 이를 위해서는 Tenderly, 포크된 노드 시뮬레이션(Forked-node simulation), 또는 적절한 트랜잭션 정책 엔진(Transaction-policy engine)이 필요합니다. 카나리(Canary)는 오후 시간 동안 가장 먼저 추가하는 레이어이지, 유일한 레이어가 아닙니다. 폭주하는 루프(Runaway loop)와 단일 잘못된 트랜잭션(Bad tx) 모두를 방어할 수 있도록 사전 실행 지출 한도(Pre-execution spend-cap)와 함께 사용하세요. - 이것은 MPC나 키 커스터디(Key custody)가 아닙니다. 키가 어떻게 저장되는지 또는 누가 서명할 수 있는지에 대해서는 아무런 조치도 취하지 않습니다. 만약 위협이 유출된 키라면, 이것은 완전히 잘못된 도구입니다.
eth_call은 MEV나 슬리피지(Slippage)를 감지하지 못합니다. 최신 블록에서 통과된 드라이 런(Dry-run)이라 할지라도, 채굴될 때까지 샌드위치 공격(Sandwiched)을 당하거나, 프런트러닝(Front-run)을 당하거나, 더 나쁜 가격에 체결될 수 있습니다. "리버트되지 않음"을 실행 품질에 대한 보장이 아닌, 최소한의 하한선으로 취급하십시오.- 가격 확인은 특정 시점의 단일 소스 기반입니다. CoinGecko는 급격한 변동을 따라가지 못할 수 있으며, 단일 소스는 단일 소스일 뿐입니다. 중요한 작업을 수행할 때는 두 번째 피드(Feed)와 교차 검증하고, 실제 슬리피지 예산에 맞춰 허용 오차를 넓히십시오. 2%는 시작을 위한 기본값일 뿐, 절대적인 진리가 아닙니다.
- 공용 RPC(Public RPCs)는 최선 노력(Best-effort) 방식입니다. 속도 제한(Rate-limit)이 걸리거나 가끔 5xx 에러가 발생할 수 있습니다. 읽기 전용 카나리로는 괜찮지만, 프로덕션(Production) 환경에서는 자체 노드나 키 기반 제공업체를 사용하십시오.
이 중 어느 것도 핵심 주장(Core claim)을 훼손하지 않습니다. Grok의 손실은 에이전트가 신뢰해서는 안 될 텍스트에 따라 행동했으며, 잘못된 결정과 전송(Send) 사이에 아무것도 없었다는 점입니다. 세 가지 읽기 전용 확인(Read-only checks)은 그 사이에 존재하는 무언가입니다.
내가 스스로에게 아직 답하지 못한 질문
가격 장벽은 소프트한(soft) 장벽입니다. eth_getCode와 eth_call은 코드의 존재 여부나 리버트(revert) 여부와 같이 명확한 예/아니오(yes/no) 답변을 제공합니다. 하지만 "이 가격 변동(price drift)이 너무 심한가?"라는 질문은 전적으로 자산과 그 시점에 달려 있습니다. 스테이블코인(stablecoin)에게 2%는 괜찮은 수치지만, 블록 사이에도 2%가 움직이는 유동성이 낮은 밈코인(memecoin)에게는 터무니없는 수치입니다. 저는 고정된 상수 대신 최근의 실현 변동성(realized volatility)으로부터 자산별 허용 오차를 도출하는 방법을 고민해 왔지만, 가격 이력(price-history)에 대한 의존성을 불러오지 않으면서 이 도구가 "오후에 잠깐 하는 프로젝트" 수준을 넘어 비대해지지 않게 만드는 깔끔한 방법을 아직 찾지 못했습니다. 만약 여러분이 유동성이 낮은 토큰에서도 견고하게 작동하는 에이전트(agent)용 자산별 건전성 범위(sanity band)를 구축했다면, 어떻게 범위를 설정했는지 정말 궁금합니다. 댓글로 공유해 주세요.
온체인 에이전트(on-chain agent)를 위한 실행 전 제어(pre-execution control)에 관한 이 시리즈의 다음 글을 기대해 주세요.
AI의 도움을 받아 작성되었으며 사람이 검토 및 편집했습니다. 이 포스트의 코드는 게시 전 실제 CoinGecko 및 공개 Ethereum RPC 엔드포인트를 대상으로 실행되었습니다; 위의 출력 블록은 실제 실행 결과(2026-06-08)입니다. Grok 사건의 수치(약 $175K, 약 3B DRB, 모스 부호 주입, 약 80% 회수)는 2026년 5월 4일 전후로 SlowMist와 암호화폐 언론에서 보도된 내용입니다 — 인용하기 전에 원문을 확인하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기