feat: implement L2ETHBridge.bridgeETH and L1ETHBridge.completeBridge

This commit is contained in:
Andrea Franz
2025-06-26 13:59:34 +02:00
parent b8f6c7b230
commit dc1549d03d
8 changed files with 163 additions and 7 deletions

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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];
}
}

View File

@@ -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 }));
}