จัดทำขึ้นเพื่อการศึกษาเท่านั้น ไม่ใช่คำแนะนำการลงทุน การลงทุนอาจทำให้สูญเสียเงินลงทุนได้
คำตอบโดยตรง
การชนกันของพื้นที่จัดเก็บพร็อกซีเกิดขึ้นเมื่อโค้ด implementation อ่านหรือเขียนสล็อตของพร็อกซีด้วยความหมายที่ต่างจาก layout ที่สร้างสถานะเดิม การอัปเกรดจึงอาจทำให้ยอดคงเหลือถูกตีความเป็นแอดเดรส ล้างค่า owner ทำลายสล็อตฐานของ mapping หรือเขียนทับข้อมูลควบคุมการอัปเกรด
เมื่อใช้ delegatecall ไบต์โค้ดของ implementation จะทำงานในบริบทของพร็อกซี พื้นที่จัดเก็บ ยอดคงเหลือ และ address(this) เป็นของพร็อกซี ชื่อตัวแปรไม่ได้ถูกเก็บไว้บนเชน ดังนั้น EVM จึงทำตามเฉพาะสล็อตและออฟเซ็ตไบต์ที่โค้ดใหม่คำนวณ
ด้วยเหตุนี้ การอัปเกรดต้องรักษา layout ที่ติดตั้งอยู่ ไม่ใช่เพียงคอมไพล์ผ่านหรือเปิดเผยฟังก์ชันเดิม ก่อนอนุมัติการอัปเกรด ให้เปรียบเทียบ layout ที่คอมไพเลอร์สร้างกับเวอร์ชันที่ติดตั้งจริงอย่างถูกต้อง
วิธีการทำงาน
โดยทั่วไป Solidity วางตัวแปรสถานะตั้งแต่สล็อต 0 ตามลำดับการประกาศหลังทำ C3 linearization ของการสืบทอด ค่าเล็กกว่า 32 ไบต์อาจใช้สล็อตร่วมกันได้ struct และ array มีกฎเพิ่มเติม ส่วน mapping และ dynamic array คำนวณตำแหน่งข้อมูลจากสล็อตฐาน หากสล็อตฐานเลื่อน ตำแหน่งข้อมูลที่คำนวณจากสล็อตนั้นก็เปลี่ยนด้วย
มีขอบเขตการชนกันสี่ประเภทที่ต้องตรวจสอบ:
- พร็อกซีกับ implementation: ฟิลด์ของพร็อกซี เช่น implementation หรือ administrator ต้องไม่ใช้สล็อตเดียวกับสถานะของแอปพลิเคชัน ERC-1967 กำหนดสล็อตมาตรฐานที่หลีกจากการจัดสรรปกติของคอมไพเลอร์สำหรับ implementation, beacon และ administrator
- implementation เก่ากับใหม่: ตัวแปรเดิมต้องคงสล็อต ออฟเซ็ต และชนิดที่เข้ากันได้ การเพิ่มตัวแปรต่อท้ายอาจปลอดภัย แต่การแทรก สลับลำดับ ลบ หรือเปลี่ยนชนิด อาจทำให้ word เดิมถูกตีความใหม่
- การสืบทอด: การเพิ่มสถานะในสัญญาฐานหรือเปลี่ยนลำดับการสืบทอดอาจเลื่อนพื้นที่จัดเก็บของสัญญาลูก แม้ซอร์สโค้ดของสัญญาลูกไม่เปลี่ยน
- พื้นที่สำรองหรือแบบ namespace: storage gap ที่ใช้อย่างถูกต้องสำรองพื้นที่ให้สัญญาฐานได้ และ namespace แบบ ERC-7201 แยก layout ได้ แต่ทั้งสองวิธีไม่อนุญาตให้แก้ไขภายใน layout เดิมโดยพลการ
ตัวอย่าง
สมมติว่าเวอร์ชัน 1 มี layout ดังนี้:
uint256 totalAssets; // slot 0
address owner; // slot 1เวอร์ชัน 2 แทรกตัวแปรไว้ด้านหน้าอย่างไม่ถูกต้อง:
bool paused; // slot 0, offset 0
uint256 totalAssets; // slot 1
address owner; // slot 2หลังอัปเกรด paused จะอ่านไบต์ล่างของ totalAssets เดิม totalAssets ใหม่จะอ่าน word ของ owner เดิมเป็นจำนวนเต็ม และ owner จะอ่านค่าที่มีอยู่ในสล็อต 2 ซึ่งมักเป็นศูนย์ word เดิมยังอยู่ในพื้นที่จัดเก็บ แต่โค้ดใหม่ให้ความหมายต่างออกไป ธุรกรรมอาจสำเร็จทั้งที่ใช้การอนุญาตหรือการบัญชีกับสถานะที่ถูกตีความผิด
ความเสี่ยงและการตรวจสอบการอัปเกรด
- สร้างผลลัพธ์ storage layout ของ implementation ทั้งสอง แล้วเปรียบเทียบสล็อต ออฟเซ็ต ชนิด และการสืบทอดกับสัญญาอ้างอิงที่ติดตั้งจริง
- สำหรับ layout เชิงเส้นแบบเดิม ให้เพิ่มตัวแปรใหม่ต่อท้ายเท่านั้น อย่าสลับฟิลด์ เปลี่ยนชนิด ลบแล้วนำกลับมาใช้ หรือแก้สัญญาฐานโดยไม่พิสูจน์ความเข้ากันได้
- เมื่อใช้ storage gap ให้ลดขนาดตามจำนวนสล็อตสำรองที่ใช้จริงอย่างแม่นยำ สำหรับ namespace ให้ใช้ตัวระบุที่ไม่ซ้ำและตรวจสอบการเปลี่ยนแปลงในทุก namespace เดิม
- อย่าถือว่า ERC-1967 ป้องกันได้ทั้งหมด มาตรฐานนี้แยก metadata ของพร็อกซีออกจากสล็อตแอปพลิเคชันที่คอมไพเลอร์จัดสรร แต่ไม่ได้ทำให้ layout ของ implementation สองชุดเข้ากันได้
- ทดสอบการอัปเกรดและ reinitializer บน fork หรือ snapshot ของสถานะ ตรวจสอบ owner, role, ยอดคงเหลือ, allowance, รายการ mapping, สถานะ pause, สล็อต implementation และการควบคุม rollback หรือฉุกเฉิน ทั้งก่อนและหลังทำงาน
หากสงสัยว่าการอัปเกรดทำให้เกิดการชนกันแล้ว ให้หยุดการอัปเกรดเพิ่มเติมและการเรียกที่เปลี่ยนสถานะเมื่อกลไกกำกับดูแลอนุญาต เก็บหมายเลขบล็อกและไบต์โค้ดก่อนอัปเกรด เปรียบเทียบข้อมูลดิบในสล็อตที่ได้รับผลกระทบ และให้วิศวกรสัญญาที่มีคุณสมบัติออกแบบการย้ายพร้อมรับการตรวจสอบอิสระ การอัปเกรดซ้ำโดยไม่มีแผนผังพื้นที่จัดเก็บที่พิสูจน์แล้วอาจทำลายสถานะที่ยังกู้คืนได้มากขึ้น
ความเข้าใจผิดที่พบบ่อย
- “ชื่อตัวแปรไม่เปลี่ยน ดังนั้น layout จึงปลอดภัย” ชื่อไม่ได้กำหนดตำแหน่งพื้นที่จัดเก็บ ชนิด ลำดับ การแพ็ก การสืบทอด และกฎ namespace เป็นตัวกำหนด
- “การลบตัวแปรทำให้สล็อตว่างสำหรับใช้ใหม่” พื้นที่จัดเก็บของพร็อกซียังคงอยู่ การใช้ซ้ำคือการให้ความหมายใหม่แก่ word เดิม เว้นแต่การย้ายที่ผ่านการตรวจสอบจะล้างหรือแปลงค่านั้น
- “ธุรกรรมทดสอบที่สำเร็จพิสูจน์ว่าอัปเกรดเข้ากันได้” การทดสอบอาจแตะเพียงไม่กี่สล็อต การตรวจ layout และความต่างของสถานะต้องครอบคลุมฟิลด์สิทธิ์สูง ค่าที่แพ็ก mapping, array และพื้นที่จัดเก็บจากการสืบทอด
หัวข้อที่เกี่ยวข้อง
- ความเสี่ยงของพื้นที่จัดเก็บจาก delegatecall
- การยึด initializer
- สัญญาพร็อกซี
- การติดตามการอัปเกรดพร็อกซี
- สัญญาที่อัปเกรดได้
แหล่งที่มา
- บทนำสู่สัญญาอัจฉริยะ - Solidity Documentation (เข้าถึง: 2026-08-21)
- Layout ของตัวแปรสถานะในพื้นที่จัดเก็บ - Solidity Documentation (เข้าถึง: 2026-08-21)
- ERC-1967: สล็อตพื้นที่จัดเก็บพร็อกซี - Ethereum Improvement Proposals (เข้าถึง: 2026-08-21)
- การเขียนสัญญาที่อัปเกรดได้ - OpenZeppelin Documentation (เข้าถึง: 2026-08-21)