diff --git a/contracts/src/bridging/eth/L1ETHBridge.sol b/contracts/src/bridging/eth/L1ETHBridge.sol index f60fa8c4..708e133b 100644 --- a/contracts/src/bridging/eth/L1ETHBridge.sol +++ b/contracts/src/bridging/eth/L1ETHBridge.sol @@ -12,12 +12,22 @@ import { IMessageService } from "../../messaging/interfaces/IMessageService.sol" import { IETHYieldManager } from "./interfaces/IETHYieldManager.sol"; contract L1ETHBridge is IL1ETHBridge, Initializable, UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable, MessageServiceBase { + /** + * @notice The message status enum. + */ + enum MessageStatus { + Unknown, + Requested, + Finalized + } + /** * @notice The completed message struct. */ - struct CompletedMessage { + struct Message { uint256 withdrawalRequestId; bytes32 messageHash; + MessageStatus status; } /** @@ -33,7 +43,7 @@ contract L1ETHBridge is IL1ETHBridge, Initializable, UUPSUpgradeable, OwnableUpg /** * @notice The completed messages. */ - mapping(uint256 => CompletedMessage) public completedMessages; + mapping(uint256 => Message) public messages; /** * @dev Ensures the address is not address(0). @@ -117,10 +127,12 @@ contract L1ETHBridge is IL1ETHBridge, Initializable, UUPSUpgradeable, OwnableUpg uint256 _value, bytes memory _calldata ) external nonReentrant onlyMessagingService onlyAuthorizedRemoteSender { - emit MessageCompleted(nextCompletedMessageId); - bytes32 hash = keccak256(abi.encode(_to, _value, _calldata)); uint256 requestId = IETHYieldManager(yieldManager).requestWithdrawal(_value); - completedMessages[nextCompletedMessageId] = CompletedMessage(requestId, hash); + bytes32 hash = keccak256(abi.encode(_to, _value, _calldata)); + messages[nextCompletedMessageId] = Message(requestId, hash, MessageStatus.Requested); + + emit BridgingCompleted(nextCompletedMessageId); + nextCompletedMessageId++; } @@ -142,6 +154,35 @@ contract L1ETHBridge is IL1ETHBridge, Initializable, UUPSUpgradeable, OwnableUpg messageService.sendMessage(remoteSender, 0, data); } + function finalizeWithdrawal(uint256 _messageId, uint256 _hintId, address _to, uint256 _value, bytes memory _calldata) external { + Message memory message = messages[_messageId]; + bytes32 hash = keccak256(abi.encode(_to, _value, _calldata)); + + if (message.status != MessageStatus.Requested) { + revert L1ETHBridge__MessageAlreadyFinalized(); + } + + if (message.messageHash != hash) { + revert L1ETHBridge__InvalidMessageParameters(); + } + + message.status = MessageStatus.Finalized; + + uint256 balanceBefore = address(this).balance; + IETHYieldManager(yieldManager).claimWithdrawal(message.withdrawalRequestId, _hintId); + if (address(this).balance != balanceBefore + _value) { + revert L1ETHBridge__WithdrawalClaimFailed(); + } + + + (bool success, ) = _to.call{ value: _value }(_calldata); + if (!success) { + revert L1ETHBridge__ETHTransferFailed(); + } + + emit MessageFinalized(_messageId); + } + function _authorizeUpgrade(address) internal view override { _checkOwner(); } diff --git a/contracts/src/bridging/eth/interfaces/IETHYieldManager.sol b/contracts/src/bridging/eth/interfaces/IETHYieldManager.sol index 9513d844..e0d47712 100644 --- a/contracts/src/bridging/eth/interfaces/IETHYieldManager.sol +++ b/contracts/src/bridging/eth/interfaces/IETHYieldManager.sol @@ -3,4 +3,6 @@ pragma solidity ^0.8.26; interface IETHYieldManager { function requestWithdrawal(uint256 _amount) external returns (uint256 requestId); + + function claimWithdrawal(uint256 _requestId, uint256 _hintId) external returns (bool success); } \ No newline at end of file diff --git a/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol b/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol index 532b9fb0..cc84fe83 100644 --- a/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol +++ b/contracts/src/bridging/eth/interfaces/IL1ETHBridge.sol @@ -14,7 +14,19 @@ interface IL1ETHBridge { address indexed setBy ); - event MessageCompleted( + /** + * @notice Emitted when a bridging is completed. + * @param messageId The indexed message id. + */ + event BridgingCompleted( + uint256 indexed messageId + ); + + /** + * @notice Emitted when a message is finalized. + * @param messageId The indexed message id. + */ + event MessageFinalized( uint256 indexed messageId ); @@ -34,6 +46,9 @@ interface IL1ETHBridge { error L1ETHBridge__ZeroAddressNotAllowed(); error L1ETHBridge__ETHTransferFailed(); error L1ETHBridge__YieldManagerDepositFailed(); + error L1ETHBridge__MessageAlreadyFinalized(); + error L1ETHBridge__InvalidMessageParameters(); + error L1ETHBridge__WithdrawalClaimFailed(); 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 87718db4..9073dd52 100644 --- a/contracts/test/foundry/bridging/eth/L1ETHBridge.t.sol +++ b/contracts/test/foundry/bridging/eth/L1ETHBridge.t.sol @@ -149,8 +149,9 @@ contract L1ETHBridgeTest is Test { bridge.completeBridging(user1, 100, "test-data"); assertEq(bridge.nextCompletedMessageId(), 1); - (uint256 withdrawalRequestId, bytes32 hash) = bridge.completedMessages(0); + (uint256 withdrawalRequestId, bytes32 hash, L1ETHBridge.MessageStatus status) = bridge.messages(0); assertEq(withdrawalRequestId, 0); assertEq(hash, keccak256(abi.encode(user1, 100, "test-data"))); + assertEq(uint256(status), uint256(L1ETHBridge.MessageStatus.Requested)); } } diff --git a/contracts/test/foundry/bridging/eth/mocks/ETHYieldManagerMock.sol b/contracts/test/foundry/bridging/eth/mocks/ETHYieldManagerMock.sol index 4d378983..94106151 100644 --- a/contracts/test/foundry/bridging/eth/mocks/ETHYieldManagerMock.sol +++ b/contracts/test/foundry/bridging/eth/mocks/ETHYieldManagerMock.sol @@ -24,7 +24,7 @@ contract ETHYieldManagerMock { return deposits[deposits.length - 1]; } - function requestWithdrawal(uint256 _amount) external returns (uint256 requestId) { + function requestWithdrawal(uint256) external returns (uint256 requestId) { return nextRequestId++; } }