// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** * @title Lockbox138 * @notice Asset lock contract on ChainID 138 (Besu) for trustless bridge deposits * @dev Supports both native ETH and ERC-20 tokens. Immutable after deployment (no admin functions). */ contract Lockbox138 is ReentrancyGuard { using SafeERC20 for IERC20; // Replay protection: track nonces per user mapping(address => uint256) public nonces; // Track processed deposit IDs to prevent double deposits mapping(uint256 => bool) public processedDeposits; event Deposit( uint256 indexed depositId, address indexed asset, uint256 amount, address indexed recipient, bytes32 nonce, address depositor, uint256 timestamp ); error ZeroAmount(); error ZeroRecipient(); error ZeroAsset(); error DepositAlreadyProcessed(); error TransferFailed(); /** * @notice Lock native ETH for cross-chain transfer * @param recipient Address on destination chain (Ethereum) to receive funds * @param nonce Unique nonce for this deposit (prevents replay attacks) * @return depositId Unique identifier for this deposit */ function depositNative( address recipient, bytes32 nonce ) external payable nonReentrant returns (uint256 depositId) { if (msg.value == 0) revert ZeroAmount(); if (recipient == address(0)) revert ZeroRecipient(); // Increment user nonce nonces[msg.sender]++; // Generate unique deposit ID depositId = _generateDepositId( address(0), // Native ETH is represented as address(0) msg.value, recipient, nonce ); // Replay protection: ensure deposit ID hasn't been used if (processedDeposits[depositId]) revert DepositAlreadyProcessed(); processedDeposits[depositId] = true; emit Deposit( depositId, address(0), // address(0) represents native ETH msg.value, recipient, nonce, msg.sender, block.timestamp ); return depositId; } /** * @notice Lock ERC-20 tokens (e.g., WETH) for cross-chain transfer * @param token ERC-20 token address to lock * @param amount Amount of tokens to lock * @param recipient Address on destination chain (Ethereum) to receive funds * @param nonce Unique nonce for this deposit (prevents replay attacks) * @return depositId Unique identifier for this deposit */ function depositERC20( address token, uint256 amount, address recipient, bytes32 nonce ) external nonReentrant returns (uint256 depositId) { if (token == address(0)) revert ZeroAsset(); if (amount == 0) revert ZeroAmount(); if (recipient == address(0)) revert ZeroRecipient(); // Increment user nonce nonces[msg.sender]++; // Generate unique deposit ID depositId = _generateDepositId(token, amount, recipient, nonce); // Replay protection: ensure deposit ID hasn't been used if (processedDeposits[depositId]) revert DepositAlreadyProcessed(); processedDeposits[depositId] = true; // Transfer tokens from user to this contract IERC20(token).safeTransferFrom(msg.sender, address(this), amount); emit Deposit( depositId, token, amount, recipient, nonce, msg.sender, block.timestamp ); return depositId; } /** * @notice Get current nonce for a user * @param user Address to check nonce for * @return Current nonce value */ function getNonce(address user) external view returns (uint256) { return nonces[user]; } /** * @notice Check if a deposit ID has been processed * @param depositId Deposit ID to check * @return True if deposit has been processed */ function isDepositProcessed(uint256 depositId) external view returns (bool) { return processedDeposits[depositId]; } /** * @notice Generate a unique deposit ID from deposit parameters * @dev Uses keccak256 hash of all deposit parameters + sender + timestamp to ensure uniqueness * @param asset Asset address (address(0) for native ETH) * @param amount Deposit amount * @param recipient Recipient address on destination chain * @param nonce User-provided nonce * @return depositId Unique deposit identifier */ function _generateDepositId( address asset, uint256 amount, address recipient, bytes32 nonce ) internal view returns (uint256) { return uint256( keccak256( abi.encodePacked( asset, amount, recipient, nonce, msg.sender, block.timestamp, block.number ) ) ); } }