모든 사기 토큰을 잡았다고 생각했지만, dev.to 글이 사각지대를 드러냈다.
출처: Dev.to
우리는 이더리움을 위한 실시간 사기‑토큰 탐지기를 운영하고 있습니다. 새로운 ERC‑20을 분석하고, 구매·판매 시뮬레이션을 통해 허니팟을 잡으며, 배포자와 자금 제공자를 클러스터링하고, 모든 정보를 실시간으로 점수화합니다. 4개월 동안 약 93,000개의 계약을 분석한 결과, 우리 파이프라인이 중요한 모든 것을 포착하고 있다고 꽤 확신했습니다.
그때 @sanjeevkkansal이 evm-deploy-watch(https://github.com/sanjeevkkansal/evm-deploy-watch)를 공개했습니다. 이는 MIT 라이선스로 배포된, 이더리움에 새로 배포되는 모든 계약을 일주일 동안 조사한 연구입니다. 정말 훌륭한 작업이며, 우리에게 두 가지를 알려주었습니다. 첫째, 우리의 핵심 가설을 독립적으로 검증했습니다. 둘째, 우리가 몰랐던 블라인드 스팟을 직접 비추었습니다.
이것이 그 격차이며, 우리가 어떻게 메꿨는지에 대한 내용입니다.
이미 공유한 가설
배포‑감시(deploy‑watch) 작성자의 핵심 발견은 배포 시점의 휴리스틱이 뒤처지는 공개 블랙리스트보다 우수하다는 것입니다. 소스 코드, 배포자 이력, 바이트코드를 계약이 생성되는 순간 바로 읽어들이면, 사기를 블록리스트에 오르기 며칠 전에 잡아낼 수 있습니다. 연구 결과는 ScamSniffer의 피드와 거의 겹치지 않음을 보여주었습니다—플래그된 계약들은 아직 어느 리스트에도 올라와 있지 않았습니다.
바로 우리가 파이프라인을 구축한 근거이므로, 완전히 독립적인 데이터셋과 방법론을 가진 누군가가 이를 확인해 주어 기뻤습니다. 기사에 적힌 “향후 단계”는 거의 우리 변경 로그와 같습니다: 구매·판매 시뮬레이션, CREATE2/팩토리 클러스터링, 배포자‑자금 제공자 그래프, 다중 신호 점수화. 우리는 이미 이를 프로덕션에서 운영하고 있습니다. 마지막에 더 자세히 다루겠지만, 이 글의 솔직한 부분은 우리가 하지 않았던 일입니다.
블라인드 스팟: 우리 파이프라인은 DEX 트리거에 의존한다
우리 설계에서 빠진 부분이 바로 여기였습니다.
우리는 유동성을 기반으로 데이터를 수집합니다. factory-watcher 서비스가 Uniswap V2/V3/V4 풀 생성 이벤트에 반응합니다. 토큰이 풀을 열면 파이프라인에 들어가 전체 분석을 받게 됩니다: 소스 정규식, 바이트코드 클러스터링, 실제 구매‑후‑판매 시뮬레이션, 배포자 그래프, 점수화. 이는 우리가 가장 우려하는 사기, 즉 러그나 허니팟에 적합한 트리거입니다. 풀 없이는 구매자를 속일 수 없으니까요.
하지만 풀을 전혀 만들지 않는 사기 유형도 존재합니다. 이들은 누구도 거래하도록 의도하지 않기 때문에 풀을 생성하지 않습니다.
FlashUSDT / 가짜 테더: 온체인 피해자가 없는 사기
“FlashUSDT” 혹은 “proof‑of‑funds” 계열은 전적으로 오프체인에서 이루어집니다. 메커니즘은 다음과 같습니다.
- 운영자는
name()이Tether USD,symbol()이USDT,decimals()가6을 반환하는 ERC‑20을 배포합니다. 누구든 할 수 있습니다. 온체인에서는 토큰 이름을 자유롭게 정할 수 있기 때문입니다. - 그들은 이 토큰을 대량 보유한 지갑을 만들거나, 이미 보유하고 있는 지갑을 보여줍니다. 대부분의 지갑 UI와 탐색기는 메타데이터를 읽어 250,000 USDT처럼 표시합니다.
- “자금”을 이동시켜야 한다는 이유를 제시합니다: OTC 거래, 에스크로 증명, 채용 보너스, “유동성 봇” 시연 등.
- 사용자는 실제 가치가 있는 무언가—실제 USDT, ETH, 물품, 토큰 승인—를 보내게 됩니다.
- 가짜 잔액은 언제나 가치가 0입니다. 풀도, 가격도, 거래도 없습니다. 전체 사기는 위조된 메타데이터와 사회공학에 기반합니다.
이 토큰들은 DEX와 전혀 접촉하지 않기 때문에 시뮬레이션 기반 탐지는 전혀 감지하지 못합니다. 시뮬레이션할 것이 없으니 말이죠. Uniswap 페어를 기다리는 어떤 시스템에도 보이지 않았습니다—우리도 마찬가지였습니다.
격차, 수치화
“우린 블라인드 스팟이 있다”는 느낌만으로는 부족합니다. “우린 이 계열의 0.5%만 잡는다”는 버그 리포트가 필요합니다.
- 우리 데이터베이스에는 15개의 FlashUSDT‑명 토큰이 4개월(2026‑02‑12 ~ 06‑08) 동안 존재했으며, 전체 분석 계약은 93,000개였습니다.
- deploy‑watch 연구에서는 해당 기간에 주당 약 192개(134
FlashUSDT+ 58FlashUSDTLiquidityBot)가 관찰되었습니다. - 따라서 우리는 대략 0.5% 정도만 포착한 셈입니다.
- 기사에서 자세히 분석한 두 계약은 우리 데이터베이스에 전혀 존재하지 않았습니다.
이는 점수 산출 오류가 아니라 커버리지 누락입니다. 계약이 파이프라인에 들어오지 않았으니 점수를 매길 수 없었습니다. ingest하지 않은 것을 순위 매길 수는 없죠.
해결책: 모든 배포를 ingest, 모든 풀을 기다리지 않는다
해법은 개념적으로 간단합니다—풀 생성이 아니라 계약 생성을 감시하면 됩니다. 엔지니어링 측면에서는 트래픽을 제한하는 것이 핵심입니다.
우리는 이미 mempool-watcher를 운영하고 있어, 대기 중인 트랜잭션을 스트리밍합니다. 계약 생성은 to === null인 트랜잭션이므로, 가장 저렴한 훅 포인트는 바로 mempool 루프입니다:
// main-loop.ts - a contract creation is a tx with no recipient
if (tx.to === null && adapters.deployWatcher) {
adapters.deployWatcher.onDeploy({ from: tx.from, nonce: tx.nonce, hash: tx.hash });
}
deployWatcher는 발신자와 nonce로 계약 주소를 예측하고, 배포가 채굴될 때까지 기다린 뒤 두 개의 메타데이터 필드를 읽고, 그때 비로소 전체 분석이 필요할지 판단합니다:
import { Contract, JsonRpcProvider, getCreateAddress } from "ethers";
const ERC20_MIN_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
];
onDeploy(tx: DeployTx): void {
let address: string;
try {
address = getCreateAddress({ from: tx.from, nonce: tx.nonce }).toLowerCase();
} catch {
return; // CREATE2 / malformed - out of scope for v1
}
if (seen.has(address)) return; // dedupe; bounded Set, cleared at 50k
seen.add(address);
setTimeout(async () => {
const code = await provider.getCode(address);
if (!code || code === "0x") return; // reverted or not mined yet
const c = new Contract(address, ERC20_MIN_ABI, provider);
const [name, symbol] = await Promise.all([
c.name().catch(() => ""),
c.symbol().catch(() => ""),
]);
if (isImpersonation(String(name), String(symbol), address)) {
await opts.enqueue(address); // -> analysis_queue -> contract-analyzer
}
}, delayMs); // ~45s: deploys are seen pending, read metadata after they mine
}
여기서 의도적으로 선택한 몇 가지 요소
-
메타데이터‑전용 트리아지
이 단계에서는name()과symbol()두 개의eth_call만 수행합니다. 시뮬레이션, 소스 코드 가져오기, 그래프 분석은 하지 않죠. 연구에서는 하루에 약 1,200개의 최상위 배포가 관찰되었습니다. 자체 Nethermind 노드에 두 번 호출하는 정도는 부담이 없습니다. 실제 분석은 위조된 주요 토큰을 가장少수만 대상으로 실행됩니다. -
예측‑후‑확인
getCreateAddress(from, nonce)를 사용하면 순수CREATE배포에 대한 결정론적 주소를 알 수 있어, 트랜잭션이 대기 중일 때 바로 감시를 시작하고, 채굴 후 코드를 읽을 수 있습니다. -
bounded
seenSet
배포는 mempool 스냅샷마다 여러 번 나타날 수 있습니다. Set을 이용해 중복을 제거하고, 50,000 항목에 도달하면 자동으로 비워 메모리 누수를 방지합니다. 오래된 주소를 다시 확인해도 무해합니다. -
CREATE2는 v1 범위 외
CREATE2로 배포된 계약은 주소가 salt에 의존하므로getCreateAddress로는 예측할 수 없습니다. 이를 알려진 제한 사항으로 명시했으며, 향후 개선 과제로 남겨두었습니다.
전체 계열을 무력화하는 체크
실제 탐지는 거의 반전이 없습니다.