Hướng dẫn hợp đồng cầu nối tiêu chuẩn Optimism
Optimismopens in a new tab là một gộp giao dịch lạc quan. Các gộp giao dịch lạc quan có thể xử lý các giao dịch với mức giá thấp hơn nhiều so với Ethereum Mainnet (còn được gọi là lớp 1 hoặc L1) vì các giao dịch chỉ được xử lý bởi một vài nút, thay vì mọi nút trên mạng. Đồng thời, tất cả dữ liệu được ghi vào L1 để mọi thứ có thể được chứng minh và tái tạo lại với tất cả sự đảm bảo về tính toàn vẹn và tính khả dụng của Mainnet.
Để sử dụng tài sản L1 trên Optimism (hoặc bất kỳ L2 nào khác), tài sản cần được bắc cầu. Một cách để thực hiện điều này là người dùng khóa tài sản (ETH và token ERC-20 là những tài sản phổ biến nhất) trên L1, và nhận tài sản tương đương để sử dụng trên L2. Cuối cùng, bất cứ ai sở hữu chúng đều có thể muốn bắc cầu chúng trở lại L1. Khi làm điều này, các tài sản sẽ bị đốt trên L2 và sau đó được trả lại cho người dùng trên L1.
Đây là cách mà cầu nối tiêu chuẩn của Optimismopens in a new tab hoạt động. Trong bài viết này, chúng ta sẽ xem qua mã nguồn của cầu nối đó để xem nó hoạt động như thế nào và nghiên cứu nó như một ví dụ về mã Solidity được viết tốt.
Luồng điều khiển
Cầu nối có hai luồng chính:
- Gửi tiền (từ L1 đến L2)
- Rút tiền (từ L2 đến L1)
Luồng gửi tiền
Lớp 1
- Nếu gửi một ERC-20, người gửi cấp cho cầu nối một khoản phụ cấp để chi tiêu số tiền đang được gửi
- Người gửi gọi cầu nối L1 (
depositERC20,depositERC20To,depositETH, hoặcdepositETHTo) - Cầu nối L1 nắm giữ tài sản được bắc cầu
- ETH: Tài sản được người gửi chuyển như một phần của lệnh gọi
- ERC-20: Tài sản được cầu nối tự chuyển cho chính nó bằng cách sử dụng khoản phụ cấp do người gửi cung cấp
- Cầu nối L1 sử dụng cơ chế thông điệp liên miền để gọi
finalizeDeposittrên cầu nối L2
Lớp 2
- Cầu nối L2 xác minh lệnh gọi đến
finalizeDepositlà hợp lệ:- Đến từ hợp đồng thông điệp liên miền
- Ban đầu đến từ cầu nối trên L1
- Cầu nối L2 kiểm tra xem hợp đồng token ERC-20 trên L2 có phải là hợp đồng chính xác không:
- Hợp đồng L2 báo cáo rằng đối tác L1 của nó giống với hợp đồng mà các token đến từ đó trên L1
- Hợp đồng L2 báo cáo rằng nó hỗ trợ giao diện chính xác (sử dụng ERC-165opens in a new tab).
- Nếu hợp đồng L2 là hợp đồng chính xác, hãy gọi nó để đúc số lượng token thích hợp vào địa chỉ thích hợp. Nếu không, hãy bắt đầu quy trình rút tiền để cho phép người dùng nhận lại các token trên L1.
Luồng rút tiền
Lớp 2
- Người rút gọi cầu nối L2 (
withdrawhoặcwithdrawTo) - Cầu nối L2 đốt số lượng token thích hợp thuộc về
msg.sender - Cầu nối L2 sử dụng cơ chế thông điệp liên miền để gọi
finalizeETHWithdrawalhoặcfinalizeERC20Withdrawaltrên cầu nối L1
Lớp 1
- Cầu nối L1 xác minh lệnh gọi đến
finalizeETHWithdrawalhoặcfinalizeERC20Withdrawallà hợp lệ:- Đến từ cơ chế thông điệp liên miền
- Ban đầu đến từ cầu nối trên L2
- Cầu nối L1 chuyển tài sản thích hợp (ETH hoặc ERC-20) đến địa chỉ thích hợp
Mã lớp 1
Đây là mã chạy trên L1, Ethereum Mainnet.
IL1ERC20Bridge
Giao diện này được định nghĩa ở đâyopens in a new tab. Nó bao gồm các hàm và định nghĩa cần thiết để bắc cầu các token ERC-20.
1// SPDX-License-Identifier: MITHầu hết mã của Optimism được phát hành theo giấy phép MITopens in a new tab.
1pragma solidity >0.5.0 <0.9.0;Tại thời điểm viết bài, phiên bản mới nhất của Solidity là 0.8.12. Cho đến khi phiên bản 0.9.0 được phát hành, chúng tôi không biết liệu mã này có tương thích với nó hay không.
1/**2 * @title IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * Các sự kiện *7 **********/89 event ERC20DepositInitiated(Hiện tất cảTrong thuật ngữ cầu nối Optimism deposit (gửi tiền) có nghĩa là chuyển từ L1 sang L2, và withdrawal (rút tiền) có nghĩa là chuyển từ L2 sang L1.
1 address indexed _l1Token,2 address indexed _l2Token,Trong hầu hết các trường hợp, địa chỉ của một ERC-20 trên L1 không giống với địa chỉ của ERC-20 tương đương trên L2.
Bạn có thể xem danh sách các địa chỉ token ở đâyopens in a new tab.
Địa chỉ có chainId 1 là trên L1 (Mainnet) và địa chỉ có chainId 10 là trên L2 (Optimism).
Hai giá trị chainId còn lại dành cho mạng thử nghiệm Kovan (42) và mạng thử nghiệm Optimistic Kovan (69).
1 address indexed _from,2 address _to,3 uint256 _amount,4 bytes _data5 );Có thể thêm ghi chú vào các giao dịch chuyển tiền, trong trường hợp đó chúng sẽ được thêm vào các sự kiện báo cáo chúng.
1 event ERC20WithdrawalFinalized(2 address indexed _l1Token,3 address indexed _l2Token,4 address indexed _from,5 address _to,6 uint256 _amount,7 bytes _data8 );Cùng một hợp đồng cầu nối xử lý các giao dịch chuyển tiền theo cả hai hướng. Trong trường hợp của cầu nối L1, điều này có nghĩa là khởi tạo gửi tiền và hoàn tất rút tiền.
12 /********************3 * Các hàm công khai *4 ********************/56 /**7 * @dev lấy địa chỉ của hợp đồng cầu nối L2 tương ứng.8 * @return Địa chỉ của hợp đồng cầu nối L2 tương ứng.9 */10 function l2TokenBridge() external returns (address);Hiện tất cảHàm này không thực sự cần thiết, vì trên L2 nó là một hợp đồng được triển khai trước, vì vậy nó luôn ở địa chỉ 0x4200000000000000000000000000000000000010.
Nó ở đây để đối xứng với cầu nối L2, vì địa chỉ của cầu nối L1 không dễ dàng để biết.
1 /**2 * @dev gửi một lượng ERC20 vào số dư của người gọi trên L2.3 * @param _l1Token Địa chỉ của L1 ERC20 mà chúng ta đang gửi4 * @param _l2Token Địa chỉ của ERC20 L2 tương ứng của L15 * @param _amount Lượng ERC20 cần gửi6 * @param _l2Gas Giới hạn gas cần thiết để hoàn tất việc gửi tiền trên L2.7 * @param _data Dữ liệu tùy chọn để chuyển tiếp đến L2. Dữ liệu này được cung cấp8 * chỉ để thuận tiện cho các hợp đồng bên ngoài. Ngoài việc thực thi một9 * độ dài tối đa, các hợp đồng này không cung cấp bất kỳ đảm bảo nào về nội dung của nó.10 */11 function depositERC20(12 address _l1Token,13 address _l2Token,14 uint256 _amount,15 uint32 _l2Gas,16 bytes calldata _data17 ) external;Hiện tất cảTham số _l2Gas là lượng gas L2 mà giao dịch được phép chi tiêu.
Lên đến một giới hạn nhất định (cao), điều này là miễn phíopens in a new tab, vì vậy trừ khi hợp đồng ERC-20 làm điều gì đó thực sự kỳ lạ khi đúc, nó sẽ không phải là một vấn đề.
Hàm này xử lý kịch bản phổ biến, trong đó người dùng bắc cầu tài sản đến cùng một địa chỉ trên một chuỗi khối khác.
1 /**2 * @dev gửi một lượng ERC20 vào số dư của người nhận trên L2.3 * @param _l1Token Địa chỉ của L1 ERC20 mà chúng ta đang gửi4 * @param _l2Token Địa chỉ của ERC20 L2 tương ứng của L15 * @param _to Địa chỉ L2 để ghi có khoản rút tiền.6 * @param _amount Số lượng ERC20 cần gửi.7 * @param _l2Gas Giới hạn gas cần thiết để hoàn tất việc gửi tiền trên L2.8 * @param _data Dữ liệu tùy chọn để chuyển tiếp đến L2. Dữ liệu này được cung cấp9 * chỉ để thuận tiện cho các hợp đồng bên ngoài. Ngoài việc thực thi một10 * độ dài tối đa, các hợp đồng này không cung cấp bất kỳ đảm bảo nào về nội dung của nó.11 */12 function depositERC20To(13 address _l1Token,14 address _l2Token,15 address _to,16 uint256 _amount,17 uint32 _l2Gas,18 bytes calldata _data19 ) external;Hiện tất cảHàm này gần như giống hệt với depositERC20, nhưng nó cho phép bạn gửi ERC-20 đến một địa chỉ khác.
1 /*************************2 * Các hàm liên chuỗi *3 *************************/45 /**6 * @dev Hoàn tất việc rút tiền từ L2 sang L1 và ghi có tiền vào số dư của người nhận7 * token ERC20 L1.8 * Lệnh gọi này sẽ thất bại nếu việc rút tiền đã được khởi tạo từ L2 chưa được hoàn tất.9 *10 * @param _l1Token Địa chỉ của token L1 để finalizeWithdrawal.11 * @param _l2Token Địa chỉ của token L2 nơi việc rút tiền được khởi tạo.12 * @param _from Địa chỉ L2 khởi tạo giao dịch chuyển tiền.13 * @param _to Địa chỉ L1 để ghi có khoản rút tiền.14 * @param _amount Số lượng ERC20 cần gửi.15 * @param _data Dữ liệu do người gửi cung cấp trên L2. Dữ liệu này được cung cấp16 * chỉ để thuận tiện cho các hợp đồng bên ngoài. Ngoài việc thực thi một17 * độ dài tối đa, các hợp đồng này không cung cấp bất kỳ đảm bảo nào về nội dung của nó.18 */19 function finalizeERC20Withdrawal(20 address _l1Token,21 address _l2Token,22 address _from,23 address _to,24 uint256 _amount,25 bytes calldata _data26 ) external;27}Hiện tất cảViệc rút tiền (và các thông điệp khác từ L2 sang L1) trong Optimism là một quy trình hai bước:
- Một giao dịch khởi tạo trên L2.
- Một giao dịch hoàn tất hoặc yêu cầu trên L1. Giao dịch này cần phải xảy ra sau khi thời gian thử thách lỗiopens in a new tab cho giao dịch L2 kết thúc.
IL1StandardBridge
Giao diện này được định nghĩa ở đâyopens in a new tab.
Tệp này chứa các định nghĩa sự kiện và hàm cho ETH.
Các định nghĩa này rất tương tự với những định nghĩa trong IL1ERC20Bridge ở trên cho ERC-20.
Giao diện cầu nối được chia thành hai tệp vì một số token ERC-20 yêu cầu xử lý tùy chỉnh và không thể được xử lý bởi cầu nối tiêu chuẩn.
Bằng cách này, cầu nối tùy chỉnh xử lý một token như vậy có thể triển khai IL1ERC20Bridge và không phải bắc cầu cả ETH.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * Các sự kiện *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );Hiện tất cảSự kiện này gần như giống hệt phiên bản ERC-20 (ERC20DepositInitiated), ngoại trừ không có địa chỉ token L1 và L2.
Điều tương tự cũng đúng với các sự kiện và hàm khác.
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );67 /********************8 * Các hàm công khai *9 ********************/1011 /**12 * @dev Gửi một lượng ETH vào số dư của người gọi trên L2.13 .14 .15 .16 */17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;1819 /**20 * @dev Gửi một lượng ETH vào số dư của người nhận trên L2.21 .22 .23 .24 */25 function depositETHTo(26 address _to,27 uint32 _l2Gas,28 bytes calldata _data29 ) external payable;3031 /*************************32 * Các hàm liên chuỗi *33 *************************/3435 /**36 * @dev Hoàn tất việc rút tiền từ L2 sang L1 và ghi có tiền vào số dư của người nhận37 * token L1 ETH. Vì chỉ có xDomainMessenger mới có thể gọi hàm này, nên nó sẽ không bao giờ được gọi38 * trước khi việc rút tiền được hoàn tất.39 .40 .41 .42 */43 function finalizeETHWithdrawal(44 address _from,45 address _to,46 uint256 _amount,47 bytes calldata _data48 ) external;49}Hiện tất cảCrossDomainEnabled
Hợp đồng nàyopens in a new tab được kế thừa bởi cả hai cầu nối (L1 và L2) để gửi thông điệp đến lớp kia.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34/* Interface Imports */5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";Giao diện nàyopens in a new tab cho hợp đồng biết cách gửi thông điệp đến lớp kia, sử dụng trình nhắn tin liên miền. Trình nhắn tin liên miền này là một hệ thống hoàn toàn khác, và xứng đáng có một bài viết riêng, mà tôi hy vọng sẽ viết trong tương lai.
1/**2 * @title CrossDomainEnabled3 * @dev Hợp đồng trợ giúp cho các hợp đồng thực hiện giao tiếp liên miền4 *5 * Trình biên dịch được sử dụng: được định nghĩa bởi hợp đồng kế thừa6 */7contract CrossDomainEnabled {8 /*************9 * Biến *10 *************/1112 // Hợp đồng Messenger được sử dụng để gửi và nhận thông điệp từ miền khác.13 address public messenger;1415 /***************16 * Hàm dựng *17 ***************/1819 /**20 * @param _messenger Địa chỉ của CrossDomainMessenger trên lớp hiện tại.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }Hiện tất cảTham số duy nhất mà hợp đồng cần biết, địa chỉ của trình nhắn tin liên miền trên lớp này. Tham số này được thiết lập một lần trong hàm dựng và không bao giờ thay đổi.
12 /**********************3 * Bổ ngữ hàm *4 **********************/56 /**7 * @dev Chỉ cho phép hàm được sửa đổi được gọi bởi một tài khoản liên miền cụ thể.8 * @param _sourceDomainAccount Tài khoản duy nhất trên miền gốc được9 * xác thực để gọi hàm này.10 */11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {Hiện tất cảCơ chế nhắn tin liên miền có thể được truy cập bởi bất kỳ hợp đồng nào trên chuỗi khối nơi nó đang chạy (hoặc mainnet Ethereum hoặc Optimism). Nhưng chúng ta cần cầu nối ở mỗi bên chỉ tin cậy một số thông điệp nhất định nếu chúng đến từ cầu nối ở bên kia.
1 require(2 msg.sender == address(getCrossDomainMessenger()),3 "OVM_XCHAIN: messenger contract unauthenticated"4 );Chỉ các thông điệp từ trình nhắn tin liên miền thích hợp (messenger, như bạn thấy bên dưới) mới có thể được tin cậy.
12 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: wrong sender of cross-domain message"5 );Cách mà trình nhắn tin liên miền cung cấp địa chỉ đã gửi một thông điệp với lớp kia là hàm .xDomainMessageSender()opens in a new tab.
Miễn là nó được gọi trong giao dịch được khởi tạo bởi thông điệp, nó có thể cung cấp thông tin này.
Chúng ta cần đảm bảo rằng thông điệp chúng ta nhận được đến từ cầu nối kia.
12 _;3 }45 /**********************6 * Các hàm nội bộ *7 **********************/89 /**10 * @dev Lấy trình nhắn tin, thường từ bộ nhớ lưu trữ. Hàm này được hiển thị trong trường hợp một hợp đồng con11 * cần ghi đè.12 * @return Địa chỉ của hợp đồng trình nhắn tin liên miền nên được sử dụng.13 */14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {15 return ICrossDomainMessenger(messenger);16 }Hiện tất cảHàm này trả về trình nhắn tin liên miền.
Chúng tôi sử dụng một hàm thay vì biến messenger để cho phép các hợp đồng kế thừa từ hợp đồng này sử dụng một thuật toán để chỉ định trình nhắn tin liên miền nào sẽ sử dụng.
12 /**3 * Gửi một thông điệp đến một tài khoản trên một miền khác4 * @param _crossDomainTarget Người nhận dự định trên miền đích5 * @param _message Dữ liệu để gửi đến mục tiêu (thường là calldata cho một hàm với6 * `onlyFromCrossDomainAccount()`)7 * @param _gasLimit gasLimit cho việc nhận thông điệp trên miền đích.8 */9 function sendCrossDomainMessage(10 address _crossDomainTarget,11 uint32 _gasLimit,12 bytes memory _messageHiện tất cảCuối cùng là hàm gửi thông điệp đến lớp kia.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benignSlitheropens in a new tab là một trình phân tích tĩnh mà Optimism chạy trên mọi hợp đồng để tìm kiếm các lỗ hổng và các vấn đề tiềm ẩn khác. Trong trường hợp này, dòng sau đây kích hoạt hai lỗ hổng:
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);2 }3}Trong trường hợp này, chúng tôi không lo lắng về việc tái nhập, chúng tôi biết getCrossDomainMessenger() trả về một địa chỉ đáng tin cậy, ngay cả khi Slither không có cách nào để biết điều đó.
Hợp đồng cầu nối L1
Mã nguồn của hợp đồng này ở đâyopens in a new tab.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;Các giao diện có thể là một phần của các hợp đồng khác, vì vậy chúng phải hỗ trợ một loạt các phiên bản Solidity. Nhưng bản thân cầu nối là hợp đồng của chúng tôi, và chúng tôi có thể nghiêm ngặt về phiên bản Solidity mà nó sử dụng.
1/* Interface Imports */2import { IL1StandardBridge } from "./IL1StandardBridge.sol";3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";IL1ERC20Bridge và IL1StandardBridge được giải thích ở trên.
1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";Giao diện nàyopens in a new tab cho phép chúng ta tạo thông điệp để điều khiển cầu nối tiêu chuẩn trên L2.
1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Giao diện nàyopens in a new tab cho phép chúng ta điều khiển các hợp đồng ERC-20. Bạn có thể đọc thêm về nó ở đây.
1/* Library Imports */2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";Như đã giải thích ở trên, hợp đồng này được sử dụng để nhắn tin giữa các lớp.
1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";Lib_PredeployAddressesopens in a new tab có các địa chỉ cho các hợp đồng L2 luôn có cùng một địa chỉ. Điều này bao gồm cầu nối tiêu chuẩn trên L2.
1import { Address } from "@openzeppelin/contracts/utils/Address.sol";Các tiện ích Địa chỉ của OpenZeppelinopens in a new tab. Nó được sử dụng để phân biệt giữa các địa chỉ hợp đồng và những địa chỉ thuộc về tài khoản sở hữu bên ngoài (EOA).
Lưu ý rằng đây không phải là một giải pháp hoàn hảo, vì không có cách nào để phân biệt giữa các lệnh gọi trực tiếp và các lệnh gọi được thực hiện từ hàm dựng của hợp đồng, nhưng ít nhất điều này cho phép chúng tôi xác định và ngăn chặn một số lỗi người dùng phổ biến.
1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";Tiêu chuẩn ERC-20opens in a new tab hỗ trợ hai cách để một hợp đồng báo cáo lỗi:
- Hoàn nguyên
- Trả về
false
Xử lý cả hai trường hợp sẽ làm cho mã của chúng ta phức tạp hơn, vì vậy thay vào đó chúng ta sử dụng SafeERC20 của OpenZeppelinopens in a new tab, đảm bảo tất cả các lỗi đều dẫn đến việc hoàn nguyênopens in a new tab.
1/**2 * @title L1StandardBridge3 * @dev Cầu nối L1 ETH và ERC20 là một hợp đồng lưu trữ các khoản tiền L1 đã gửi và các token tiêu chuẩn4 * đang được sử dụng trên L2. Nó đồng bộ hóa một Cầu nối L2 tương ứng, thông báo cho nó về các khoản tiền gửi5 * và lắng nghe nó về các khoản rút tiền mới được hoàn tất.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;Hiện tất cảDòng này là cách chúng ta chỉ định sử dụng trình bao bọc SafeERC20 mỗi khi chúng ta sử dụng giao diện IERC20.
12 /********************************3 * Tham chiếu Hợp đồng Bên ngoài *4 ********************************/56 address public l2TokenBridge;Địa chỉ của L2StandardBridge.
12 // Ánh xạ token L1 đến token L2 đến số dư của token L1 đã gửi3 mapping(address => mapping(address => uint256)) public deposits;Một ánh xạopens in a new tab kép như thế này là cách bạn định nghĩa một mảng thưa hai chiềuopens in a new tab.
Các giá trị trong cấu trúc dữ liệu này được xác định là deposit[địa chỉ token L1][địa chỉ token L2].
Giá trị mặc định là không.
Chỉ các ô được thiết lập thành một giá trị khác mới được ghi vào bộ nhớ lưu trữ.
12 /***************3 * Hàm dựng *4 ***************/56 // Hợp đồng này hoạt động đằng sau một proxy, vì vậy các tham số của hàm dựng sẽ không được sử dụng.7 constructor() CrossDomainEnabled(address(0)) {}Để có thể nâng cấp hợp đồng này mà không cần phải sao chép tất cả các biến trong bộ nhớ lưu trữ.
Để làm điều đó, chúng tôi sử dụng một Proxyopens in a new tab, một hợp đồng sử dụng delegatecallopens in a new tab để chuyển các lệnh gọi đến một hợp đồng riêng biệt có địa chỉ được lưu trữ bởi hợp đồng proxy (khi bạn nâng cấp, bạn yêu cầu proxy thay đổi địa chỉ đó).
Khi bạn sử dụng delegatecall, bộ nhớ lưu trữ vẫn là bộ nhớ lưu trữ của hợp đồng gọi, vì vậy giá trị của tất cả các biến trạng thái hợp đồng không bị ảnh hưởng.
Một ảnh hưởng của mô hình này là bộ nhớ lưu trữ của hợp đồng được gọi của delegatecall không được sử dụng và do đó các giá trị hàm dựng được truyền cho nó không quan trọng.
Đây là lý do chúng ta có thể cung cấp một giá trị vô nghĩa cho hàm dựng CrossDomainEnabled.
Đó cũng là lý do tại sao việc khởi tạo bên dưới được tách biệt khỏi hàm dựng.
1 /******************2 * Khởi tạo *3 ******************/45 /**6 * @param _l1messenger Địa chỉ L1 Messenger đang được sử dụng để giao tiếp liên chuỗi.7 * @param _l2TokenBridge Địa chỉ cầu nối tiêu chuẩn L2.8 */9 // slither-disable-next-line external-functionHiện tất cảThử nghiệm Slither nàyopens in a new tab xác định các hàm không được gọi từ mã hợp đồng và do đó có thể được khai báo external thay vì public.
Chi phí gas của các hàm external có thể thấp hơn, vì chúng có thể được cung cấp các tham số trong calldata.
Các hàm được khai báo là public phải có thể truy cập được từ bên trong hợp đồng.
Các hợp đồng không thể sửa đổi calldata của chính chúng, vì vậy các tham số phải ở trong bộ nhớ.
Khi một hàm như vậy được gọi từ bên ngoài, cần phải sao chép calldata vào bộ nhớ, điều này tốn gas.
Trong trường hợp này, hàm chỉ được gọi một lần, vì vậy sự thiếu hiệu quả không quan trọng đối với chúng tôi.
1 function initialize(address _l1messenger, address _l2TokenBridge) public {2 require(messenger == address(0), "Contract has already been initialized.");Hàm initialize chỉ nên được gọi một lần.
Nếu địa chỉ của trình nhắn tin liên miền L1 hoặc cầu nối token L2 thay đổi, chúng tôi tạo một proxy mới và một cầu nối mới gọi nó.
Điều này không có khả năng xảy ra ngoại trừ khi toàn bộ hệ thống được nâng cấp, một sự kiện rất hiếm.
Lưu ý rằng hàm này không có bất kỳ cơ chế nào hạn chế ai có thể gọi nó.
Điều này có nghĩa là về mặt lý thuyết, một kẻ tấn công có thể đợi cho đến khi chúng tôi triển khai proxy và phiên bản đầu tiên của cầu nối, sau đó chạy trướcopens in a new tab để đến hàm initialize trước người dùng hợp pháp. Nhưng có hai phương pháp để ngăn chặn điều này:
- Nếu các hợp đồng không được triển khai trực tiếp bởi một EOA mà trong một giao dịch có một hợp đồng khác tạo ra chúngopens in a new tab, toàn bộ quá trình có thể là nguyên tử, và kết thúc trước khi bất kỳ giao dịch nào khác được thực hiện.
- Nếu lệnh gọi hợp pháp đến
initializethất bại, luôn có thể bỏ qua proxy và cầu nối mới được tạo và tạo ra những cái mới.
1 messenger = _l1messenger;2 l2TokenBridge = _l2TokenBridge;3 }Đây là hai tham số mà cầu nối cần biết.
12 /**************3 * Gửi tiền *4 **************/56 /** @dev Bổ ngữ yêu cầu người gửi phải là EOA. Kiểm tra này có thể bị bỏ qua bởi một hợp đồng7 * độc hại thông qua initcode, nhưng nó xử lý lỗi người dùng mà chúng tôi muốn tránh.8 */9 modifier onlyEOA() {10 // Được sử dụng để ngăn chặn việc gửi tiền từ các hợp đồng (tránh mất token do nhầm lẫn)11 require(!Address.isContract(msg.sender), "Account not EOA");12 _;13 }Hiện tất cảĐây là lý do chúng ta cần các tiện ích Address của OpenZeppelin.
1 /**2 * @dev Hàm này có thể được gọi mà không có dữ liệu3 * để gửi một lượng ETH vào số dư của người gọi trên L2.4 * Vì hàm nhận không lấy dữ liệu, một lượng5 * mặc định thận trọng được chuyển tiếp đến L2.6 */7 receive() external payable onlyEOA {8 _initiateETHDeposit(msg.sender, msg.sender, 200_000, bytes(""));9 }Hiện tất cảHàm này tồn tại cho mục đích thử nghiệm. Lưu ý rằng nó không xuất hiện trong các định nghĩa giao diện - nó không dành cho việc sử dụng thông thường.
1 /**2 * @inheritdoc IL1StandardBridge3 */4 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {5 _initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);6 }78 /**9 * @inheritdoc IL1StandardBridge10 */11 function depositETHTo(12 address _to,13 uint32 _l2Gas,14 bytes calldata _data15 ) external payable {16 _initiateETHDeposit(msg.sender, _to, _l2Gas, _data);17 }Hiện tất cảHai hàm này là các trình bao bọc xung quanh _initiateETHDeposit, hàm xử lý việc gửi ETH thực tế.
1 /**2 * @dev Thực hiện logic gửi tiền bằng cách lưu trữ ETH và thông báo cho Cổng ETH L23 * về việc gửi tiền.4 * @param _from Tài khoản để lấy tiền gửi từ L1.5 * @param _to Tài khoản để cấp tiền gửi trên L2.6 * @param _l2Gas Giới hạn gas cần thiết để hoàn tất việc gửi tiền trên L2.7 * @param _data Dữ liệu tùy chọn để chuyển tiếp đến L2. Dữ liệu này được cung cấp8 * chỉ để thuận tiện cho các hợp đồng bên ngoài. Ngoài việc thực thi một9 * độ dài tối đa, các hợp đồng này không cung cấp bất kỳ đảm bảo nào về nội dung của nó.10 */11 function _initiateETHDeposit(12 address _from,13 address _to,14 uint32 _l2Gas,15 bytes memory _data16 ) internal {17 // Xây dựng calldata cho lệnh gọi finalizeDeposit18 bytes memory message = abi.encodeWithSelector(Hiện tất cảCách thức hoạt động của các thông điệp liên miền là hợp đồng đích được gọi với thông điệp làm calldata của nó.
Các hợp đồng Solidity luôn diễn giải calldata của chúng theo
các thông số kỹ thuật ABIopens in a new tab.
Hàm Solidity abi.encodeWithSelectoropens in a new tab tạo ra calldata đó.
1 IL2ERC20Bridge.finalizeDeposit.selector,2 address(0),3 Lib_PredeployAddresses.OVM_ETH,4 _from,5 _to,6 msg.value,7 _data8 );Thông điệp ở đây là để gọi hàm finalizeDepositopens in a new tab với các tham số này:
| Thông số | Giá trị | Ý nghĩa |
|---|---|---|
| _l1Token | address(0) | Giá trị đặc biệt để đại diện cho ETH (không phải là một token ERC-20) trên L1 |
| _l2Token | Lib_PredeployAddresses.OVM_ETH | Hợp đồng L2 quản lý ETH trên Optimism, 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000 (hợp đồng này chỉ dành cho mục đích sử dụng nội bộ của Optimism) |
| _from | _from | Địa chỉ trên L1 gửi ETH |
| _to | _to | Địa chỉ trên L2 nhận ETH |
| số lượng | msg.value | Số lượng wei đã gửi (đã được gửi đến cầu nối) |
| _data | _data | Dữ liệu bổ sung để đính kèm vào khoản tiền gửi |
1 // Gửi calldata vào L22 // slither-disable-next-line reentrancy-events3 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);Gửi thông điệp thông qua trình nhắn tin liên miền.
1 // slither-disable-next-line reentrancy-events2 emit ETHDepositInitiated(_from, _to, msg.value, _data);3 }Phát ra một sự kiện để thông báo cho bất kỳ ứng dụng phi tập trung nào đang lắng nghe về giao dịch chuyển tiền này.
1 /**2 * @inheritdoc IL1ERC20Bridge3 */4 function depositERC20(5 .6 .7 .8 ) external virtual onlyEOA {9 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);10 }1112 /**13 * @inheritdoc IL1ERC20Bridge14 */15 function depositERC20To(16 .17 .18 .19 ) external virtual {20 _initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);21 }Hiện tất cảHai hàm này là các trình bao bọc xung quanh _initiateERC20Deposit, hàm xử lý việc gửi ERC-20 thực tế.
1 /**2 * @dev Thực hiện logic gửi tiền bằng cách thông báo cho Token đã gửi L23 * hợp đồng về việc gửi tiền và gọi một trình xử lý để khóa các khoản tiền L1. (ví dụ, transferFrom)4 *5 * @param _l1Token Địa chỉ của L1 ERC20 chúng ta đang gửi6 * @param _l2Token Địa chỉ của ERC20 L2 tương ứng của L17 * @param _from Tài khoản để lấy tiền gửi từ L18 * @param _to Tài khoản để cấp tiền gửi trên L29 * @param _amount Số lượng ERC20 cần gửi.10 * @param _l2Gas Giới hạn gas cần thiết để hoàn tất việc gửi tiền trên L2.11 * @param _data Dữ liệu tùy chọn để chuyển tiếp đến L2. Dữ liệu này được cung cấp12 * chỉ để thuận tiện cho các hợp đồng bên ngoài. Ngoài việc thực thi một13 * độ dài tối đa, các hợp đồng này không cung cấp bất kỳ đảm bảo nào về nội dung của nó.14 */15 function _initiateERC20Deposit(16 address _l1Token,17 address _l2Token,18 address _from,19 address _to,20 uint256 _amount,21 uint32 _l2Gas,22 bytes calldata _data23 ) internal {Hiện tất cảHàm này tương tự như _initiateETHDeposit ở trên, với một vài khác biệt quan trọng.
Sự khác biệt đầu tiên là hàm này nhận các địa chỉ token và số lượng cần chuyển làm tham số.
Trong trường hợp ETH, lệnh gọi đến cầu nối đã bao gồm việc chuyển tài sản đến tài khoản cầu nối (msg.value).
1 // Khi một khoản tiền gửi được khởi tạo trên L1, Cầu nối L1 sẽ chuyển tiền vào chính nó cho các khoản2 // rút tiền trong tương lai. safeTransferFrom cũng kiểm tra xem hợp đồng có mã hay không, vì vậy điều này sẽ thất bại nếu3 // _from là một EOA hoặc address(0).4 // slither-disable-next-line reentrancy-events, reentrancy-benign5 IERC20(_l1Token).safeTransferFrom(_from, address(this), _amount);Việc chuyển token ERC-20 theo một quy trình khác với ETH:
- Người dùng (
_from) cấp cho cầu nối một khoản phụ cấp để chuyển các token thích hợp. - Người dùng gọi cầu nối với địa chỉ của hợp đồng token, số lượng, v.v.
- Cầu nối chuyển các token (cho chính nó) như một phần của quá trình gửi tiền.
Bước đầu tiên có thể xảy ra trong một giao dịch riêng biệt so với hai bước cuối cùng.
Tuy nhiên, chạy trước không phải là một vấn đề vì hai hàm gọi _initiateERC20Deposit (depositERC20 và depositERC20To) chỉ gọi hàm này với msg.sender làm tham số _from.
1 // Xây dựng calldata cho _l2Token.finalizeDeposit(_to, _amount)2 bytes memory message = abi.encodeWithSelector(3 IL2ERC20Bridge.finalizeDeposit.selector,4 _l1Token,5 _l2Token,6 _from,7 _to,8 _amount,9 _data10 );1112 // Gửi calldata vào L213 // slither-disable-next-line reentrancy-events, reentrancy-benign14 sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);1516 // slither-disable-next-line reentrancy-benign17 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] + _amount;Hiện tất cảThêm số lượng token đã gửi vào cấu trúc dữ liệu deposits.
Có thể có nhiều địa chỉ trên L2 tương ứng với cùng một token L1 ERC-20, vì vậy không đủ để sử dụng số dư của token L1 ERC-20 của cầu nối để theo dõi các khoản tiền gửi.
12 // slither-disable-next-line reentrancy-events3 emit ERC20DepositInitiated(_l1Token, _l2Token, _from, _to, _amount, _data);4 }56 /*************************7 * Các hàm liên chuỗi *8 *************************/910 /**11 * @inheritdoc IL1StandardBridge12 */13 function finalizeETHWithdrawal(14 address _from,15 address _to,16 uint256 _amount,17 bytes calldata _dataHiện tất cảCầu nối L2 gửi một thông điệp đến trình nhắn tin liên miền L2, điều này khiến trình nhắn tin liên miền L1 gọi hàm này (tất nhiên, một khi giao dịch hoàn tất thông điệpopens in a new tab được gửi trên L1).
1 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Đảm bảo rằng đây là một thông điệp hợp lệ, đến từ trình nhắn tin liên miền và bắt nguồn từ cầu nối token L2. Hàm này được sử dụng để rút ETH từ cầu nối, vì vậy chúng ta phải đảm bảo nó chỉ được gọi bởi người gọi được ủy quyền.
1 // slither-disable-next-line reentrancy-events2 (bool success, ) = _to.call{ value: _amount }(new bytes(0));Cách để chuyển ETH là gọi người nhận với số lượng wei trong msg.value.
1 require(success, "TransferHelper::safeTransferETH: ETH transfer failed");23 // slither-disable-next-line reentrancy-events4 emit ETHWithdrawalFinalized(_from, _to, _amount, _data);Phát ra một sự kiện về việc rút tiền.
1 }23 /**4 * @inheritdoc IL1ERC20Bridge5 */6 function finalizeERC20Withdrawal(7 address _l1Token,8 address _l2Token,9 address _from,10 address _to,11 uint256 _amount,12 bytes calldata _data13 ) external onlyFromCrossDomainAccount(l2TokenBridge) {Hiện tất cảHàm này tương tự như finalizeETHWithdrawal ở trên, với những thay đổi cần thiết cho các token ERC-20.
1 deposits[_l1Token][_l2Token] = deposits[_l1Token][_l2Token] - _amount;Cập nhật cấu trúc dữ liệu deposits.
12 // Khi một khoản rút tiền được hoàn tất trên L1, Cầu nối L1 sẽ chuyển tiền cho người rút3 // slither-disable-next-line reentrancy-events4 IERC20(_l1Token).safeTransfer(_to, _amount);56 // slither-disable-next-line reentrancy-events7 emit ERC20WithdrawalFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);8 }91011 /*****************************12 * Tạm thời - Di chuyển ETH *13 *****************************/1415 /**16 * @dev Thêm số dư ETH vào tài khoản. Điều này nhằm cho phép ETH17 * được di chuyển từ một cổng cũ sang một cổng mới.18 * LƯU Ý: Điều này được để lại cho một lần nâng cấp duy nhất để chúng tôi có thể nhận được ETH đã di chuyển từ19 * hợp đồng cũ20 */21 function donateETH() external payable {}22}Hiện tất cảCó một triển khai trước đó của cầu nối.
Khi chúng tôi chuyển từ triển khai đó sang triển khai này, chúng tôi đã phải di chuyển tất cả các tài sản.
Các token ERC-20 chỉ cần được di chuyển.
Tuy nhiên, để chuyển ETH đến một hợp đồng, bạn cần sự chấp thuận của hợp đồng đó, đó là điều mà donateETH cung cấp cho chúng tôi.
Các token ERC-20 trên L2
Để một token ERC-20 phù hợp với cầu nối tiêu chuẩn, nó cần cho phép cầu nối tiêu chuẩn, và chỉ cầu nối tiêu chuẩn, đúc token. Điều này là cần thiết vì các cầu nối cần đảm bảo rằng số lượng token lưu hành trên Optimism bằng với số lượng token bị khóa bên trong hợp đồng cầu nối L1. Nếu có quá nhiều token trên L2, một số người dùng sẽ không thể bắc cầu tài sản của họ trở lại L1. Thay vì một cầu nối đáng tin cậy, chúng ta về cơ bản sẽ tái tạo lại hệ thống ngân hàng dự trữ một phầnopens in a new tab. Nếu có quá nhiều token trên L1, một số token đó sẽ bị khóa bên trong hợp đồng cầu nối mãi mãi vì không có cách nào để giải phóng chúng mà không đốt các token L2.
IL2StandardERC20
Mọi token ERC-20 trên L2 sử dụng cầu nối tiêu chuẩn cần cung cấp giao diện nàyopens in a new tab, có các hàm và sự kiện mà cầu nối tiêu chuẩn cần.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";Giao diện ERC-20 tiêu chuẩnopens in a new tab không bao gồm các hàm mint và burn.
Các phương thức đó không được yêu cầu bởi tiêu chuẩn ERC-20opens in a new tab, tiêu chuẩn này không chỉ định các cơ chế để tạo và hủy token.
1import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";Giao diện ERC-165opens in a new tab được sử dụng để chỉ định các hàm mà một hợp đồng cung cấp. Bạn có thể đọc tiêu chuẩn ở đâyopens in a new tab.
1interface IL2StandardERC20 is IERC20, IERC165 {2 function l1Token() external returns (address);Hàm này cung cấp địa chỉ của token L1 được bắc cầu đến hợp đồng này. Lưu ý rằng chúng ta không có một hàm tương tự theo hướng ngược lại. Chúng ta cần có thể bắc cầu bất kỳ token L1 nào, bất kể hỗ trợ L2 có được lên kế hoạch khi nó được triển khai hay không.
12 function mint(address _to, uint256 _amount) external;34 function burn(address _from, uint256 _amount) external;56 event Mint(address indexed _account, uint256 _amount);7 event Burn(address indexed _account, uint256 _amount);8}Các hàm và sự kiện để đúc (tạo) và đốt (hủy) token. Cầu nối nên là thực thể duy nhất có thể chạy các hàm này để đảm bảo số lượng token là chính xác (bằng với số lượng token bị khóa trên L1).
L2StandardERC20
Đây là triển khai của chúng tôi về giao diện IL2StandardERC20opens in a new tab.
Trừ khi bạn cần một loại logic tùy chỉnh nào đó, bạn nên sử dụng cái này.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";Hợp đồng ERC-20 của OpenZeppelinopens in a new tab. Optimism không tin vào việc phát minh lại bánh xe, đặc biệt là khi bánh xe được kiểm toán kỹ lưỡng và cần phải đủ đáng tin cậy để giữ tài sản.
1import "./IL2StandardERC20.sol";23contract L2StandardERC20 is IL2StandardERC20, ERC20 {4 address public l1Token;5 address public l2Bridge;Đây là hai tham số cấu hình bổ sung mà chúng tôi yêu cầu và ERC-20 thường không có.
12 /**3 * @param _l2Bridge Địa chỉ của cầu nối tiêu chuẩn L2.4 * @param _l1Token Địa chỉ của token L1 tương ứng.5 * @param _name Tên ERC20.6 * @param _symbol Ký hiệu ERC20.7 */8 constructor(9 address _l2Bridge,10 address _l1Token,11 string memory _name,12 string memory _symbol13 ) ERC20(_name, _symbol) {14 l1Token = _l1Token;15 l2Bridge = _l2Bridge;16 }Hiện tất cảĐầu tiên gọi hàm dựng cho hợp đồng mà chúng ta kế thừa từ đó (ERC20(_name, _symbol)) và sau đó thiết lập các biến của riêng chúng ta.
12 modifier onlyL2Bridge() {3 require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn");4 _;5 }678 // slither-disable-next-line external-function9 function supportsInterface(bytes4 _interfaceId) public pure returns (bool) {10 bytes4 firstSupportedInterface = bytes4(keccak256("supportsInterface(bytes4)")); // ERC16511 bytes4 secondSupportedInterface = IL2StandardERC20.l1Token.selector ^12 IL2StandardERC20.mint.selector ^13 IL2StandardERC20.burn.selector;14 return _interfaceId == firstSupportedInterface || _interfaceId == secondSupportedInterface;15 }Hiện tất cảĐây là cách ERC-165opens in a new tab hoạt động. Mỗi giao diện là một tập hợp các hàm được hỗ trợ và được xác định là kết quả của phép toán OR độc quyềnopens in a new tab của các bộ chọn hàm ABIopens in a new tab của các hàm đó.
Cầu nối L2 sử dụng ERC-165 như một kiểm tra hợp lý để đảm bảo rằng hợp đồng ERC-20 mà nó gửi tài sản đến là một IL2StandardERC20.
Lưu ý: Không có gì ngăn cản hợp đồng giả mạo cung cấp câu trả lời sai cho supportsInterface, vì vậy đây là một cơ chế kiểm tra hợp lý, không phải là một cơ chế bảo mật.
1 // slither-disable-next-line external-function2 function mint(address _to, uint256 _amount) public virtual onlyL2Bridge {3 _mint(_to, _amount);45 emit Mint(_to, _amount);6 }78 // slither-disable-next-line external-function9 function burn(address _from, uint256 _amount) public virtual onlyL2Bridge {10 _burn(_from, _amount);1112 emit Burn(_from, _amount);13 }14}Hiện tất cảChỉ có cầu nối L2 mới được phép đúc và đốt tài sản.
_mint và _burn thực sự được định nghĩa trong hợp đồng OpenZeppelin ERC-20.
Hợp đồng đó chỉ không hiển thị chúng ra bên ngoài, vì các điều kiện để đúc và đốt token rất đa dạng như số cách sử dụng ERC-20.
Mã cầu nối L2
Đây là mã chạy cầu nối trên Optimism. Nguồn của hợp đồng này ở đâyopens in a new tab.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;34/* Interface Imports */5import { IL1StandardBridge } from "../../L1/messaging/IL1StandardBridge.sol";6import { IL1ERC20Bridge } from "../../L1/messaging/IL1ERC20Bridge.sol";7import { IL2ERC20Bridge } from "./IL2ERC20Bridge.sol";Giao diện IL2ERC20Bridgeopens in a new tab rất tương tự với tương đương L1 mà chúng ta đã thấy ở trên. Có hai sự khác biệt đáng kể:
- Trên L1, bạn khởi tạo gửi tiền và hoàn tất rút tiền. Ở đây bạn khởi tạo rút tiền và hoàn tất gửi tiền.
- Trên L1, cần phải phân biệt giữa ETH và các token ERC-20. Trên L2, chúng ta có thể sử dụng cùng các hàm cho cả hai vì nội bộ số dư ETH trên Optimism được xử lý như một token ERC-20 với địa chỉ 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000opens in a new tab.
1/* Library Imports */2import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";3import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";4import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";56/* Contract Imports */7import { IL2StandardERC20 } from "../../standards/IL2StandardERC20.sol";89/**10 * @title L2StandardBridge11 * @dev Cầu nối Tiêu chuẩn L2 là một hợp đồng hoạt động cùng với cầu nối Tiêu chuẩn L1 để12 * cho phép chuyển đổi ETH và ERC20 giữa L1 và L2.13 * Hợp đồng này hoạt động như một trình đúc các token mới khi nó nghe về các khoản tiền gửi vào cầu nối Tiêu chuẩn14 * L1.15 * Hợp đồng này cũng hoạt động như một trình đốt các token dự định rút, thông báo cho cầu nối L116 * để giải phóng các khoản tiền L1.17 */18contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {19 /********************************20 * Tham chiếu Hợp đồng Bên ngoài *21 ********************************/2223 address public l1TokenBridge;Hiện tất cảTheo dõi địa chỉ của cầu nối L1. Lưu ý rằng ngược lại với tương đương L1, ở đây chúng ta cần biến này. Địa chỉ của cầu nối L1 không được biết trước.
12 /***************3 * Hàm dựng *4 ***************/56 /**7 * @param _l2CrossDomainMessenger Trình nhắn tin liên miền được sử dụng bởi hợp đồng này.8 * @param _l1TokenBridge Địa chỉ của cầu nối L1 được triển khai trên chuỗi chính.9 */10 constructor(address _l2CrossDomainMessenger, address _l1TokenBridge)11 CrossDomainEnabled(_l2CrossDomainMessenger)12 {13 l1TokenBridge = _l1TokenBridge;14 }1516 /***************17 * Rút tiền *18 ***************/1920 /**21 * @inheritdoc IL2ERC20Bridge22 */23 function withdraw(24 address _l2Token,25 uint256 _amount,26 uint32 _l1Gas,27 bytes calldata _data28 ) external virtual {29 _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _l1Gas, _data);30 }3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function withdrawTo(36 address _l2Token,37 address _to,38 uint256 _amount,39 uint32 _l1Gas,40 bytes calldata _data41 ) external virtual {42 _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);43 }Hiện tất cảHai hàm này khởi tạo việc rút tiền. Lưu ý rằng không cần phải chỉ định địa chỉ token L1. Các token L2 được mong đợi sẽ cho chúng tôi biết địa chỉ tương đương của L1.
12 /**3 * @dev Thực hiện logic rút tiền bằng cách đốt token và thông báo4 * cho Cổng token L1 về việc rút tiền.5 * @param _l2Token Địa chỉ của token L2 nơi việc rút tiền được khởi tạo.6 * @param _from Tài khoản để lấy tiền rút từ L2.7 * @param _to Tài khoản để cấp tiền rút trên L1.8 * @param _amount Số lượng token cần rút.9 * @param _l1Gas Không sử dụng, nhưng được bao gồm cho các cân nhắc tương thích về phía trước tiềm năng.10 * @param _data Dữ liệu tùy chọn để chuyển tiếp đến L1. Dữ liệu này được cung cấp11 * chỉ để thuận tiện cho các hợp đồng bên ngoài. Ngoài việc thực thi một12 * độ dài tối đa, các hợp đồng này không cung cấp bất kỳ đảm bảo nào về nội dung của nó.13 */14 function _initiateWithdrawal(15 address _l2Token,16 address _from,17 address _to,18 uint256 _amount,19 uint32 _l1Gas,20 bytes calldata _data21 ) internal {22 // Khi một khoản rút tiền được khởi tạo, chúng tôi đốt tiền của người rút để ngăn chặn việc sử dụng L223 // sau đó24 // slither-disable-next-line reentrancy-events25 IL2StandardERC20(_l2Token).burn(msg.sender, _amount);Hiện tất cảLưu ý rằng chúng tôi không dựa vào tham số _from mà vào msg.sender khó giả mạo hơn nhiều (không thể, theo như tôi biết).
12 // Xây dựng calldata cho l1TokenBridge.finalizeERC20Withdrawal(_to, _amount)3 // slither-disable-next-line reentrancy-events4 address l1Token = IL2StandardERC20(_l2Token).l1Token();5 bytes memory message;67 if (_l2Token == Lib_PredeployAddresses.OVM_ETH) {Trên L1, cần phải phân biệt giữa ETH và ERC-20.
1 message = abi.encodeWithSelector(2 IL1StandardBridge.finalizeETHWithdrawal.selector,3 _from,4 _to,5 _amount,6 _data7 );8 } else {9 message = abi.encodeWithSelector(10 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,11 l1Token,12 _l2Token,13 _from,14 _to,15 _amount,16 _data17 );18 }1920 // Gửi thông điệp lên cầu nối L121 // slither-disable-next-line reentrancy-events22 sendCrossDomainMessage(l1TokenBridge, _l1Gas, message);2324 // slither-disable-next-line reentrancy-events25 emit WithdrawalInitiated(l1Token, _l2Token, msg.sender, _to, _amount, _data);26 }2728 /************************************29 * Hàm liên chuỗi: Gửi tiền *30 ************************************/3132 /**33 * @inheritdoc IL2ERC20Bridge34 */35 function finalizeDeposit(36 address _l1Token,37 address _l2Token,38 address _from,39 address _to,40 uint256 _amount,41 bytes calldata _dataHiện tất cảHàm này được gọi bởi L1StandardBridge.
1 ) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {Đảm bảo nguồn của thông điệp là hợp lệ.
Điều này quan trọng vì hàm này gọi _mint và có thể được sử dụng để cấp các token không được bảo đảm bởi các token mà cầu nối sở hữu trên L1.
1 // Kiểm tra token mục tiêu có tuân thủ và2 // xác minh token đã gửi trên L1 khớp với biểu diễn token đã gửi L2 ở đây3 if (4 // slither-disable-next-line reentrancy-events5 ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&6 _l1Token == IL2StandardERC20(_l2Token).l1Token()Các kiểm tra hợp lý:
- Giao diện chính xác được hỗ trợ
- Địa chỉ L1 của hợp đồng ERC-20 L2 khớp với nguồn L1 của các token
1 ) {2 // Khi một khoản tiền gửi được hoàn tất, chúng tôi ghi có vào tài khoản trên L2 với cùng một số lượng3 // token.4 // slither-disable-next-line reentrancy-events5 IL2StandardERC20(_l2Token).mint(_to, _amount);6 // slither-disable-next-line reentrancy-events7 emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);Nếu các kiểm tra hợp lý vượt qua, hoàn tất việc gửi tiền:
- Đúc các token
- Phát ra sự kiện thích hợp
1 } else {2 // Hoặc token L2 đang được gửi vào không đồng ý về địa chỉ chính xác3 // của token L1 của nó, hoặc không hỗ trợ giao diện chính xác.4 // Điều này chỉ nên xảy ra nếu có một token L2 độc hại, hoặc nếu người dùng bằng cách nào đó5 // chỉ định sai địa chỉ token L2 để gửi vào.6 // Trong cả hai trường hợp, chúng tôi dừng quá trình ở đây và xây dựng một thông điệp7 // rút tiền để người dùng có thể lấy lại tiền của họ trong một số trường hợp.8 // Không có cách nào để ngăn chặn hoàn toàn các hợp đồng token độc hại, nhưng điều này giới hạn9 // lỗi người dùng và giảm thiểu một số hình thức hành vi hợp đồng độc hại.Hiện tất cảNếu người dùng mắc lỗi có thể phát hiện bằng cách sử dụng sai địa chỉ token L2, chúng tôi muốn hủy bỏ việc gửi tiền và trả lại các token trên L1. Cách duy nhất chúng ta có thể làm điều này từ L2 là gửi một thông điệp sẽ phải đợi hết thời gian thử thách lỗi, nhưng điều đó tốt hơn nhiều cho người dùng so với việc mất các token vĩnh viễn.
1 bytes memory message = abi.encodeWithSelector(2 IL1ERC20Bridge.finalizeERC20Withdrawal.selector,3 _l1Token,4 _l2Token,5 _to, // đã hoán đổi _to và _from ở đây để trả lại khoản tiền gửi cho người gửi6 _from,7 _amount,8 _data9 );1011 // Gửi thông điệp lên cầu nối L112 // slither-disable-next-line reentrancy-events13 sendCrossDomainMessage(l1TokenBridge, 0, message);14 // slither-disable-next-line reentrancy-events15 emit DepositFailed(_l1Token, _l2Token, _from, _to, _amount, _data);16 }17 }18}Hiện tất cảKết luận
Cầu nối tiêu chuẩn là cơ chế linh hoạt nhất cho việc chuyển tài sản. Tuy nhiên, vì nó rất chung chung nên không phải lúc nào cũng là cơ chế dễ sử dụng nhất. Đặc biệt đối với việc rút tiền, hầu hết người dùng thích sử dụng các cầu nối của bên thứ baopens in a new tab không phải chờ đợi hết thời gian thử thách và không yêu cầu bằng chứng Merkle để hoàn tất việc rút tiền.
Các cầu nối này thường hoạt động bằng cách có tài sản trên L1, mà họ cung cấp ngay lập tức với một khoản phí nhỏ (thường ít hơn chi phí gas cho một lần rút tiền qua cầu nối tiêu chuẩn). Khi cầu nối (hoặc những người điều hành nó) dự đoán sẽ thiếu tài sản L1, nó sẽ chuyển đủ tài sản từ L2. Vì đây là những khoản rút tiền rất lớn, chi phí rút tiền được phân bổ trên một số lượng lớn và là một tỷ lệ phần trăm nhỏ hơn nhiều.
Hy vọng bài viết này đã giúp bạn hiểu thêm về cách lớp 2 hoạt động, và cách viết mã Solidity rõ ràng và bảo mật.
Xem thêm công việc của tôi tại đâyopens in a new tab.
Lần cập nhật trang lần cuối: 22 tháng 10, 2025