ข้ามไปยังเนื้อหา

การชนกันของพื้นที่จัดเก็บพร็อกซี: การอัปเกรดทำลายสถานะสัญญาอย่างไร

พร็อกซีเก็บสถานะไว้ขณะที่โค้ด implementation เปลี่ยน เรียนรู้ว่า layout ที่ไม่เข้ากันเขียนทับยอดคงเหลือ owner และการควบคุมอย่างไร พร้อมวิธีตรวจสอบการอัปเกรด

อัปเดต

จัดทำขึ้นเพื่อการศึกษาเท่านั้น ไม่ใช่คำแนะนำการลงทุน การลงทุนอาจทำให้สูญเสียเงินลงทุนได้

คำตอบโดยตรง

การชนกันของพื้นที่จัดเก็บพร็อกซีเกิดขึ้นเมื่อโค้ด 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 และพื้นที่จัดเก็บจากการสืบทอด

หัวข้อที่เกี่ยวข้อง

แหล่งที่มา

การนำทาง

ค้นหาในวิกิ...