Uniswap 독풀 함정, 29시간 만에 13만 달러 벌어들여

BlockbeatsBlockbeats

원문 제목: 《견적 0%, 실행 12.8%: Uniswap v4 "독이 든 풀" 동적 수수료 함정 복기》

원문 저자: 왕쯔하오, BitsLab

 

2026년 8월 22일부터 23일까지, BNB Chain의 한 Uniswap v4 USDC/WBNB 동적 수수료 풀은 애그리게이터 견적에서 0% 수수료를 표시했지만, 사용자 실제 실행 시 12.8%의 LP 수수료가 부과되었습니다. 약 29시간 동안 이 풀은 21,086건의 거래를 실행하고 누적 수수료 131,888달러를 기록한 뒤 제거되었습니다. 피해 거래는 모두 성공했습니다. 컨트랙트는 도난되지 않았고, 출력은 사용자가 설정한 하한선보다 높았으며, 어떤 이상 신호도 없었습니다. 차액은 조용히 수수료로 전환되었습니다.

 

본문은 그중 한 건의 샘플 거래를 출발점으로 삼습니다:

(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: 사용자가 5,926.90 USDT를 투입했지만 예상보다 757.43 USDT 적게 받음), 이 풀이 어떻게 "견적 0%, 실행 12.8%"를 구현했는지 분해합니다. 수수료 함수의 3단계 판정, 우회된 슬리피지 보호, 다른 LP를 차단하는 화이트리스트까지. 모든 결론은 온체인 데이터와 로컬 통제 검증에 기반합니다.

 

01 견적부터 실행까지의 경로

사용자가 애그리게이터 프런트엔드에서 스왑을 시작하면 전체 경로는 다음과 같이 진행됩니다:

 

 

이 경로에는 두 가지 배경 사실이 있으며, 이후의 모든 판정 단계는 이 두 가지 위에 구축됩니다.

먼저 v4 측을 보면, Hook이 건별로 수수료를 재정의할 수 있습니다. PoolKey.fee = 0x800000은 동적 수수료 풀을 선언하며, Hook은 beforeSwap의 세 번째 반환값에 0x400000 | fee를 담아 전달합니다. v4-core에서 이 반환값을 받는 코드(Pool.sol:303-305):

isOverride()는 재정의 플래그를 확인하고, removeOverrideFlagAndValidate()는 0x400000을 제거한 뒤 MAX_LP_FEE = 1,000,000(100%)을 초과하지 않는지 검증한 다음, 해당 수수료율로 이번 스왑을 실행하고 Swap 이벤트에 기록합니다. 즉, Hook이 반환하는 대로 사용자가 지불합니다. 출력이 amountOutMin보다 높기만 하면 됩니다. 이는 공식 기능으로, 변동성이나 재고에 따라 수수료를 조정하는 것은 정당한 용도입니다.

다음으로 이 Hook 자체를 보면, 두 가지 권한만 선언했습니다. v4는 권한을 컨트랙트 주소의 하위 14비트에 인코딩하며(Hooks.sol:29-47), 배포 시 검증되고 이후 변경할 수 없습니다. v4-core 측의 플래그 정의와 주소 디코딩 대조:

 

온체인 실측 getHookPermissions() 반환값은 비트맵과 일치하며, 이 두 비트만 참입니다.

Hook은 소스 코드가 없습니다. 본문은 바이트코드에서 수수료 함수 전체를 디컴파일했습니다. 다음은 이미 온체인에서 실행된 이 로직의 동작을 분해한 것입니다:

이제 단계별로 살펴봅니다. 처음 두 단계는 "이것이 시뮬레이션인가"에 답하고, 세 번째 단계는 실제 거래가 얼마를 지불할지 결정합니다.

 

02 수수료 함수의 3단계 판정

판정 1: 호출자 구분

수수료 함수가 호출되면 먼저 tx.origin을 비교합니다. 세 개의 상수 주소 중 하나와 같으면 바로 폴백 수수료를 반환합니다. 폴백 수수료는 현재 0%로 설정되어 있습니다.

세 주소는 무작위로 선택된 것이 아닙니다. eth_call에서 from을 지정하지 않으면 트랜잭션 origin이 제로 주소에 위치합니다. 애그리게이터의 일괄 견적은 보통 from을 지정하지 않습니다. 로컬 포크에서 직접 검증:

 

첫 번째 단계는 이 세 주소만 인식합니다. 다음 단계에서는 일반 주소를 지정한 후 Hook이 어떻게 더 구분하는지 살펴봅니다.

 

판정 2: 가스 확인

tx.origin을 일반 주소로 바꿔 다시 시도해도 반환값은 여전히 0%입니다. 첫 번째 단계는 우회되었지만 수수료는 변하지 않았습니다. 이는 첫 번째 단계가 유일한 판정이 아님을 의미하며, 두 번째 단계를 찾아냈습니다: gasleft() * 10100 / 10000 >= gate.

애그리게이터 견적은 가스 부족으로 후보 경로가 시뮬레이션에서 실패하는 것을 방지하기 위해 매우 높은 가스를 제공하는 것이 관례입니다. 실제 거래가 Hook에 도달하기 전에 가스는 Router, 승인, 이전 홉에서 이미 소모됩니다. 동일한 코드, 세 가지 가스:

경계선은 실측 결과 16.85M–16.9M 가스 사이에 위치합니다. 30M과 2.59M의 차이가 바로 "견적"과 "실행"의 차이입니다.

 

판정 3: 건별 의사 난수

앞의 두 단계를 통과한 실제 거래는 세 번째 단계로 진입합니다. 블록 환경 지문을 해시한 뒤 10,000으로 나눈 나머지를 구하고, 결과가 속한 구간에 따라 해당 수수료 등급을 반환합니다. 현재 설정은 8% / 10% / 10%의 세 등급이며, 어디에도 속하지 않으면 0%로 폴백합니다.

이 단계에는 주목할 만한 설계가 두 가지 있습니다. 지문에는 calldata의 세 필드가 혼합되어 있습니다. 애그리게이터 견적의 calldata와 실제 라우팅의 calldata는 원래 다르기 때문에 지문도 자연히 달라집니다. 또한 건별 의사 난수는 수수료에 분포를 부여하여 몇 건만 보면 규칙을 알아차리기 어렵고, 단순한 규칙 열거로는 막을 수 없습니다.

전체 21,086건의 온체인 이벤트에서 19개의 과거 수수료 등급(6.8%–28%)이 나타났으며, 이는 수수료 등급이 관리자 함수에 의해 필요에 따라 계속 재설정되었음을 의미합니다. 샘플 거래 실행 시 적용된 수수료는 12.8%였습니다.

 

3단계 결합의 전체 그림

 

이제 "독이 든 풀"을 정의할 수 있습니다. 다음 세 가지 조건을 동시에 충족하는 v4 풀입니다: PoolKey.fee가 동적 수수료 플래그를 포함하고, 수수료 결정이 공개 시장 상태가 아닌 실행 환경 신호(gas, origin, 블록 환경)를 읽으며, 견적 시뮬레이션과 온체인 실행의 수수료가 체계적으로 분기하여 수혜자가 독이 든 풀 배포자가 되는 경우입니다. 경계는 수수료가 무엇을 읽는가에 있습니다. 모든 호출자에게 동일한 수수료를 반환하면 프로그래머블 시장 설계이고, "누가 견적하는가"에 따라 다른 수수료를 반환하면 라우팅 시스템에 대한 기만입니다.

여기까지 수수료 분기 메커니즘이 완전히 드러났습니다. 하지만 수수료가 부과된 후에도 거래가 성공하는 이유가 다음 질문입니다.

 

03 슬리피지 보호가 롤백되지 않은 이유

먼저 이 거래의 전체 경로와 금액을 살펴봅니다:

 

 

해당 홉의 Swap 이벤트는 적용된 수수료를 fee 필드에 기록합니다: 128000. v4의 수수료 단위는 1,000,000이 100%이므로 128000은 12.8%입니다. 이 거래의 손실은 두 가지 관점에서 측정할 수 있습니다. 풀 관점에서는 수수료가 WBNB 입력에 부과되며, 전체 경로 관점에서는 스테이블코인 입출금 차액이 757.43 USDT로 12.7794%의 손실률이며, 여기에는 이전 홉 수수료와 페그 이탈로 인한 차이도 포함됩니다. 두 숫자의 차이는 0.02%포인트에 불과하므로 손실의 주요 원인이 이 풀의 LP 수수료임을 알 수 있습니다.

amountOutMin은 최종 출력 하한만 검증할 뿐 중간 홉별 수수료는 확인하지 않습니다:

 

 

12.8%의 수수료는 20.32%의 버퍼 안에 완전히 포함되어 출력이 여전히 하한선보다 높습니다. 거래는 성공하고 롤백되지 않습니다. 주류 스테이블코인 경로의 일반적인 기본값은 0.1%–1%인데, 이 경로는 20배 이상의 여유를 제공했습니다. 사용자는 넉넉한 버퍼가 "안정적"이라고 생각하지만, 실제로는 경로상의 모든 컨트랙트를 상대방의 자율에 맡기는 것입니다. 이 계산을 그림으로 나타내면:

 

 

04 독이 든 풀 배포자의 작업

화이트리스트: 다른 LP 차단

화이트리스트 로직은 beforeAddLiquidity에 있으며, 디컴파일 결과는 다음과 같습니다:

 

 

유동성 추가에는 세 개의 관문을 통과해야 합니다. 먼저 모든 v4 Hook에 있는 PoolManager 호출 검사, 그다음 공식 PositionManager를 경유해야 하며, 마지막으로 포지션 보유자의 화이트리스트를 확인합니다. 목록에 없는 주소는 여기서 컨트랙트에 의해 롤백되므로 다른 LP는 진입할 수 없고, 수수료 수입은 희석되지 않고 전액 배포자에게 귀속됩니다. 목록 자체는 관리자 함수(셀렉터 0xc4452e52)에 의해 주소별로 유지됩니다.

 

0% 등급과 거래량 부풀리기

21,086건의 거래를 함께 놓고 보면 분포에서 뚜렷한 패턴이 나타납니다. 0% 등급은 6,946건, 거래액 12,947,751달러, 건당 평균 1,864달러이며, 수수료 부과 등급은 14,140건, 거래액 1,120,106달러, 건당 평균 79달러입니다. 대규모 거래는 0% 등급에 집중되고, 수수료는 소액 거래에 집중됩니다. 대규모 0%의 가장 합리적인 설명은 배포자가 높은 가스로 자전거래를 한 것입니다. 높은 가스는 애그리게이터 견적과 동일한 판정에 해당하므로 자전거래의 수수료 비용은 거의 0에 가깝습니다. 부풀려진 거래량은 풀을 시세 확인 사이트 순위에 올려놓았고(스냅샷 기준 24시간 거래액 약 1,082만 달러, 16,671건), 애그리게이터의 눈에는 깊이가 좋고 수수료가 낮은 풀로 보입니다.

 

필요에 따른 수수료 등급 재설정

판정 3에서 언급한 19개의 과거 수수료 등급은 디컴파일된 수수료 설정 함수(셀렉터 0x4d909a45, 공개 시그니처 미등록)에서 비롯됩니다:

 

 

네 개의 수수료 등급과 임계값이 하나로 패킹되어 keccak(poolId, 2) 저장 슬롯에 기록됩니다. 온체인 실측 결과 해당 슬롯 값은 0x0138800186a00186a0이며, 함수의 패킹 형식과 구간별로 일치합니다. 수수료 등급은 여기서 언제든 조정 가능한 매개변수이며, 배포 시 고정되지 않습니다.

 

수명 주기

이 풀은 08-22 06:57에 생성되었고, 07:13에 첫 스왑이 발생했으며, 21:39에 본문의 샘플 거래가 이루어졌습니다. 08-23 12:25에 마지막 거래가 실행된 후 제거되었습니다. 전체 활성 기간은 약 29시간이며, 총 거래액 14,067,857달러, 수수료 수입 131,888달러(배포자 비용 미차감)입니다. 재확인 시 등록 플래그는 다시 활성화 상태로 돌아와 있었습니다. 풀은 여전히 존재하며 언제든 다시 활성화할 수 있습니다.

 

05 전체 경로

전체 공격 체인을 연결하면:

 

 

전체 체인의 인과 관계는 위 그림에 모두 담겨 있으며, 세 가지 조건이 모두 필요합니다. 시뮬레이션이 처음 두 단계에 해당하고, 실제 거래가 세 번째 단계에 해당하며, 버퍼가 수수료보다 커야 합니다. 어느 하나라도 성립하지 않으면 이 수법은 무력화됩니다.

 

06 수정 및 제안

첫 번째는 애그리게이터의 견적 방식입니다. 근본 원인은 시뮬레이션과 실행이 같은 경로를 따르지 않는다는 점입니다. 견적은 이상화된 풀 수준 조회에 제로 주소와 높은 가스를 사용하지만, 실제 거래는 실제 신원과 이미 소모된 가스를 가지고 옵니다. Hook은 이러한 차이를 정확히 읽어낼 수 있으므로 수수료가 분기됩니다. 해결책은 시뮬레이션을 실행에 가깝게 만드는 것입니다. 실제로 전송할 Router calldata, 실제 from/to, 온체인 거래에 가까운 가스로 시뮬레이션하고, 실행 후 Swap 이벤트를 디코딩하여 fee를 확인한 뒤 견적과 불일치하면 우선순위를 낮추거나 제거하는 것입니다.

두 번째는 사용자의 슬리피지 설정입니다. 근본 원인은 min_out이 너무 느슨하게 설정된 것입니다. 20.32%의 버퍼가 12.8%의 수수료를 완전히 흡수하여 거래가 정상적으로 성공합니다. 주류 스테이블코인 경로에서는 0.1%–1%면 일상적인 사용에 충분하므로, 거래를 시작하기 전에 이 숫자를 확인해야 합니다. 지갑과 프런트엔드와 백엔드가 기본값을 좁히면 사용자 대부분의 위험을 차단할 수 있습니다.

세 번째는 라우팅 진입 기준입니다. 근본 원인은 모든 동적 수수료 풀이 견적 경쟁에 직접 참여할 수 있다는 점입니다. 오픈소스도, 감사 기록도 없는 Hook도 부풀려진 거래량으로 추천 경로에 진입할 수 있습니다. 신뢰할 수 있는 프로필이 없는 동적 수수료 풀은 기본적으로 라우팅에 포함하지 않거나 우선순위를 크게 낮추는 것이 가장 안전한 처리입니다.

 

데이터 및 설명

·샘플 거래:

https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0(블록 117497524)

·풀 초기화 거래:

https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05

·PoolManager:

0x28e2ea090877bf75740558f6bfb36a5ffee9e9df

·Pool ID:

0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea

·Hook:

0xd111b3ddd92e627f1864520c770e913ec04e0880(소스 코드 없음, 의사 코드는 본문이 독립적으로 디컴파일함; 관리자 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)

·거래 및 수수료: 해당 풀의 전체 21,086건 Swap 이벤트를 건별로 누적(eth_getLogs), WBNB는 동일 거래의 실행 가격으로 환산; 수입은 총액 기준이며 배포자 비용 미차감

이 콘텐츠는 정보 및 교육 목적으로만 제공되며 BTCC와 관련된 투자 자문을 하지 않습니다. BTCC는 위 내용의 진실성, 정확성 및 독창성을 보장하기 위해 최선을 다하지만, 보장할 수는 없습니다.

추천

9월 FOMC 전망: 단 한 번의 금리 인상, 정말 필요한가?《CLARITY Act》 표결 임박, BTC 급등 후 반락…단기 변동성 확대 가능성BTCC 아침 주요 뉴스 선별 (9월16일)BTCC 아침 주요 뉴스 선별 (9월15일)BTCC 데일리 (9.16)|CLARITY 법안 다시 제동, ARB 18% 역주행 상승