กับดักพูลพิษ Uniswap เก็บค่าธรรมเนียม 131,888 ดอลลาร์ใน 29 ชั่วโมง

BlockbeatsBlockbeats

ชื่อเรื่องต้นฉบับ: 《เสนอราคา 0% ดำเนินการ 12.8%: ทบทวนกับดักค่าธรรมเนียมไดนามิก "พูลพิษ" ของ Uniswap v4》

ผู้เขียนต้นฉบับ: หวัง จื่อหาว, BitsLab

 

ระหว่างวันที่ 22-23 สิงหาคม 2026 พูลค่าธรรมเนียมไดนามิก USDC/WBNB ของ Uniswap v4 บน BNB Chain แสดงค่าธรรมเนียม 0% ในราคาเสนอของ aggregator แต่ผู้ใช้ถูกเรียกเก็บค่าธรรมเนียม LP 12.8% จริง ในช่วงเวลาประมาณ 29 ชั่วโมง พูลนี้มีการทำธุรกรรม 21,086 รายการ ค่าธรรมเนียมสะสม 131,888 ดอลลาร์สหรัฐ ก่อนถูกปิด ธุรกรรมของเหยื่อทั้งหมดสำเร็จ: สัญญาไม่ถูกแฮ็ก ผลลัพธ์สูงกว่าขีดจำกัดล่างที่ผู้ใช้ตั้งไว้ ไม่มีสัญญาณผิดปกติใดๆ ส่วนต่างถูกเปลี่ยนเป็นค่าธรรมเนียมอย่างเงียบๆ

 

บทความนี้ใช้ธุรกรรมตัวอย่างหนึ่งรายการเป็นจุดเริ่มต้น:

(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: ผู้ใช้ใส่ 5,926.90 USDT ได้รับน้อยกว่าที่คาด 757.43 USDT) เพื่อวิเคราะห์ว่าพูลนี้ทำ "เสนอราคา 0% ดำเนินการ 12.8%" ได้อย่างไร: การตัดสินสามชั้นของฟังก์ชันค่าธรรมเนียม การป้องกัน slippage ที่ถูกทำให้ไร้ผล และ whitelist ที่กัน LP รายอื่นออกไป ข้อสรุปทั้งหมดอิงจากข้อมูลบนเชนและการตรวจสอบแบบควบคุมในเครื่อง

 

01 เส้นทางจากราคาเสนอสู่การดำเนินการ

หลังจากผู้ใช้เริ่มการแลกเปลี่ยนที่ frontend ของ aggregator เส้นทางทั้งหมดเป็นดังนี้:

 

 

มีข้อเท็จจริงพื้นฐานสองประการบนเส้นทางนี้ ซึ่งการตัดสินทุกชั้นถัดไปสร้างขึ้นจากสองจุดนี้

ดูด้าน 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 ตามค่าธรรมเนียมนั้นและเขียนลงในเหตุการณ์ Swap กล่าวคือ Hook ส่งค่าอะไรกลับ ผู้ใช้ก็จ่ายตามนั้น—ตราบใดที่ผลลัพธ์ยังสูงกว่า amountOutMin นี่เป็นฟังก์ชันอย่างเป็นทางการ การปรับค่าธรรมเนียมตามความผันผวนหรือสินค้าคงคลังเป็นการใช้งานที่ถูกต้อง

ดู Hook นี้เอง: ประกาศสิทธิ์เพียงสองรายการ v4 เข้ารหัสสิทธิ์ใน 14 บิตล่างของที่อยู่สัญญา (Hooks.sol:29-47) ตรวจสอบตอน deploy และแก้ไขภายหลังไม่ได้ นิยามธงฝั่ง v4-core และการถอดรหัสที่อยู่สอดคล้องกัน:

 

การทดสอบบนเชน getHookPermissions() คืนค่าตรงกับ bitmap: มีเพียงสองบิตนี้เป็นจริง

Hook ไม่มีซอร์สโค้ด บทความนี้ decompile ฟังก์ชันค่าธรรมเนียมทั้งหมดจาก bytecode—ต่อไปนี้คือการวิเคราะห์พฤติกรรมของตรรกะที่เคยทำงานบนเชนแล้ว:

ต่อไปดูทีละชั้น สองชั้นแรกตอบคำถาม "นี่คือการจำลองหรือไม่" ชั้นที่สามตัดสินว่าธุรกรรมจริงจ่ายเท่าไร

 

02 การตัดสินสามชั้นของฟังก์ชันค่าธรรมเนียม

การตัดสินที่หนึ่ง: แยกผู้เรียก

เมื่อฟังก์ชันค่าธรรมเนียมถูกเรียก ขั้นแรกเปรียบเทียบ tx.origin: ถ้าเท่ากับหนึ่งในสามที่อยู่คงที่ จะคืนค่ากลับเป็นค่าสำรองทันที ค่าสำรองปัจจุบันตั้งเป็น 0%

สามที่อยู่ไม่ได้เลือกมาสุ่ม เมื่อ eth_call ไม่ระบุ from ต้นทางของธุรกรรมจะอยู่ที่ zero address—การสอบถามราคาแบบกลุ่มของ aggregator มักไม่ระบุ ตรวจสอบโดยตรงใน fork ท้องถิ่น:

 

ชั้นแรกยอมรับเฉพาะสามที่อยู่นี้ ชั้นถัดไปดูว่าเมื่อระบุที่อยู่ธรรมดาแล้ว Hook จะแยกได้อย่างไรอีก

 

การตัดสินที่สอง: ตรวจสอบ gas

เปลี่ยน tx.origin เป็นที่อยู่ธรรมดาแล้วลองใหม่ ผลลัพธ์ยังเป็น 0%—ชั้นแรกถูกข้ามไป แต่ค่าธรรมเนียมไม่เปลี่ยน แสดงว่าชั้นแรกไม่ใช่การตัดสินเดียว พบชั้นที่สอง: gasleft() * 10100 / 10000 >= gate

การเสนอราคาของ aggregator มักให้ gas สูงมาก เพื่อป้องกันเส้นทางผู้สมัครล้มเหลวในการจำลองเพราะ gas ไม่พอ; ก่อนธุรกรรมจริงเข้า Hook gas ถูกใช้ไปแล้วโดย Router การอนุญาต และ hops ก่อนหน้า โค้ดเดียวกัน gas สามระดับ:

เส้นแบ่งที่ทดสอบจริงอยู่ระหว่าง 16.85M–16.9M gas ความแตกต่างระหว่าง 30M และ 2.59M คือความแตกต่างระหว่าง "ราคาเสนอ" และ "การดำเนินการ"

 

การตัดสินที่สาม: สุ่มเทียมรายธุรกรรม

ธุรกรรมจริงที่ผ่านสองชั้นแรกเข้าสู่ชั้นที่สาม: แฮชลายนิ้วมือสภาพแวดล้อมบล็อกแล้ว mod ด้วย 10,000 คืนค่าธรรมเนียมตามช่วงที่ผลลัพธ์อยู่ ปัจจุบันตั้งสามระดับ 8% / 10% / 10% ถ้าไม่ตรงคืนค่า 0%

ชั้นนี้มีการออกแบบที่น่าสนใจสองจุด ลายนิ้วมือผสมสามฟิลด์ของ calldata—calldata ของราคาเสนอ aggregator กับ calldata ของเส้นทางจริง本来就ต่างกัน ลายนิ้วมือจึงต่างกันด้วย นอกจากนี้ การสุ่มเทียมรายธุรกรรมทำให้ค่าธรรมเนียมมีการกระจาย ดูไม่กี่รายการยากที่จะเห็นรูปแบบ การแจกแจงกฎง่ายๆ กันไม่อยู่

ในเหตุการณ์บนเชนทั้งหมด 21,086 รายการ มีค่าธรรมเนียมในอดีต 19 ระดับ (6.8%–28%) แสดงว่าระดับค่าธรรมเนียมถูกปรับใหม่โดยฟังก์ชันผู้ดูแลตามต้องการ; ตอนธุรกรรมตัวอย่างเกิดขึ้น ระดับที่มีผลคือ 12.8%

 

ภาพรวมเมื่อรวมสามชั้น

 

ถึงตรงนี้สามารถนิยาม "พูลพิษ" ได้—พูล v4 ที่เข้าเงื่อนไขสามข้อพร้อมกัน: PoolKey.fee มีธงค่าธรรมเนียมไดนามิก; การตัดสินค่าธรรมเนียมอ่านสัญญาณสภาพแวดล้อมการดำเนินการ (gas, origin, สภาพแวดล้อมบล็อก) ไม่ได้อ่านสถานะตลาดสาธารณะ; ค่าธรรมเนียมระหว่างการจำลองราคาเสนอและการดำเนินการบนเชนแยกออกจากกันอย่างเป็นระบบ ผู้ได้ประโยชน์คือผู้ deploy พูลพิษ เส้นแบ่งอยู่ที่ค่าธรรมเนียมอ่านอะไร: คืนค่าธรรมเนียมเดียวกันกับผู้เรียกทุกคนคือการออกแบบตลาดที่โปรแกรมได้; คืนค่าธรรมเนียมต่างกันตาม "ใครถามราคา" คือการหลอกลวงระบบเส้นทาง

ถึงตรงนี้ กลไกการแยกค่าธรรมเนียมสมบูรณ์แล้ว แต่หลังจากเก็บค่าธรรมเนียมแล้ว ทำไมธุรกรรมยังสำเร็จ เป็นคำถามถัดไป

 

03 ทำไมการป้องกัน slippage ไม่ย้อนกลับ

ดูเส้นทางและจำนวนเงินทั้งหมดของธุรกรรมนี้ก่อน:

 

 

เหตุการณ์ Swap ของ hop นั้นบันทึกค่าธรรมเนียมที่มีผลในฟิลด์ fee: 128000 หน่วยค่าธรรมเนียมของ v4 ใช้ 1,000,000 เป็น 100% ดังนั้น 128000 คือ 12.8% การสูญเสียของธุรกรรมนี้วัดได้สองมุม: จากมุมพูล ค่าธรรมเนียมเรียกเก็บจากอินพุต WBNB; จากมุมเส้นทางทั้งหมด ส่วนต่าง stablecoin เข้า-ออก 757.43 USDT คิดเป็นการสูญเสีย 12.7794% ซึ่งรวมค่าธรรมเนียม hop ก่อนหน้าและความเบี่ยงเบนจาก peg สองตัวเลขต่างกันเพียง 0.02 จุดเปอร์เซ็นต์ แสดงว่าแหล่งหลักของการสูญเสียคือค่าธรรมเนียม LP ของพูลนี้

amountOutMin ตรวจสอบเฉพาะขีดจำกัดล่างของผลลัพธ์สุดท้าย ไม่ดูว่าแต่ละ hop เก็บค่าธรรมเนียมเท่าไร:

 

 

ค่าธรรมเนียม 12.8% ทั้งหมดอยู่ในบัฟเฟอร์ 20.32% ผลลัพธ์ยังสูงกว่าขีดจำกัดล่าง—ธุรกรรมสำเร็จ ไม่ย้อนกลับ ค่าเริ่มต้นปกติของเส้นทาง stablecoin หลักคือ 0.1%–1% เส้นทางนี้ให้พื้นที่มากกว่า 20 เท่า ผู้ใช้คิดว่าบัฟเฟอร์กว้างคือ "ปลอดภัยขึ้น" แต่จริงๆ แล้วมอบทุกสัญญาบนเส้นทางให้กับความซื่อสัตย์ของคู่สัญญา วาดบัญชีนี้เป็นภาพ:

 

 

04 การดำเนินการของผู้ deploy พูลพิษ

Whitelist: กัน LP รายอื่นออกไป

ตรรกะ whitelist อยู่ใน beforeAddLiquidity ผลการ decompile เป็นดังนี้:

 

 

การเพิ่มสภาพคล่องต้องผ่านสามด่าน: อย่างแรกคือการตรวจสอบการเรียก PoolManager ที่ทุก v4 Hook มี จากนั้นต้องผ่าน PositionManager อย่างเป็นทางการ สุดท้ายตรวจสอบ whitelist ของผู้ถือ position ที่อยู่ที่ไม่อยู่ในรายชื่อจะถูกสัญญาย้อนกลับตรงนี้ LP รายอื่นเข้าไม่ได้ รายได้ค่าธรรมเนียมไม่ถูกเจือจาง ตกเป็นของผู้ deploy ทั้งหมด รายชื่อดูแลโดยฟังก์ชันผู้ดูแล (selector 0xc4452e52) ตามที่อยู่

 

ระดับ 0% และการปั่นปริมาณ

เมื่อดูธุรกรรม 21,086 รายการรวมกัน การกระจายแสดงรูปแบบชัดเจน: ระดับ 0% 6,946 รายการ ปริมาณ 12,947,751 ดอลลาร์ เฉลี่ยรายการละ 1,864 ดอลลาร์; ระดับมีค่าธรรมเนียม 14,140 รายการ ปริมาณ 1,120,106 ดอลลาร์ เฉลี่ยรายการละ 79 ดอลลาร์ ธุรกรรมขนาดใหญ่กระจุกในระดับ 0% ค่าธรรมเนียมกระจุกในธุรกรรมขนาดเล็ก คำอธิบายที่สมเหตุสมผลที่สุดสำหรับ 0% ขนาดใหญ่คือผู้ deploy ใช้ gas สูงซื้อขายกับตัวเอง: gas สูงตรงกับการตัดสินเดียวกับราคาเสนอ aggregator ต้นทุนค่าธรรมเนียมของการซื้อขายกับตัวเองใกล้ศูนย์ ปริมาณที่ปั่นขึ้นดันพูลติดอันดับบนเว็บข้อมูลตลาด (ภาพ snapshot แสดงปริมาณ 24 ชม. ประมาณ 10.82 ล้านดอลลาร์ 16,671 รายการ) ในสายตา aggregator นี่คือพูลที่มีความลึกดี ค่าธรรมเนียมต่ำ

 

ปรับระดับค่าธรรมเนียมตามต้องการ

การตัดสินที่สามกล่าวถึงค่าธรรมเนียมในอดีต 19 ระดับ มาจากฟังก์ชันตั้งค่าค่าธรรมเนียมที่ decompile ได้ (selector 0x4d909a45 ไม่มีลายเซ็นสาธารณะ):

 

 

ระดับค่าธรรมเนียมสี่ระดับและเกณฑ์ถูกแพ็ครวมกัน เขียนลง storage slot keccak(poolId, 2) การทดสอบบนเชนอ่านค่า slot นี้ได้ 0x0138800186a00186a0 ตรงกับรูปแบบการแพ็คของฟังก์ชันทีละส่วน ระดับค่าธรรมเนียมที่นี่เป็นพารามิเตอร์ที่ปรับได้ตลอดเวลา ไม่ได้ถูกกำหนดตายตัวตอน deploy

 

วงจรชีวิต

พูลนี้สร้างเมื่อ 08-22 06:57 ธุรกรรม swap แรก 07:13 ธุรกรรมตัวอย่างของบทความนี้ 21:39; ธุรกรรมสุดท้าย 08-23 12:25 จากนั้นถูกปิด ช่วงเวลาที่ใช้งานทั้งหมดประมาณ 29 ชั่วโมง ปริมาณรวม 14,067,857 ดอลลาร์ รายได้ค่าธรรมเนียม 131,888 ดอลลาร์ (ยังไม่หักต้นทุนผู้ deploy) ตอนตรวจสอบซ้ำ ธงการลงทะเบียนกลับมาเป็นเปิดใช้งาน—พูลยังอยู่ พร้อมเปิดใช้งานใหม่ได้ทุกเมื่อ

 

05 เส้นทางทั้งหมด

เชื่อมต่อห่วงโซ่การโจมตีทั้งหมด:

 

 

เหตุและผลทั้งหมดของห่วงโซ่อยู่ในภาพข้างบน สามเงื่อนไขขาดไม่ได้: การจำลองตรงกับสองชั้นแรก ธุรกรรมจริงตกชั้นที่สาม บัฟเฟอร์ใหญ่กว่าค่าธรรมเนียม ถ้าเงื่อนไขใดไม่เป็นจริง วิธีการนี้จะล้มเหลว

 

06 การแก้ไขและข้อเสนอแนะ

จุดแรกอยู่ที่วิธีการเสนอราคาของ aggregator สาเหตุหลักคือการจำลองและการดำเนินการไม่ใช้เส้นทางเดียวกัน: ราคาเสนอใช้การสอบถามระดับพูลในอุดมคติ พร้อม zero address และ gas สูง; ธุรกรรมจริงมาพร้อมตัวตนจริงและ gas ที่ถูกใช้ไปแล้ว Hook อ่านความแตกต่างเหล่านี้ได้พอดี ค่าธรรมเนียมจึงแยกออกจากกัน วิธีแก้คือทำให้การจำลองใกล้เคียงการดำเนินการ—ใช้ calldata ของ Router ที่จะส่งจริง from/to จริง และ gas ใกล้เคียงธุรกรรมบนเชนในการจำลอง; หลังทำธุรกรรม ถอดรหัสเหตุการณ์ Swap เพื่อตรวจสอบ fee ถ้าไม่ตรงกับราคาเสนอให้ลดน้ำหนักหรือถอดออก

จุดที่สองอยู่ที่การตั้งค่า slippage ของผู้ใช้ สาเหตุหลักคือ min_out ตั้งหลวมเกินไป: บัฟเฟอร์ 20.32% กลืนค่าธรรมเนียม 12.8% ทั้งหมด ธุรกรรมยังสำเร็จตามปกติ เส้นทาง stablecoin หลัก 0.1%–1% เพียงพอสำหรับการใช้งานประจำวัน ก่อนเริ่มธุรกรรมให้ดูตัวเลขนี้; กระเป๋าเงินและ frontend/backend ควร收紧ค่าเริ่มต้น ช่วยกันความเสี่ยงส่วนใหญ่ให้ผู้ใช้

จุดที่สามอยู่ที่การคัดกรองเส้นทาง สาเหตุหลักคือพูลค่าธรรมเนียมไดนามิกใดๆ ก็เข้าร่วมแข่งขันราคาเสนอได้โดยตรง: Hook ที่ไม่เปิดซอร์ส ไม่มีบันทึกการตรวจสอบ ก็เข้าสู่เส้นทางแนะนำได้ด้วยปริมาณที่ปั่นขึ้น สำหรับพูลค่าธรรมเนียมไดนามิกที่ไม่มี profile ที่เชื่อถือได้ ควรไม่เข้าสู่เส้นทางโดยค่าเริ่มต้นหรือลดน้ำหนักอย่างมาก เป็นการจัดการที่ปลอดภัยที่สุด

 

ข้อมูลและคำอธิบาย

·ธุรกรรมตัวอย่าง:

https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (บล็อก 117497524)

·ธุรกรรมเริ่มต้นพูล:

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

·PoolManager:

0x28e2ea090877bf75740558f6bfb36a5ffee9e9df

·Pool ID:

0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea

·Hook:

0xd111b3ddd92e627f1864520c770e913ec04e0880 (ไม่มีซอร์สโค้ด โค้ดเทียมเป็นผลการ decompile อิสระของบทความนี้; ผู้ดูแล 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)

·ปริมาณและค่าธรรมเนียม: สะสมเหตุการณ์ Swap ทั้งหมด 21,086 รายการของพูลนี้ทีละรายการ (eth_getLogs) WBNB แปลงตามราคาดำเนินการของรายการเดียวกัน; รายได้เป็นยอดรวม ยังไม่หักต้นทุนผู้ deploy

เนื้อหานี้จัดทำขึ้นโดยมีวัตถุประสงค์เพื่อแจ้งข้อมูลและให้ความรู้เท่านั้น และไม่ถือว่าเป็นคำแนะนำด้านการลงทุนที่เกี่ยวข้องกับ BTCC แต่อย่างใด BTCC ใช้ความพยายามอย่างเต็มที่ แต่ไม่สามารถรับประกันความจริงแท้ ความถูกต้อง หรือความเป็นต้นฉบับของเนื้อหาข้างต้นได้

แนะนำ

ฟื้นคืนชีพระบบนิเวศ Solana: Pump.fun เปิดตัวการจับคู่ Meme หุ้น-เหรียญราคา Bitcoin (BTC): BTC ยังต่ำกว่าแนวต้าน 80,000 ดอลลาร์ ก่อนรายงาน CPIราคา Bitcoin (BTC) อาจแตะ 400,000 ดอลลาร์ภายในปี 2030 ซีอีโอ Coinbase กล่าวบิตคอยน์ (BTC) ร่วงแตะ 77,000 ดอลลาร์ ท่ามกลางเงินเฟ้อพุ่ง น้ำมันแตะ 100 ดอลลาร์ และ ETF ไหลออก — อะไรจะเกิดขึ้นต่อไป?DeFi ที่ไม่กระจายศูนย์ต้องจดทะเบียน เกณฑ์ 60 เสียง: กฎหมาย CLARITY Act ฉบับใหม่เปลี่ยนกฎสำคัญอะไรบ้าง?