mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-09 04:08:01 -05:00
feat: implement L2ETHBridge.bridgeETH and L1ETHBridge.completeBridge
This commit is contained in:
@@ -4,18 +4,37 @@ pragma solidity ^0.8.30;
|
||||
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
||||
|
||||
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
|
||||
import { IL1ETHBridge } from "./interfaces/IL1ETHBridge.sol";
|
||||
import { IL2ETHBridge } from "./interfaces/IL2ETHBridge.sol";
|
||||
import { MessageServiceBase } from "../../messaging/MessageServiceBase.sol";
|
||||
import { IMessageService } from "../../messaging/interfaces/IMessageService.sol";
|
||||
|
||||
contract L1ETHBridge is IL1ETHBridge, Initializable, UUPSUpgradeable, OwnableUpgradeable, MessageServiceBase {
|
||||
contract L1ETHBridge is IL1ETHBridge, Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable, MessageServiceBase {
|
||||
/**
|
||||
* @notice The completed message struct.
|
||||
*/
|
||||
struct CompletedMessage {
|
||||
address to;
|
||||
uint256 value;
|
||||
bytes callData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice The yield manager address.
|
||||
*/
|
||||
address public yieldManager;
|
||||
|
||||
/**
|
||||
* @notice The next completed message id.
|
||||
*/
|
||||
uint256 public nextCompletedMessageId;
|
||||
|
||||
/**
|
||||
* @notice The completed messages.
|
||||
*/
|
||||
mapping(uint256 => CompletedMessage) public completedMessages;
|
||||
|
||||
/**
|
||||
* @dev Ensures the address is not address(0).
|
||||
* @param _addr Address to check.
|
||||
@@ -87,6 +106,23 @@ contract L1ETHBridge is IL1ETHBridge, Initializable, UUPSUpgradeable, OwnableUpg
|
||||
yieldManager = _yieldManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Completes the bridge. Callable only by the L1MessageService.
|
||||
* @param _to The recipient address.
|
||||
* @param _value The amount of ETH to transfer.
|
||||
* @param _calldata The calldata to pass to the recipient.
|
||||
*/
|
||||
function completeBridge(
|
||||
address _to,
|
||||
uint256 _value,
|
||||
bytes memory _calldata
|
||||
) external nonReentrant onlyMessagingService onlyAuthorizedRemoteSender {
|
||||
completedMessages[nextCompletedMessageId] = CompletedMessage(_to, _value, _calldata);
|
||||
nextCompletedMessageId++;
|
||||
|
||||
emit MessageCompleted(nextCompletedMessageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Bridges ETH to the L2ETHBridge.
|
||||
* @param _to The recipient address on the L2.
|
||||
|
||||
@@ -20,6 +20,15 @@ contract L2ETHBridge is IL2ETHBridge, Initializable, UUPSUpgradeable, OwnableUpg
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the amount is not 0.
|
||||
* @param _amount amount to check.
|
||||
*/
|
||||
modifier nonZeroAmount(uint256 _amount) {
|
||||
if (_amount == 0) revert L2ETHBridge__ZeroValueNotAllowed();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Disables initializers to prevent reinitialization.
|
||||
*/
|
||||
@@ -75,6 +84,19 @@ contract L2ETHBridge is IL2ETHBridge, Initializable, UUPSUpgradeable, OwnableUpg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Bridges ETH to the L1ETHBridge.
|
||||
* @param _to The recipient address on the L1.
|
||||
* @param _calldata The calldata to be sent to the L1ETHBridge.
|
||||
*/
|
||||
function bridgeETH(
|
||||
address _to,
|
||||
bytes memory _calldata
|
||||
) external payable nonZeroAmount(msg.value) nonZeroAddress(_to) {
|
||||
bytes memory data = abi.encodeWithSelector(IL2ETHBridge.completeBridge.selector, _to, msg.value, _calldata);
|
||||
messageService.sendMessage(remoteSender, 0, data);
|
||||
}
|
||||
|
||||
function _authorizeUpgrade(address) internal view override {
|
||||
_checkOwner();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ interface IL1ETHBridge {
|
||||
address indexed setBy
|
||||
);
|
||||
|
||||
event MessageCompleted(
|
||||
uint256 indexed messageId
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when the yield manager address is set.
|
||||
* @param newYieldManager The indexed new yield manager address.
|
||||
@@ -28,6 +32,7 @@ interface IL1ETHBridge {
|
||||
|
||||
error L1ETHBridge__ZeroValueNotAllowed();
|
||||
error L1ETHBridge__ZeroAddressNotAllowed();
|
||||
error L1ETHBridge__ETHTransferFailed();
|
||||
error L1ETHBridge__YieldManagerDepositFailed();
|
||||
|
||||
function setRemoteSender(address _remoteSender) external;
|
||||
@@ -37,4 +42,6 @@ interface IL1ETHBridge {
|
||||
function setYieldManager(address _yieldManager) external;
|
||||
|
||||
function bridgeETH(address _to, bytes memory _calldata) external payable;
|
||||
|
||||
function completeBridge(address _to, uint256 _value, bytes calldata _calldata) external;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ interface IL2ETHBridge {
|
||||
address indexed setBy
|
||||
);
|
||||
|
||||
error L2ETHBridge__ETHTransferFailed();
|
||||
|
||||
error L2ETHBridge__ZeroValueNotAllowed();
|
||||
error L2ETHBridge__ZeroAddressNotAllowed();
|
||||
error L2ETHBridge__ETHTransferFailed();
|
||||
|
||||
function setRemoteSender(address _remoteSender) external;
|
||||
|
||||
|
||||
@@ -127,4 +127,31 @@ contract L1ETHBridgeTest is Test {
|
||||
assertEq(message.value, 0);
|
||||
assertEq(message.data, expectedData);
|
||||
}
|
||||
|
||||
function test_CompleteBridgeRevertsIfMsgSenderIsNotL2MessageService() public {
|
||||
vm.prank(nonAuthorizedSender);
|
||||
vm.expectRevert("CallerIsNotMessageService()");
|
||||
bridge.completeBridge(user1, 0, "");
|
||||
}
|
||||
|
||||
function test_CompleteBridgeRevertsIfRemoteSenderIsNotL2ETHBridge() public {
|
||||
messageService.setOriginalSender(nonAuthorizedSender);
|
||||
vm.prank(address(messageService));
|
||||
vm.expectRevert("SenderNotAuthorized()");
|
||||
bridge.completeBridge(user1, 0, "");
|
||||
}
|
||||
|
||||
function test_CompleteBridge() public {
|
||||
assertEq(bridge.nextCompletedMessageId(), 0);
|
||||
messageService.setOriginalSender(remoteSender);
|
||||
|
||||
vm.prank(address(messageService));
|
||||
bridge.completeBridge(user1, 100, "test-data");
|
||||
|
||||
assertEq(bridge.nextCompletedMessageId(), 1);
|
||||
(address to, uint256 value, bytes memory callData) = bridge.completedMessages(0);
|
||||
assertEq(to, user1);
|
||||
assertEq(value, 100);
|
||||
assertEq(callData, "test-data");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,15 @@ import { L2ETHBridge } from "../../../../src/bridging/eth/L2ETHBridge.sol";
|
||||
import { DeployL2ETHBridge } from "../../../../scripts/yield/bridge/l2/DeployL2ETHBridge.s.sol";
|
||||
import { RecipientMock } from "./mocks/RecipientMock.sol";
|
||||
import { L2MessageServiceMock } from "./mocks/L2MessageServiceMock.sol";
|
||||
import { IL1ETHBridge } from "../../../../src/bridging/eth/interfaces/IL1ETHBridge.sol";
|
||||
|
||||
contract L2ETHBridgeTest is Test {
|
||||
L2ETHBridge bridge;
|
||||
|
||||
address deployer;
|
||||
address l1ETHBridge;
|
||||
address user1 = makeAddr("user1");
|
||||
address user2 = makeAddr("user2");
|
||||
address nonAuthorizedSender = makeAddr("nonAuthorizedSender");
|
||||
L2MessageServiceMock l2MessageService;
|
||||
|
||||
@@ -66,8 +69,6 @@ contract L2ETHBridgeTest is Test {
|
||||
}
|
||||
|
||||
function test_CompleteBridgeRevertsIfMsgSenderIsNotL2MessageService() public {
|
||||
l2MessageService.setOriginalSender(l1ETHBridge);
|
||||
|
||||
vm.prank(nonAuthorizedSender);
|
||||
vm.expectRevert("CallerIsNotMessageService()");
|
||||
bridge.completeBridge(l1ETHBridge, 0, "");
|
||||
@@ -78,7 +79,7 @@ contract L2ETHBridgeTest is Test {
|
||||
|
||||
vm.prank(address(l2MessageService));
|
||||
vm.expectRevert("SenderNotAuthorized()");
|
||||
bridge.completeBridge(nonAuthorizedSender, 0, "");
|
||||
bridge.completeBridge(user1, 0, "");
|
||||
}
|
||||
|
||||
function test_CompleteBridge() public {
|
||||
@@ -94,4 +95,35 @@ contract L2ETHBridgeTest is Test {
|
||||
assertEq(recipientMock.lastCallParam(), 77);
|
||||
assertEq(address(recipientMock).balance, 100);
|
||||
}
|
||||
|
||||
function test_bridgeETHRevertsIfValueIsZero() public {
|
||||
vm.expectRevert("L2ETHBridge__ZeroValueNotAllowed()");
|
||||
bridge.bridgeETH(address(recipientMock), "");
|
||||
}
|
||||
|
||||
function test_bridgeETHRevertsIfToIsZeroAddress() public {
|
||||
vm.expectRevert("L2ETHBridge__ZeroAddressNotAllowed()");
|
||||
bridge.bridgeETH{value: 100}(address(0), "");
|
||||
}
|
||||
|
||||
function test_ETHBridgeMessagesAreSentToL1ETHBridge() public {
|
||||
vm.deal(user1, 100);
|
||||
vm.prank(user1);
|
||||
bridge.bridgeETH{ value: 100 }(user2, "test-message");
|
||||
|
||||
assertEq(l2MessageService.messagesLength(), 1);
|
||||
|
||||
bytes memory expectedData = abi.encodeWithSelector(
|
||||
IL1ETHBridge.completeBridge.selector,
|
||||
user2,
|
||||
100,
|
||||
"test-message"
|
||||
);
|
||||
|
||||
L2MessageServiceMock.Message memory message = l2MessageService.lastMessage();
|
||||
assertEq(message.to, l1ETHBridge);
|
||||
assertEq(message.fee, 0);
|
||||
assertEq(message.value, 0);
|
||||
assertEq(message.data, expectedData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
contract L2MessageServiceMock {
|
||||
struct Message {
|
||||
address to;
|
||||
uint256 fee;
|
||||
uint256 value;
|
||||
bytes data;
|
||||
}
|
||||
|
||||
Message[] public messages;
|
||||
|
||||
address public originalSender;
|
||||
|
||||
function setOriginalSender(address _originalSender) external {
|
||||
@@ -11,4 +20,17 @@ contract L2MessageServiceMock {
|
||||
function sender() external view returns (address) {
|
||||
return originalSender;
|
||||
}
|
||||
|
||||
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable {
|
||||
messages.push(Message({ to: _to, fee: _fee, value: msg.value, data: _calldata }));
|
||||
}
|
||||
|
||||
function messagesLength() external view returns (uint256) {
|
||||
return messages.length;
|
||||
}
|
||||
|
||||
function lastMessage() external view returns (Message memory) {
|
||||
require(messages.length > 0, "No messages made");
|
||||
return messages[messages.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,16 @@ contract RollupMock {
|
||||
|
||||
Message[] public messages;
|
||||
|
||||
address public originalSender;
|
||||
|
||||
function setOriginalSender(address _originalSender) external {
|
||||
originalSender = _originalSender;
|
||||
}
|
||||
|
||||
function sender() external view returns (address) {
|
||||
return originalSender;
|
||||
}
|
||||
|
||||
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable {
|
||||
messages.push(Message({ to: _to, fee: _fee, value: msg.value, data: _calldata }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user