- Intro
- Tạo địa chỉ tất định (deterministic address) với
CREATE2
- Thử nghiệm đầu tiên: Entry Point deploy contract bất kì
- Thử nghiệm tốt hơn: Factory
- Tham khảo
Intro
Có một điều mà chúng ta chưa đề cập đến là wallet được tạo ra như thế nào? Cách “truyền thống” là sử dụng một EOA để deploy wallet contract lên trên mạng blockchain. Nó thực sự là một phương án tồi, vì chúng ta đã đi cả một chặng đường dài ở các phần trước để xây dựng một hệ thống có thể hoạt động mà không cần chuẩn bị thêm một EOA riêng biệt để kích hoạt giao dịch nữa. Nếu ta vẫn cần EOA ở thời điểm ban đầu khi tạo wallet, thì rõ ràng những điều ta xây dựng sau đó là vô nghĩa.
Ta sẽ làm rõ lại những gì ta muốn làm: một user chưa có wallet sẽ muốn có một wallet mới, on-chain, có thể tự trả phí gas bằng ETH hoặc tìm một paymaster để trả hộ, toàn bộ quá trình này không cần phải tạo thêm một EOA nào.
Khi ta tạo một EOA, ta có thể generate private key tại local và sở hữu account mà không cần send một transaction nào cả. Ta có thể share địa chỉ của ta cho người khác để nhận ETH hay token trước khi ta thực hiện bất cứ một transaction nào.
Ta cũng muốn việc tạo smart contract wallet của chúng ta trở nên tương tự như vậy, có nghĩa là ta cũng sẽ khả năng tạo ra và sở hữu wallet ngay dưới local, ta cũng có thể share địa chỉ này cho người khác để nhận token trước khi ta thực sự thực hiện một transaction.
Đấy là lúc ta cần đến CREATE2
.
Tạo địa chỉ tất định (deterministic address) với CREATE2
Một địa chỉ contract đã được tính toán trước nhưng chưa deploy lên được gọi là một counterfactual address.
Với CREATE2
, dù ta chưa thực sự deploy contract, nhưng từ các dữ liệu đầu vào ta đã tính toán được địa chỉ của nó, gọi là địa chỉ tất định (deterministic address). Và lúc này ta hoàn toàn đã có thể sử dụng địa chỉ này để nhận token được.
Đầu vào của CREATE2
bao gồm:
- Địa chỉ của contract gọi
CREATE2
- Một giá trị
salt
, là một chuỗi 32-bytes bất kì init code
của contract sẽ được deploy. Init code này là một dãy bytecode mà sau khi được thực hiện trên EVM, nó sẽ trả về một dãy các bytecode khác, và dãy trả về này sẽ được lưu tại blockchain, trở thành một contract mới được deploy.
Có thể bạn không để ý: khi deploy một contract, code thực sự được lưu trên blockchain sẽ không giống với code mà ta đã submit lên.
Hơn thế nữa, sử dụng cùng một init code
nhiều lần cũng không đảm bảo rằng contract được deploy lên sẽ luôn có code giống nhau, vì init code
có thể đọc từ storage các biến số thay đổi theo thời gian, ví dụ TIMESTAMP
chẳng hạn.
Thử nghiệm đầu tiên: Entry Point deploy contract bất kì
Giờ đây ta biết cách sử dụng của CREATE2
, ta sẽ thử nghiệm cho phép user thêm init code vào operation và nhờ Entry Point tiến hành deploy contract nếu nó chưa tồn tại.
Đầu tiên ta sẽ add một trường mới vào bên trong User Operation:
struct UserOperation {
// ...
bytes initCode;
}
Sau đó, chúng ta sẽ update phần validation của Entry Point trong handleOps
như sau: Với mỗi khi validate một op, nếu op có dữ liệu cho trường initCode
, thì sử dụng CREATE2
để deploy một contract với initCode
đó. Phần còn lại của validation thì vẫn tiến hành như bình thường:
- Gọi
validateOp
tại wallet mới được tạo. - Sau đó nếu nó có paymaster, gọi
validatePaymasterOp
tại paymaster.
Đây có thể coi là một thử nghiệm khá ổn! Nó hoàn tất được tất cả các nhiệm vụ ta cần: user có thể deploy bất kì contract nào và có thể biết trước được địa chỉ của contract trước khi nó được deploy, quá trình deployment cũng có thể được sponsored bởi paymaster, hoặc user có thể tự trả tiền (bằng cách gửi ETH vào địa chỉ contract trước khi nó được deploy).
Nhưng có một rủi ro rất lớn ở đây: initCode
là mã code bất kì, nên ta không thể chắc chắn được rằng nó có phải là mã độc hại hay không?
- Khi paymaster nhìn vào user op, nó không thể phân tích được chuỗi bytecode này làm gì, và nó có nên trả tiền cho nó hay không?
- Khi user submit chuỗi bytecode để deploy một contract, họ cũng không thể thực sự kiểm tra được chuỗi bytecode này có làm đúng thứ họ muốn hay không? Nếu user sử dụng một công cụ của bên thứ 3 để deploy, nếu đây là một tool độc hại hoặc bị hack, thì có thể dẫn đến user sẽ deploy một contract độc hại (ví dụ backdoor). Để hạn chế được điều này thực sự rất khó.
initCode
là chuỗi bytecode bất kì, nên nó hoàn toàn có khả năng gây ra trường hợp success lúc validation nhưng fail lúc execution mà ta đã nói rất nhiều ở các bài trước.
Ta cần một giải pháp để user có thể an toàn khi deploy wallet contract, và những thành phần khác trong hệ thống cũng có thể được đảm bảo khi tương tác với các bản depoy này.
Ta sẽ giới thiệu contract mới: Factory
.
Thử nghiệm tốt hơn: Factory
Thay vì để Entry Point nhận initCode
bất kì và thực hiện CREATE2
, ta cho phép user có quyền lựa chọn một contract để gọi CREATE2
thay thế. Ta gọi những contract này là Factory
, được sử dụng chuyên biệt để tạo ra những wallet contract khác nhau.
Ví dụ factory này tạo ra multisig wallet contract yêu cầu 2/2 keys để mở khóa giao dịch, factory kia thì tạo ra 3/5 keys multisig wallet…
Factory sẽ có một method được gọi khi tạo contract:
contract Factory {
function deployContract(bytes data) returns (address);
}
Hàm
deployContract
trả về địa chỉ của contract mới tạo ra, do đó user có thể simulate hàm này để biết được địa chỉ sẽ được tạo ra trước khi deploy. Điều này cũng đạt được mục tiêu ban đầu ta đã đề ra.
Chúng ta cũng sẽ cần thêm những trường mới vào bên trong User Operation để dùng cho việc deploy wallet:
struct UserOperation {
// ...
address factory;
bytes factoryData;
}
Bằng cách này, ta đã giải quyết được các vấn đề ở bên trên:
- Khi user tạo một wallet mới từ factory wallet, user sẽ có quyền lựa chọn những trusted factory, input/output xác định rõ ràng, nên ta sẽ luôn có một wallet an toàn, không có backdoors, và hoàn toàn không phải review bytecode như trước nữa.
- Paymaster có thể lựa chọn chỉ trả tiền cho những deployment từ những factory được nó đồng ý.
Vấn đề cuối cùng giống hệt với vấn đề mà ta đã gặp phải với validatePaymasterOp
trong paymaster, và ta cũng sẽ xử lý tương tự như vậy.
Bundler sẽ giới hạn factory chỉ có thể truy cập các associated storage của nó và contract mà nó đang deploy mà thôi, đồng thời cũng ban đi như opcodes thay đổi theo thời gian như TIMESTAMP
, BLOCKHASH
…
Chúng ta cũng sẽ yêu cầu các factory phải join vào reputation system
bằng cách stake một lượng ETH giống như paymaster.
Ta cũng có trường hợp ngoại lệ tương tự như với paymaster, factory sẽ không cần phải stake nếu deployment method chỉ truy cập associated storage của wallet nó đang deploy, mà không truy cập associated storage của chính factory.
Ta đã xong với wallet creation!
Tại thời điểm này, với kiến trúc này ta đã có thể thực hiện toàn bộ những features trong ERC-4337!
Phần 4 cuối cùng ta sẽ đi vào triển khai aggregating signatures
, để cung cấp khả năng tối ưu hóa gas cho các giao dịch.