- vừa được xem lúc

Làm thế nào để upgrade một smart contract trên ethereum ? (Phần 1)

0 0 101

Người đăng: thiên thần gãy cánh

Theo Viblo Asia

1. Tại sao cần phải upgrade smart contract?

Một smart contract một khi đã được deploy lên network thì nó sẽ tồn tại vĩnh viễn trên đó và không thể nào sửa đổi được. Đây chính ưu điểm lớn nhất của Blockchain, tuy nhiên, trong một số trường hợp nó lại là hạn chế.

Chẳng hạn như một lập trình viên phát hiện ra có lỗ hổng trong logic smart contract của mình, nó có thể bị hacker khai thác và gây thiệt hại cho anh ta, bây giờ anh ta muốn tìm cách làm thế nào để vừa có thể vá được lỗ hổng vừa giữ được toàn bộ dữ liệu của smart contract đó.

Theo tư duy thông thường, anh ta sẽ backup hết dữ liệu của smart contract cũ về một nơi nào đó ngoài network, deploy một smart contract mới đã vá lỗ hổng, sau đó migrate lại dữ liệu đã backup lên nó, nhưng cách này có những hạn chế sau:

  • việc này chỉ khả thi khi dữ liệu tương đối ít
  • bắt buộc smart contract mới phải có function để gọi migrate dữ liệu => gây rối cho smart contract
  • tốn nhiều gas

Như vậy, làm thế nào để một smart contract đã được deploy network, sau khi chạy một thời gian đã lưu rất nhiều dữ liệu trên đó, nếu đột nhiên phát hiện có lỗ hổng trong logic thì vẫn có thể cho smart contract chạy theo một logic khác an toàn hơn mà không cần deploy lại một bản mới rồi migrate dữ liệu? Người ta đã nghiên cứu ra một cách rất hay dựa trên tính năng của hàm delegatecall() để làm điều này.

2. Cách delegatecall cập nhật storage của contract

Như các bạn đã biết, trong solidity có nhiều cách gọi low-level call từ contract A đến contract B gồm:

  • call()
  • staticcall()
  • delegatecall()

Trong đó, delegatecall() hoạt động theo một cách đặc biệt, có thể hiểu như sau: Khi một contract X thực hiện delegatecall() đến một contract Y thì nó sẽ thực hiện logic của contract Y nhưng dưới contextstorage của contract X. Ví dụ có mã nguồn bên dưới:

// Contract A
pragma solidity >=0.6.2 <0.8.0; contract A { address public sender; uint256 public balance; function delegateCallToB(address _contractLogic, uint256 _balance) external { (bool success, ) = _contractLogic.delegatecall(abi.encodePacked(bytes4(keccak256("setBalance(uint256)")), _balance)); require(success, "Delegatecall failed"); }
} 
// Contract B
pragma solidity >=0.6.2 <0.8.0; contract B { address public sender; uint256 public balance; function setBalance(uint256 _balance) external { sender = msg.sender; balance = _balance; }
} 

cả 2 contract A và B đều khai báo biến là _balances, ở contract A có function là delegateCallToB sẽ delegatecall() đến function setBalance() của B.

Ta deploy 2 contract lên network bằng Remix sau đó:

  • dùng một user_addressnào đó (có địa chỉ 0x8f287eA4DAD62A3A626942d149509D6457c2516C chẳng hạn) gọi đến function delegateCallToB() của A với các tham số là địa chỉ deploy của B và mốt số nguyên là 10 chẳng hạn => delegateCallToB(B_contract_address, 10).
  • truy vấn senderbalance của B, kết quả là 0x00000000000000000000000000000000000000000.
  • truy vấn senderbalance của A, kết quả là 0x8f287eA4DAD62A3A626942d149509D6457c2516C10.

Nếu bây giờ ta deploy thêm một contract C có mã nguồn như sau:

pragma solidity >=0.6.2 <0.8.0; contract C { address public sender; uint256 public balance; function setBalance(uint256 _balance) external { sender = msg.sender; balance = balance + _balance * 2; }
}

Thực hiện:

  • gọi function delegateCallTo(B) của A với các tham số là C_contract_address3 => delegateCallToB(C_contract_address, 3).
  • truy vấn senderbalance của C, kết quả là 0x00000000000000000000000000000000000000000.
  • truy vấn senderbalance của A, kết quả là 0x8f287eA4DAD62A3A626942d149509D6457c2516C16.

Như vậy, điều đó hoàn toàn đúng với tính chất của delegatecall() mà ta đã nói ở trên.

Khi một contract X thực hiện delegatecall() đến một contract Y thì nó sẽ thực hiện logic của contract Y nhưng dưới contextstorage của contract X

3. Triển khai một upgradeable contract theo cách Inherited Storage

Như vậy từ tính chất của delegatecall() đã mở ra cho ta khả năng upgrade một contract. Tuy nhiên để triển khai như thế nào cho nó hợp lý là một vấn đề khác.

Thực chất, cách mà delegatecall() tác động lên storage của contract X là nó tác động lên theo thứ tự của slot tương ứng giữa X và Y. Ví dụ có contract D như sau:

pragma solidity >=0.6.2 <0.8.0; contract D uint256 public balance; address public sender; function setBalance(uint256 _balance) external { sender = msg.sender; balance = _balance; }
}

thứ tự khai báo 2 biến balancesender của D khác so với A, B, C. Thực hiện:

  • gọi function delegateCallToB(B) của A với các tham số là D_contract_address10 => delegateCallToB(D_contract_address, 10)
  • truy vấn senderbalance của D, kết quả là 0x00000000000000000000000000000000000000000
  • truy vấn senderbalance của A, kết quả là 0x000000000000000000000000000000000000000A (kết quả của address(10)) và 817288742280969564811718162174206570979710357868 (kết quả của uint256(0x8f287eA4DAD62A3A626942d149509D6457c2516C)). Đáng lẽ phải là sender = 0x8f287eA4DAD62A3A626942d149509D6457c2516C và balance = 10 ?

Lý do là, giữa contract A và contract D do bị xung đột về thứ tự khai báo các biến nên dẫn đến kết quả bị sai. Như vậy, khi muốn triển khai một contract có thể upgrade (upgradeable contract) chúng ta phải nhất quán thứ tự khai báo các biển giữ contract đóng vai trò là storage (contract A) và contract đóng vai trò là logic (contract B, C, D). Có thể triển khai ngắn gọn lại như sau:

pragma solidity >=0.6.2 <0.8.0; contract StorageContract { address public sender; uint256 public balance;
} contract A is StorageContract { function delegateCallToB(address _contractLogic, uint256 _balance) external { (bool success, ) = _contractLogic.delegatecall(abi.encodePacked(bytes4(keccak256("setBalance(uint256)")), _balance)); require(success, "Delegatecall failed"); }
} contract B is StorageContract { function setBalance(uint256 _balance) external { sender = msg.sender; balance = _balance; }
} contract C is StorageContract { function setBalance(uint256 _balance) external { sender = msg.sender; balance = balance + _balance * 2; }
} contract D is StorageContract { function setBalance(uint256 _balance) external { sender = msg.sender; balance = _balance; }
}

Cách triển khai này được gọi là Inherited Storage. Tuy nhiên nó một số khuyết điểm:

  • Các hợp đồng logic có thể kế thừa một storage mà trong đó có thể có những biến nó không sử dụng tới.
  • Các hợp đồng logic bắt buộc phải liên kết chặt chẽ với hợp đồng storage
  • Khó khăn trong việc quản lý các biển private
  • Dễ xảy ra xung đột lưu trữ
  • Bắt buộc storage contract phải có đủ các function get-set cần thiết

Ngoài ra còn một cách có thể dùng để triển khai một upgradeable contract tương tự như Inherited StorageEternal Storage các bạn có thể tham khảo thêm tại đây.

Kết thúc phần 1, mình muốn giới thiệu cho các bạn về:

  • lý do tại sao phải upgrade contract
  • delegatecall() là kỹ thuật chính mà người ta vận dụng để làm cho một contract có thể upgrade (upgradeable contract)
  • cách triển khai đơn giản nhất là Inherited Storage.

Sang phần 2, mình sẽ đi sau vào cách tiếp cách Unstructured Storage, đã được Openzeppelin chuẩn hóa và publish trên github của mình.

Bình luận

Bài viết tương tự

- vừa được xem lúc

[Blockchain] Road to Bitcoin

. Chắc mọi người hẳn đã không còn xa lạ gì với anh chàng tỷ phú đã ném vỡ cửa kính ô tô nhà mình cùng với siêu năng lực điều khiển vật giá chỉ bằng lời nói, người đã đẩy định giá Bitcoin trên thị trường vượt ngưỡng 50K dolar/coin với những bài twitter để đời . .

0 0 61

- vừa được xem lúc

Khi Ethereum có chi phí giao dịch quá đắt đỏ - Tương lai cho layer2 ?

Với sự phát triển như vũ bão của Blockchain, ETH dường như đang quá tải và hệ quả là chi phí Gas đã lên đến 1000Gwei, phí để tạo những transaction phức tạp đã xấp xỉ 500$ . Và một giải pháp cứu cánh cho các sản phẩm Defi trên ETH chính là Layer2, và trong nhiệm vụ lần này Matic đang thể hiện khả năn

0 0 89

- vừa được xem lúc

Blockchain với Java - Tại sao không?

Cuộc cách mạng công nghiệp 4.0 ra đời kéo theo nhiều sự thay đổi và xu hướng mới được hình thành. Riêng đối với lĩnh vực CNTT cũng không nằm ngoài vùng ảnh hưởng mạnh mẽ. Chính làn sóng 4.

0 0 92

- vừa được xem lúc

Phân loại và tầm quan trọng của các node trong mạng blockchain

Trước khi đi vào phân loại và nêu rõ được tầm quan trọng của các node trọng mạng blockchain thì mình xin được trích dẫn khái niệm về blockchain từ Wikipedia như sau:. .

0 1 65

- vừa được xem lúc

Code Smart Contract bằng Assembly ?

Introduction. Hồi còn học trong ghế nhà trường bộ môn lập trình tốn nhiều não nhất của mình là code assembly. Nôm na thì bất cứ ngôn ngữ bậc cao nào như C , Go, Java,... được sinh ra để người dễ hiểu và dễ code , tuy nhiên chúng đều sẽ được compiled down xuống assembly một ngôn ngữ bậc thấp để máy h

0 0 58

- vừa được xem lúc

Dextool - Công cụ phân tích Decentralized Exchange tuyệt vời

. Trend Defi mặc dù đã bớt nhiệt nhưng những sản phẩm nổi bật của làn sóng này mang lại thì vẫn rất được người dùng ưa chuộng. Đặc biệt là các nền tảng Decentralized Exchange, tiêu biểu là Uniswap, SushiSwap, 1inch Exchange, FalconSwap,... Nhưng khi đã sử dụng các nền tảng DEx này mà không biết đến

0 0 106