// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/IAlltraTransport.sol"; /** * @title AlltraCustomBridge * @notice Custom transport for 138 <-> ALL Mainnet (651940). Locks tokens and emits event; no CCIP. * @dev Deploy at same address on 138 and 651940 via CREATE2. On 138: lock + emit LockForAlltra. * Off-chain relayer or contract on 651940 completes mint/unlock. On 651940: implement * unlockOrMint (called by relayer or same contract) to complete the flow. */ contract AlltraCustomBridge is IAlltraTransport, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); uint256 public constant ALL_MAINNET_CHAIN_ID = 651940; mapping(bytes32 => LockRecord) public locks; mapping(bytes32 => bool) public releasedOnAlltra; // on 651940: prevent double release mapping(address => uint256) public nonces; bool private _hasRelayer; struct LockRecord { address sender; address token; uint256 amount; address recipient; uint256 createdAt; bool released; } event LockForAlltra( bytes32 indexed requestId, address indexed sender, address indexed token, uint256 amount, address recipient, uint256 sourceChainId ); event UnlockOnAlltra( bytes32 indexed requestId, address indexed recipient, address indexed token, uint256 amount ); constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(RELAYER_ROLE, admin); _hasRelayer = true; } /** * @notice Lock tokens and emit event for relay to ALL Mainnet. Does not use CCIP. */ function lockAndRelay( address token, uint256 amount, address recipient ) external payable override nonReentrant returns (bytes32 requestId) { require(recipient != address(0), "zero recipient"); require(amount > 0, "zero amount"); requestId = keccak256(abi.encodePacked( msg.sender, token, amount, recipient, nonces[msg.sender]++, block.chainid, block.timestamp )); if (token == address(0)) { require(msg.value >= amount, "insufficient value"); } else { IERC20(token).safeTransferFrom(msg.sender, address(this), amount); } locks[requestId] = LockRecord({ sender: msg.sender, token: token, amount: amount, recipient: recipient, createdAt: block.timestamp, released: false }); emit LockForAlltra(requestId, msg.sender, token, amount, recipient, block.chainid); return requestId; } function isConfigured() external view override returns (bool) { return _hasRelayer; } /** * @notice On ALL Mainnet (651940): release tokens to recipient after relay proof. * @dev Only RELAYER_ROLE; in production, verify merkle proof or signature from source chain. * Uses releasedOnAlltra[requestId] to prevent double release on this chain. */ function releaseOnAlltra( bytes32 requestId, address token, uint256 amount, address recipient ) external onlyRole(RELAYER_ROLE) nonReentrant { require(!releasedOnAlltra[requestId], "already released"); releasedOnAlltra[requestId] = true; if (token == address(0)) { (bool sent,) = payable(recipient).call{value: amount}(""); require(sent, "transfer failed"); } else { IERC20(token).safeTransfer(recipient, amount); } emit UnlockOnAlltra(requestId, recipient, token, amount); } receive() external payable {} }