// 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/IZedxionTransport.sol"; interface ICWMintBurnToken is IERC20 { function mint(address to, uint256 amount) external; function burnFrom(address from, uint256 amount) external; } /** * @title ZedxionCustomBridge * @notice GRU custom transport for Chain 138 ↔ ZEDXION (83872). No CCIP. * @dev Deploy at same address on 138 and 83872 via CREATE2 (salt keccak256("ZedxionCustomBridge")). * On 138: lock canonical c* and emit LockForZedxion. * On 83872: relayer calls releaseOnZedxion to mint cW* or transfer escrowed assets. * Reverse: lock on 83872 (LockFor138), relayer completes releaseOn138 on 138. */ contract ZedxionCustomBridge is IZedxionTransport, AccessControl, ReentrancyGuard { using SafeERC20 for IERC20; bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); uint256 public constant ZEDXION_CHAIN_ID = 83872; uint256 public constant HUB_CHAIN_ID = 138; mapping(bytes32 => LockRecord) public locks; mapping(bytes32 => bool) public releasedOnDestination; mapping(address => address) public canonicalToMirrored; mapping(address => address) public mirroredToCanonical; mapping(address => bool) public mintableMirroredToken; mapping(address => uint256) public nonces; bool private _hasRelayer; struct LockRecord { address sender; address token; uint256 amount; address recipient; uint256 createdAt; bool released; } event LockForZedxion( bytes32 indexed requestId, address indexed sender, address indexed token, uint256 amount, address recipient, uint256 sourceChainId ); event UnlockOnZedxion( bytes32 indexed requestId, address indexed recipient, address indexed token, uint256 amount ); event LockFor138( bytes32 indexed requestId, address indexed sender, address indexed token, uint256 amount, address recipient, uint256 sourceChainId ); event UnlockOn138( bytes32 indexed requestId, address indexed recipient, address indexed token, uint256 amount ); event TokenPairConfigured(address indexed canonicalToken, address indexed mirroredToken); constructor(address admin) { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(RELAYER_ROLE, admin); _hasRelayer = true; } function zedxionChainId() external pure override returns (uint256) { return ZEDXION_CHAIN_ID; } function configureTokenPair(address canonicalToken, address mirroredToken) external onlyRole(DEFAULT_ADMIN_ROLE) { require(canonicalToken != address(0) && mirroredToken != address(0), "zero token"); canonicalToMirrored[canonicalToken] = mirroredToken; mirroredToCanonical[mirroredToken] = canonicalToken; emit TokenPairConfigured(canonicalToken, mirroredToken); } function setMintableMirroredToken(address mirroredToken, bool enabled) external onlyRole(DEFAULT_ADMIN_ROLE) { mintableMirroredToken[mirroredToken] = enabled; } function bridgeToZedxion( address token, uint256 amount, address recipientOn83872 ) external payable override nonReentrant returns (bytes32 requestId) { require(block.chainid == HUB_CHAIN_ID, "ZedxionCustomBridge: hub only"); return _lock(token, amount, recipientOn83872, ZEDXION_CHAIN_ID); } function bridgeFromZedxion( address tokenOn83872, uint256 amount, address recipientOn138 ) external payable override nonReentrant returns (bytes32 requestId) { require(block.chainid == ZEDXION_CHAIN_ID, "ZedxionCustomBridge: zedxion only"); return _lock(tokenOn83872, amount, recipientOn138, HUB_CHAIN_ID); } function _lock( address token, uint256 amount, address recipient, uint256 destinationChainId ) internal 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 }); if (destinationChainId == ZEDXION_CHAIN_ID) { emit LockForZedxion(requestId, msg.sender, token, amount, recipient, block.chainid); emit ZedxionBridgeInitiated(requestId, msg.sender, token, amount, recipient, ZEDXION_CHAIN_ID); } else { emit LockFor138(requestId, msg.sender, token, amount, recipient, block.chainid); emit ZedxionBridgeInitiated(requestId, msg.sender, token, amount, recipient, HUB_CHAIN_ID); } } /** * @notice On 83872: mint cW* or release escrow after 138 lock. On 138: release canonical c* after 83872 burn/lock. */ function releaseOnZedxion( bytes32 requestId, address token, uint256 amount, address recipient ) external onlyRole(RELAYER_ROLE) nonReentrant { require(block.chainid == ZEDXION_CHAIN_ID, "ZedxionCustomBridge: zedxion only"); _release(requestId, token, amount, recipient, true); } function releaseOn138( bytes32 requestId, address token, uint256 amount, address recipient ) external onlyRole(RELAYER_ROLE) nonReentrant { require(block.chainid == HUB_CHAIN_ID, "ZedxionCustomBridge: hub only"); _release(requestId, token, amount, recipient, false); } function _release( bytes32 requestId, address token, uint256 amount, address recipient, bool onZedxion ) internal { require(!releasedOnDestination[requestId], "already released"); require(recipient != address(0), "zero recipient"); require(amount > 0, "zero amount"); releasedOnDestination[requestId] = true; if (token == address(0)) { (bool sent,) = payable(recipient).call{value: amount}(""); require(sent, "transfer failed"); } else if (mintableMirroredToken[token]) { ICWMintBurnToken(token).mint(recipient, amount); } else { IERC20(token).safeTransfer(recipient, amount); } emit ZedxionBridgeCompleted(requestId, bytes32(0)); if (onZedxion) { emit UnlockOnZedxion(requestId, recipient, token, amount); } else { emit UnlockOn138(requestId, recipient, token, amount); } } receive() external payable {} }