From dc1549d03db66e7318c479bff94f2e1417eca43f Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 26 Jun 2025 13:59:34 +0200 Subject: [PATCH] feat: implement L2ETHBridge.bridgeETH and L1ETHBridge.completeBridge --- contracts/src/bridging/eth/L1ETHBridge.sol | 40 ++++++++++++++++++- contracts/src/bridging/eth/L2ETHBridge.sol | 22 ++++++++++ .../bridging/eth/interfaces/IL1ETHBridge.sol | 7 ++++ .../bridging/eth/interfaces/IL2ETHBridge.sol | 4 +- .../foundry/bridging/eth/L1ETHBridge.t.sol | 27 +++++++++++++ .../foundry/bridging/eth/L2ETHBridge.t.sol | 38 ++++++++++++++++-- .../eth/mocks/L2MessageServiceMock.sol | 22 ++++++++++ .../foundry/bridging/eth/mocks/RollupMock.sol | 10 +++++ 8 files changed, 163 insertions(+), 7 deletions(-) diff --git a/contracts/src/bridging/eth/L1ETHBridge.sol b/contracts/src/bridging/eth/L1ETHBridge.sol index f163d827..9bd73dc7 100644 --- a/contracts/src/bridging/eth/L1ETHBridge.sol +++ b/contracts/src/bridging/eth/L1ETHBridge.sol @@ -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. diff --git a/contracts/src/bridging/eth/L2ETHBridge.sol b/contracts/src/bridging/eth/L2ETHBridge.sol index c01d8e54..b5b6bf3f 100644 --- a/contracts/src/bridging/eth/L2ETHBridge.sol +++ b/contracts/src/bridging/eth/L2ETHBridge.sol @@ -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(); } diff --git a/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol b/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol index 35c6f6fe..165b67c3 100644 --- a/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol +++ b/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol @@ -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; } diff --git a/contracts/src/bridging/eth/interfaces/IL2ETHBridge.sol b/contracts/src/bridging/eth/interfaces/IL2ETHBridge.sol index 2f8edaa9..17916555 100644 --- a/contracts/src/bridging/eth/interfaces/IL2ETHBridge.sol +++ b/contracts/src/bridging/eth/interfaces/IL2ETHBridge.sol @@ -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; diff --git a/contracts/test/foundry/bridging/eth/L1ETHBridge.t.sol b/contracts/test/foundry/bridging/eth/L1ETHBridge.t.sol index 5337aea1..b9c809f7 100644 --- a/contracts/test/foundry/bridging/eth/L1ETHBridge.t.sol +++ b/contracts/test/foundry/bridging/eth/L1ETHBridge.t.sol @@ -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"); + } } diff --git a/contracts/test/foundry/bridging/eth/L2ETHBridge.t.sol b/contracts/test/foundry/bridging/eth/L2ETHBridge.t.sol index c5e8fdb9..b977001f 100644 --- a/contracts/test/foundry/bridging/eth/L2ETHBridge.t.sol +++ b/contracts/test/foundry/bridging/eth/L2ETHBridge.t.sol @@ -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); + } } diff --git a/contracts/test/foundry/bridging/eth/mocks/L2MessageServiceMock.sol b/contracts/test/foundry/bridging/eth/mocks/L2MessageServiceMock.sol index 038e2ae2..6c691868 100644 --- a/contracts/test/foundry/bridging/eth/mocks/L2MessageServiceMock.sol +++ b/contracts/test/foundry/bridging/eth/mocks/L2MessageServiceMock.sol @@ -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]; + } } diff --git a/contracts/test/foundry/bridging/eth/mocks/RollupMock.sol b/contracts/test/foundry/bridging/eth/mocks/RollupMock.sol index 53aa34c4..a20c49d5 100644 --- a/contracts/test/foundry/bridging/eth/mocks/RollupMock.sol +++ b/contracts/test/foundry/bridging/eth/mocks/RollupMock.sol @@ -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 })); }