// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title BridgeEscrowVault * @notice Enhanced escrow vault for multi-rail bridging (EVM, XRPL, Fabric) * @dev Supports HSM-backed admin functions via EIP-712 signatures */ contract BridgeEscrowVault is ReentrancyGuard, Pausable, AccessControl, EIP712 { using SafeERC20 for IERC20; bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); bytes32 public constant REFUND_ROLE = keccak256("REFUND_ROLE"); enum DestinationType { EVM, XRPL, FABRIC } enum TransferStatus { INITIATED, DEPOSIT_CONFIRMED, ROUTE_SELECTED, EXECUTING, DESTINATION_SENT, FINALITY_CONFIRMED, COMPLETED, FAILED, REFUND_PENDING, REFUNDED } struct Transfer { bytes32 transferId; address depositor; address asset; // address(0) for native ETH uint256 amount; DestinationType destinationType; bytes destinationData; // Encoded destination address/identifier uint256 timestamp; uint256 timeout; TransferStatus status; bool refunded; } struct RefundRequest { bytes32 transferId; uint256 deadline; bytes hsmSignature; } // EIP-712 type hashes bytes32 private constant REFUND_TYPEHASH = keccak256("RefundRequest(bytes32 transferId,uint256 deadline)"); mapping(bytes32 => Transfer) public transfers; mapping(bytes32 => bool) public processedTransferIds; mapping(address => uint256) public nonces; event Deposit( bytes32 indexed transferId, address indexed depositor, address indexed asset, uint256 amount, DestinationType destinationType, bytes destinationData, uint256 timestamp ); event TransferStatusUpdated( bytes32 indexed transferId, TransferStatus oldStatus, TransferStatus newStatus ); event RefundInitiated( bytes32 indexed transferId, address indexed depositor, uint256 amount ); event RefundExecuted( bytes32 indexed transferId, address indexed depositor, uint256 amount ); error ZeroAmount(); error ZeroRecipient(); error ZeroAsset(); error TransferAlreadyProcessed(); error TransferNotFound(); error TransferNotRefundable(); error InvalidTimeout(); error InvalidSignature(); error TransferNotTimedOut(); error InvalidStatus(); constructor(address admin) EIP712("BridgeEscrowVault", "1") { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(PAUSER_ROLE, admin); _grantRole(OPERATOR_ROLE, admin); _grantRole(REFUND_ROLE, admin); } /** * @notice Deposit native ETH for cross-chain transfer * @param destinationType Type of destination (EVM, XRPL, Fabric) * @param destinationData Encoded destination address/identifier * @param timeout Timeout in seconds for refund eligibility * @param nonce User-provided nonce for replay protection * @return transferId Unique transfer identifier */ function depositNative( DestinationType destinationType, bytes calldata destinationData, uint256 timeout, bytes32 nonce ) external payable nonReentrant whenNotPaused returns (bytes32 transferId) { if (msg.value == 0) revert ZeroAmount(); if (destinationData.length == 0) revert ZeroRecipient(); if (timeout == 0) revert InvalidTimeout(); nonces[msg.sender]++; transferId = _generateTransferId( address(0), msg.value, destinationType, destinationData, nonce ); if (processedTransferIds[transferId]) revert TransferAlreadyProcessed(); processedTransferIds[transferId] = true; transfers[transferId] = Transfer({ transferId: transferId, depositor: msg.sender, asset: address(0), amount: msg.value, destinationType: destinationType, destinationData: destinationData, timestamp: block.timestamp, timeout: timeout, status: TransferStatus.INITIATED, refunded: false }); emit Deposit( transferId, msg.sender, address(0), msg.value, destinationType, destinationData, block.timestamp ); return transferId; } /** * @notice Deposit ERC-20 tokens for cross-chain transfer * @param token ERC-20 token address * @param amount Amount to deposit * @param destinationType Type of destination (EVM, XRPL, Fabric) * @param destinationData Encoded destination address/identifier * @param timeout Timeout in seconds for refund eligibility * @param nonce User-provided nonce for replay protection * @return transferId Unique transfer identifier */ function depositERC20( address token, uint256 amount, DestinationType destinationType, bytes calldata destinationData, uint256 timeout, bytes32 nonce ) external nonReentrant whenNotPaused returns (bytes32 transferId) { if (token == address(0)) revert ZeroAsset(); if (amount == 0) revert ZeroAmount(); if (destinationData.length == 0) revert ZeroRecipient(); if (timeout == 0) revert InvalidTimeout(); nonces[msg.sender]++; transferId = _generateTransferId( token, amount, destinationType, destinationData, nonce ); if (processedTransferIds[transferId]) revert TransferAlreadyProcessed(); processedTransferIds[transferId] = true; IERC20(token).safeTransferFrom(msg.sender, address(this), amount); transfers[transferId] = Transfer({ transferId: transferId, depositor: msg.sender, asset: token, amount: amount, destinationType: destinationType, destinationData: destinationData, timestamp: block.timestamp, timeout: timeout, status: TransferStatus.INITIATED, refunded: false }); emit Deposit( transferId, msg.sender, token, amount, destinationType, destinationData, block.timestamp ); return transferId; } /** * @notice Update transfer status (operator only) * @param transferId Transfer identifier * @param newStatus New status */ function updateTransferStatus( bytes32 transferId, TransferStatus newStatus ) external onlyRole(OPERATOR_ROLE) { Transfer storage transfer = transfers[transferId]; if (transfer.transferId == bytes32(0)) revert TransferNotFound(); TransferStatus oldStatus = transfer.status; transfer.status = newStatus; emit TransferStatusUpdated(transferId, oldStatus, newStatus); } /** * @notice Initiate refund (requires HSM signature) * @param request Refund request with HSM signature * @param hsmSigner HSM signer address */ function initiateRefund( RefundRequest calldata request, address hsmSigner ) external onlyRole(REFUND_ROLE) { Transfer storage transfer = transfers[request.transferId]; if (transfer.transferId == bytes32(0)) revert TransferNotFound(); if (transfer.refunded) revert TransferNotRefundable(); if (block.timestamp < transfer.timestamp + transfer.timeout) { revert TransferNotTimedOut(); } // Verify HSM signature bytes32 structHash = keccak256( abi.encode(REFUND_TYPEHASH, request.transferId, request.deadline) ); bytes32 hash = _hashTypedDataV4(structHash); if (ECDSA.recover(hash, request.hsmSignature) != hsmSigner) { revert InvalidSignature(); } if (block.timestamp > request.deadline) revert InvalidSignature(); transfer.status = TransferStatus.REFUND_PENDING; emit RefundInitiated(request.transferId, transfer.depositor, transfer.amount); } /** * @notice Execute refund after initiation * @param transferId Transfer identifier */ function executeRefund(bytes32 transferId) external nonReentrant onlyRole(REFUND_ROLE) { Transfer storage transfer = transfers[transferId]; if (transfer.transferId == bytes32(0)) revert TransferNotFound(); if (transfer.refunded) revert TransferNotRefundable(); if (transfer.status != TransferStatus.REFUND_PENDING) revert InvalidStatus(); transfer.refunded = true; transfer.status = TransferStatus.REFUNDED; if (transfer.asset == address(0)) { (bool success, ) = transfer.depositor.call{value: transfer.amount}(""); require(success, "Refund failed"); } else { IERC20(transfer.asset).safeTransfer(transfer.depositor, transfer.amount); } emit RefundExecuted(transferId, transfer.depositor, transfer.amount); } /** * @notice Get transfer details * @param transferId Transfer identifier * @return Transfer struct */ function getTransfer(bytes32 transferId) external view returns (Transfer memory) { return transfers[transferId]; } /** * @notice Check if transfer is refundable * @param transferId Transfer identifier * @return True if refundable */ function isRefundable(bytes32 transferId) external view returns (bool) { Transfer storage transfer = transfers[transferId]; if (transfer.transferId == bytes32(0)) return false; if (transfer.refunded) return false; return block.timestamp >= transfer.timestamp + transfer.timeout; } /** * @notice Generate unique transfer ID * @param asset Asset address * @param amount Amount * @param destinationType Destination type * @param destinationData Destination data * @param nonce User nonce * @return transferId Unique identifier */ function _generateTransferId( address asset, uint256 amount, DestinationType destinationType, bytes calldata destinationData, bytes32 nonce ) internal view returns (bytes32) { return keccak256( abi.encodePacked( asset, amount, uint8(destinationType), destinationData, nonce, msg.sender, block.timestamp, block.number ) ); } /** * @notice Pause contract */ function pause() external onlyRole(PAUSER_ROLE) { _pause(); } /** * @notice Unpause contract */ function unpause() external onlyRole(PAUSER_ROLE) { _unpause(); } }